feat(agents): concurrent all-agent-driver upgrade
This commit is contained in:
1 parent
2a99f91099
commit
95db01e359
15 files changed
+1083
-282
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, computed, watch, nextTick, type Ref } from "vue";
|
import { ref, reactive, onMounted, onUnmounted, computed, watch, nextTick } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { Activity, ExternalLink, Cpu, FolderOpen, FolderSync, MemoryStick, Search, Square, Trash2, Download, RotateCcw, Loader2, RefreshCw, Check, Clock3, FileUp } from "@lucide/vue";
|
import { Activity, ExternalLink, Cpu, FolderOpen, FolderSync, MemoryStick, Search, Square, Trash2, Download, RotateCcw, Loader2, RefreshCw, Check, Clock3, FileUp } from "@lucide/vue";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -18,7 +18,17 @@ import type { JdbcDriverInfo, JdbcLocalBundleInfo, JdbcMavenBundleInfo, JdbcPlug
|
|||||||
import * as api from "@/lib/backend/api";
|
import * as api from "@/lib/backend/api";
|
||||||
import type { AgentDriverInfo, DriverRuntimeInfo, DriverRuntimeSummary, DriverStoreUsage, JavaRuntimeConfig } from "@/lib/backend/api";
|
import type { AgentDriverInfo, DriverRuntimeInfo, DriverRuntimeSummary, DriverStoreUsage, JavaRuntimeConfig } from "@/lib/backend/api";
|
||||||
import { formatRuntimeBytes, formatRuntimeCpu, formatRuntimeUptime, runtimeHealthClass, runtimeStatusClass, runtimeStatusDotClass } from "@/lib/connection/driverRuntimePresentation";
|
import { formatRuntimeBytes, formatRuntimeCpu, formatRuntimeUptime, runtimeHealthClass, runtimeStatusClass, runtimeStatusDotClass } from "@/lib/connection/driverRuntimePresentation";
|
||||||
import { addDriverInstallQueue, driverInstallProgressChannel, driverInstallProgressPercent, isDriverInstallProgressTarget, removeDriverInstallQueue, takeNextDriverInstallQueue, updateDriverInstallProgress, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
|
import {
|
||||||
|
addDriverInstallQueue,
|
||||||
|
driverInstallProgressChannel,
|
||||||
|
driverInstallProgressPercent,
|
||||||
|
isDriverInstallProgressForOperation,
|
||||||
|
isDriverInstallProgressTarget,
|
||||||
|
removeDriverInstallQueue,
|
||||||
|
takeNextDriverInstallQueue,
|
||||||
|
updatePerDriverProgress,
|
||||||
|
type DriverInstallProgress,
|
||||||
|
} from "@/lib/connection/driverInstallProgressUi";
|
||||||
import { PRESTOSQL_DRIVER_DB_TYPE, prestoSqlBuiltinDriverRow, prestoSqlMavenBundle } from "@/lib/database/prestoSqlBuiltinDriver";
|
import { PRESTOSQL_DRIVER_DB_TYPE, prestoSqlBuiltinDriverRow, prestoSqlMavenBundle } from "@/lib/database/prestoSqlBuiltinDriver";
|
||||||
import type { DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint";
|
import type { DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint";
|
||||||
import { isOfflineDriverPackage, webDriverImportAccept } from "@/lib/driverStore/driverImportSelection";
|
import { isOfflineDriverPackage, webDriverImportAccept } from "@/lib/driverStore/driverImportSelection";
|
||||||
@@ -166,13 +176,13 @@ const drivers = ref<AgentDriverInfo[]>([]);
|
|||||||
const agentDriverSearch = ref("");
|
const agentDriverSearch = ref("");
|
||||||
const installing = ref<string | null>(null);
|
const installing = ref<string | null>(null);
|
||||||
const upgradingAll = ref(false);
|
const upgradingAll = ref(false);
|
||||||
const upgradingCurrent = ref("");
|
const upgradingCompletedCount = ref(0);
|
||||||
const upgradingIndex = ref(0);
|
|
||||||
const upgradingTotal = ref(0);
|
const upgradingTotal = ref(0);
|
||||||
const queuedDriverInstalls = ref<string[]>([]);
|
const queuedDriverInstalls = ref<string[]>([]);
|
||||||
const reinstallingJre = ref<string | null>(null);
|
const reinstallingJre = ref<string | null>(null);
|
||||||
|
const activeAgentOperationId = ref<string | null>(null);
|
||||||
const refreshing = ref(false);
|
const refreshing = ref(false);
|
||||||
const agentProgress = ref<DriverInstallProgress | null>(null);
|
const agentProgressByDbType = reactive<Record<string, DriverInstallProgress | null | undefined>>({});
|
||||||
const jdbcPluginProgress = ref<DriverInstallProgress | null>(null);
|
const jdbcPluginProgress = ref<DriverInstallProgress | null>(null);
|
||||||
const javaRuntimeConfig = ref<JavaRuntimeConfig>({ mode: "managed", custom_java_path: null });
|
const javaRuntimeConfig = ref<JavaRuntimeConfig>({ mode: "managed", custom_java_path: null });
|
||||||
const customJavaPath = ref("");
|
const customJavaPath = ref("");
|
||||||
@@ -188,7 +198,7 @@ const DRIVER_RUNTIME_POLL_MS = 5000;
|
|||||||
const OFFLINE_DRIVER_DOWNLOAD_URL = "https://dbxio.com/cn/drivers";
|
const OFFLINE_DRIVER_DOWNLOAD_URL = "https://dbxio.com/cn/drivers";
|
||||||
|
|
||||||
let unlisten: (() => void) | null = null;
|
let unlisten: (() => void) | null = null;
|
||||||
const lastAgentProgressPercent = ref<number | null>(null);
|
const lastAgentProgressPercent: Record<string, number> = {};
|
||||||
const lastJdbcPluginProgressPercent = ref<number | null>(null);
|
const lastJdbcPluginProgressPercent = ref<number | null>(null);
|
||||||
|
|
||||||
const installedJres = computed(() => {
|
const installedJres = computed(() => {
|
||||||
@@ -201,7 +211,7 @@ const installedJres = computed(() => {
|
|||||||
return [...jreMap.entries()].map(([key, installed]) => ({ key, installed })).sort((a, b) => b.key.localeCompare(a.key));
|
return [...jreMap.entries()].map(([key, installed]) => ({ key, installed })).sort((a, b) => b.key.localeCompare(a.key));
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatProgressText(p: DriverInstallProgress | null, includeBatch: boolean): string {
|
function formatProgressText(p: DriverInstallProgress | null | undefined): string {
|
||||||
if (!p) return "";
|
if (!p) return "";
|
||||||
if (p.step === "jre-extract") return t("driverStore.progressJreExtract");
|
if (p.step === "jre-extract") return t("driverStore.progressJreExtract");
|
||||||
if (p.step === "jdbc-plugin-extract") return t("driverStore.progressJdbcPluginExtract");
|
if (p.step === "jdbc-plugin-extract") return t("driverStore.progressJdbcPluginExtract");
|
||||||
@@ -210,27 +220,49 @@ function formatProgressText(p: DriverInstallProgress | null, includeBatch: boole
|
|||||||
const pct = Math.round(((p.downloaded ?? 0) / p.total) * 100);
|
const pct = Math.round(((p.downloaded ?? 0) / p.total) * 100);
|
||||||
const dl = formatSize(p.downloaded ?? 0);
|
const dl = formatSize(p.downloaded ?? 0);
|
||||||
const total = formatSize(p.total);
|
const total = formatSize(p.total);
|
||||||
const prefix = includeBatch && upgradingAll.value && upgradingCurrent.value ? `[${upgradingIndex.value}/${upgradingTotal.value}] ${upgradingCurrent.value} - ` : "";
|
return `${label} ${dl} / ${total} (${pct}%)`;
|
||||||
return `${prefix}${label} ${dl} / ${total} (${pct}%)`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const agentProgressText = computed(() => formatProgressText(agentProgress.value, true));
|
function getAgentProgressText(dbType: string): string {
|
||||||
const jdbcPluginProgressText = computed(() => formatProgressText(jdbcPluginProgress.value, false));
|
return formatProgressText(agentProgressByDbType[dbType]);
|
||||||
|
}
|
||||||
|
|
||||||
function progressNumber(progress: DriverInstallProgress | null, lastProgressPercent: Ref<number | null>): number | null {
|
function getJdbcPluginProgressTitle(fallback: string): string {
|
||||||
|
return jdbcPluginProgressText.value || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAgentProgressTitle(dbType: string, fallback: string): string {
|
||||||
|
return getAgentProgressText(dbType) || formatProgressText(agentProgressByDbType[dbType]) || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jdbcPluginProgressText = computed(() => formatProgressText(jdbcPluginProgress.value));
|
||||||
|
|
||||||
|
function getAgentProgressPercent(dbType: string): number | null {
|
||||||
|
const progress = agentProgressByDbType[dbType];
|
||||||
const next = driverInstallProgressPercent(progress);
|
const next = driverInstallProgressPercent(progress);
|
||||||
if (next !== null) {
|
if (next !== null) {
|
||||||
lastProgressPercent.value = next;
|
lastAgentProgressPercent[dbType] = next;
|
||||||
}
|
}
|
||||||
return next ?? lastProgressPercent.value;
|
return next ?? lastAgentProgressPercent[dbType] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const agentProgressNumber = computed(() => progressNumber(agentProgress.value, lastAgentProgressPercent));
|
const jdbcPluginProgressNumber = computed(() => {
|
||||||
const jdbcPluginProgressNumber = computed(() => progressNumber(jdbcPluginProgress.value, lastJdbcPluginProgressPercent));
|
const next = driverInstallProgressPercent(jdbcPluginProgress.value);
|
||||||
|
if (next !== null) {
|
||||||
|
lastJdbcPluginProgressPercent.value = next;
|
||||||
|
}
|
||||||
|
return next ?? lastJdbcPluginProgressPercent.value;
|
||||||
|
});
|
||||||
|
|
||||||
function resetAgentInstallProgress() {
|
function resetAgentInstallProgress() {
|
||||||
agentProgress.value = null;
|
for (const key of Object.keys(agentProgressByDbType)) {
|
||||||
lastAgentProgressPercent.value = null;
|
delete agentProgressByDbType[key];
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(lastAgentProgressPercent)) {
|
||||||
|
delete lastAgentProgressPercent[key];
|
||||||
|
}
|
||||||
|
jreReinstallProgress.value = null;
|
||||||
|
lastJreReinstallPercent.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetJdbcPluginInstallProgress() {
|
function resetJdbcPluginInstallProgress() {
|
||||||
@@ -281,7 +313,7 @@ function isDriverProgressActive(dbType: string): boolean {
|
|||||||
return isDriverInstallProgressTarget(dbType, {
|
return isDriverInstallProgressTarget(dbType, {
|
||||||
installing: installing.value,
|
installing: installing.value,
|
||||||
upgradingAll: upgradingAll.value,
|
upgradingAll: upgradingAll.value,
|
||||||
progress: agentProgress.value,
|
progressMap: agentProgressByDbType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,12 +321,18 @@ function driverRequiresJavaRuntime(driver: AgentDriverInfo): boolean {
|
|||||||
return driver.requires_java_runtime ?? Boolean(driver.jre);
|
return driver.requires_java_runtime ?? Boolean(driver.jre);
|
||||||
}
|
}
|
||||||
|
|
||||||
function agentProgressTitle(fallback: string): string {
|
// JRE reinstall is single-threaded, so a simpler model suffices.
|
||||||
return agentProgressText.value || fallback;
|
const jreReinstallProgress = ref<DriverInstallProgress | null>(null);
|
||||||
|
const lastJreReinstallPercent = ref<number | null>(null);
|
||||||
|
|
||||||
|
function getJreReinstallPercent(): number | null {
|
||||||
|
const next = driverInstallProgressPercent(jreReinstallProgress.value);
|
||||||
|
if (next !== null) lastJreReinstallPercent.value = next;
|
||||||
|
return next ?? lastJreReinstallPercent.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function jdbcPluginProgressTitle(fallback: string): string {
|
function getJreReinstallTitle(fallback: string): string {
|
||||||
return jdbcPluginProgressText.value || fallback;
|
return formatProgressText(jreReinstallProgress.value) || fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPrestoSqlBuiltinDriver(dbType: string): boolean {
|
function isPrestoSqlBuiltinDriver(dbType: string): boolean {
|
||||||
@@ -408,6 +446,7 @@ async function installDriver(dbType: string) {
|
|||||||
async function runDriverInstall(dbType: string) {
|
async function runDriverInstall(dbType: string) {
|
||||||
const label = driverLabel(dbType);
|
const label = driverLabel(dbType);
|
||||||
installing.value = dbType;
|
installing.value = dbType;
|
||||||
|
activeAgentOperationId.value = crypto.randomUUID();
|
||||||
resetAgentInstallProgress();
|
resetAgentInstallProgress();
|
||||||
try {
|
try {
|
||||||
if (isPrestoSqlBuiltinDriver(dbType)) {
|
if (isPrestoSqlBuiltinDriver(dbType)) {
|
||||||
@@ -426,13 +465,14 @@ async function runDriverInstall(dbType: string) {
|
|||||||
toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") }));
|
toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await api.installAgent(dbType);
|
await api.installAgent(dbType, activeAgentOperationId.value);
|
||||||
await refreshAgents();
|
await refreshAgents();
|
||||||
toast(t("driverStore.driverInstallSuccess", { label }));
|
toast(t("driverStore.driverInstallSuccess", { label }));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
toast(t("driverStore.driverInstallFailed", { label, error: e }));
|
toast(t("driverStore.driverInstallFailed", { label, error: e }));
|
||||||
} finally {
|
} finally {
|
||||||
installing.value = null;
|
installing.value = null;
|
||||||
|
activeAgentOperationId.value = null;
|
||||||
resetAgentInstallProgress();
|
resetAgentInstallProgress();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,16 +490,19 @@ async function runQueuedDriverInstalls() {
|
|||||||
|
|
||||||
async function upgradeAll() {
|
async function upgradeAll() {
|
||||||
upgradingAll.value = true;
|
upgradingAll.value = true;
|
||||||
|
activeAgentOperationId.value = crypto.randomUUID();
|
||||||
|
upgradingCompletedCount.value = 0;
|
||||||
queuedDriverInstalls.value = [];
|
queuedDriverInstalls.value = [];
|
||||||
resetAgentInstallProgress();
|
resetAgentInstallProgress();
|
||||||
try {
|
try {
|
||||||
const updatableDbTypes = drivers.value.filter((driver) => driver.update_available).map((driver) => driver.db_type);
|
const updatableDbTypes = drivers.value.filter((driver) => driver.update_available).map((driver) => driver.db_type);
|
||||||
|
upgradingTotal.value = updatableDbTypes.length;
|
||||||
const blockers = await api.checkAgentUpdateBlockers(updatableDbTypes);
|
const blockers = await api.checkAgentUpdateBlockers(updatableDbTypes);
|
||||||
if (blockers.length > 0) {
|
if (blockers.length > 0) {
|
||||||
toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") }));
|
toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await api.upgradeAllAgents();
|
const result = await api.upgradeAllAgents(activeAgentOperationId.value);
|
||||||
await refreshAgents();
|
await refreshAgents();
|
||||||
if (result.failed.length > 0) {
|
if (result.failed.length > 0) {
|
||||||
const failedLabels = result.failed.map((item) => drivers.value.find((driver) => driver.db_type === item.db_type)?.label ?? item.db_type).join(", ");
|
const failedLabels = result.failed.map((item) => drivers.value.find((driver) => driver.db_type === item.db_type)?.label ?? item.db_type).join(", ");
|
||||||
@@ -471,8 +514,8 @@ async function upgradeAll() {
|
|||||||
toast(t("driverStore.upgradeAllFailed", { error: e }));
|
toast(t("driverStore.upgradeAllFailed", { error: e }));
|
||||||
} finally {
|
} finally {
|
||||||
upgradingAll.value = false;
|
upgradingAll.value = false;
|
||||||
upgradingCurrent.value = "";
|
activeAgentOperationId.value = null;
|
||||||
upgradingIndex.value = 0;
|
upgradingCompletedCount.value = 0;
|
||||||
upgradingTotal.value = 0;
|
upgradingTotal.value = 0;
|
||||||
resetAgentInstallProgress();
|
resetAgentInstallProgress();
|
||||||
}
|
}
|
||||||
@@ -554,15 +597,17 @@ async function importOfflineZip() {
|
|||||||
}
|
}
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
importingZip.value = true;
|
importingZip.value = true;
|
||||||
|
activeAgentOperationId.value = crypto.randomUUID();
|
||||||
resetAgentInstallProgress();
|
resetAgentInstallProgress();
|
||||||
try {
|
try {
|
||||||
const count = await api.importAgentsFromZip(selected);
|
const count = await api.importAgentsFromZip(selected, activeAgentOperationId.value);
|
||||||
await refreshAgents();
|
await refreshAgents();
|
||||||
toast(t("driverStore.offlineImportSuccess", { count }));
|
toast(t("driverStore.offlineImportSuccess", { count }));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
toast(t("driverStore.offlineImportFailed", { error: e }));
|
toast(t("driverStore.offlineImportFailed", { error: e }));
|
||||||
} finally {
|
} finally {
|
||||||
importingZip.value = false;
|
importingZip.value = false;
|
||||||
|
activeAgentOperationId.value = null;
|
||||||
resetAgentInstallProgress();
|
resetAgentInstallProgress();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -583,9 +628,16 @@ async function importDriverFile(driver: AgentDriverInfo) {
|
|||||||
const isWindows = navigator.userAgent.toLowerCase().includes("windows");
|
const isWindows = navigator.userAgent.toLowerCase().includes("windows");
|
||||||
const installSelectedFile = async (selected: string | File) => {
|
const installSelectedFile = async (selected: string | File) => {
|
||||||
if (isOfflineDriverPackage(selected)) {
|
if (isOfflineDriverPackage(selected)) {
|
||||||
const count = await api.importAgentsFromZip(selected);
|
activeAgentOperationId.value = crypto.randomUUID();
|
||||||
await refreshAgents();
|
resetAgentInstallProgress();
|
||||||
toast(t("driverStore.offlineImportSuccess", { count }));
|
try {
|
||||||
|
const count = await api.importAgentsFromZip(selected, activeAgentOperationId.value);
|
||||||
|
await refreshAgents();
|
||||||
|
toast(t("driverStore.offlineImportSuccess", { count }));
|
||||||
|
} finally {
|
||||||
|
activeAgentOperationId.value = null;
|
||||||
|
resetAgentInstallProgress();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await api.importAgentDriver(dbType, selected);
|
await api.importAgentDriver(dbType, selected);
|
||||||
await refreshAgents();
|
await refreshAgents();
|
||||||
@@ -620,15 +672,17 @@ async function importDriverFile(driver: AgentDriverInfo) {
|
|||||||
|
|
||||||
async function reinstallJre(jreKey: string) {
|
async function reinstallJre(jreKey: string) {
|
||||||
reinstallingJre.value = jreKey;
|
reinstallingJre.value = jreKey;
|
||||||
|
activeAgentOperationId.value = crypto.randomUUID();
|
||||||
resetAgentInstallProgress();
|
resetAgentInstallProgress();
|
||||||
try {
|
try {
|
||||||
await api.reinstallJre(jreKey);
|
await api.reinstallJre(jreKey, activeAgentOperationId.value);
|
||||||
await refreshAgents();
|
await refreshAgents();
|
||||||
toast(t("driverStore.jreReinstallSuccess", { jre: jreKey }));
|
toast(t("driverStore.jreReinstallSuccess", { jre: jreKey }));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
toast(t("driverStore.jreReinstallFailed", { jre: jreKey, error: e }));
|
toast(t("driverStore.jreReinstallFailed", { jre: jreKey, error: e }));
|
||||||
} finally {
|
} finally {
|
||||||
reinstallingJre.value = null;
|
reinstallingJre.value = null;
|
||||||
|
activeAgentOperationId.value = null;
|
||||||
resetAgentInstallProgress();
|
resetAgentInstallProgress();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1133,25 +1187,36 @@ onMounted(async () => {
|
|||||||
|
|
||||||
unlisten = await api.listenAgentInstallProgress((payload) => {
|
unlisten = await api.listenAgentInstallProgress((payload) => {
|
||||||
const incoming = payload as DriverInstallProgress;
|
const incoming = payload as DriverInstallProgress;
|
||||||
|
if (!isDriverInstallProgressForOperation(incoming, activeAgentOperationId.value)) return;
|
||||||
const channel = driverInstallProgressChannel(incoming);
|
const channel = driverInstallProgressChannel(incoming);
|
||||||
const jdbcProgressBelongsToPrestoSql = channel === "jdbc-plugin" && installing.value === PRESTOSQL_DRIVER_DB_TYPE && !isInstallingJdbcPlugin.value;
|
const jdbcProgressBelongsToPrestoSql = channel === "jdbc-plugin" && installing.value === PRESTOSQL_DRIVER_DB_TYPE && !isInstallingJdbcPlugin.value;
|
||||||
if (jdbcProgressBelongsToPrestoSql) {
|
if (jdbcProgressBelongsToPrestoSql) {
|
||||||
// PrestoSQL is shown as a built-in driver but installs through the JDBC plugin pipeline.
|
// PrestoSQL is shown as a built-in driver but installs through the JDBC plugin pipeline.
|
||||||
agentProgress.value = updateDriverInstallProgress(agentProgress.value, incoming, "jdbc-plugin");
|
// Route its events into the per-driver map using the presto key.
|
||||||
} else {
|
updatePerDriverProgress(agentProgressByDbType, { ...incoming, db_type: PRESTOSQL_DRIVER_DB_TYPE });
|
||||||
agentProgress.value = updateDriverInstallProgress(agentProgress.value, incoming, "agent");
|
} else if (channel === "agent") {
|
||||||
jdbcPluginProgress.value = updateDriverInstallProgress(jdbcPluginProgress.value, incoming, "jdbc-plugin");
|
if (incoming.db_type) {
|
||||||
|
updatePerDriverProgress(agentProgressByDbType, incoming);
|
||||||
|
// Track completions for the batch counter.
|
||||||
|
if (incoming.step === "done" && upgradingAll.value) {
|
||||||
|
upgradingCompletedCount.value++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No db_type — single operation (e.g. JRE reinstall).
|
||||||
|
jreReinstallProgress.value = incoming.step === "done" ? null : incoming;
|
||||||
|
}
|
||||||
|
jdbcPluginProgress.value = null; // clear any stale jdbc-only progress
|
||||||
|
} else if (channel === "jdbc-plugin") {
|
||||||
|
jdbcPluginProgress.value = incoming.step === "done" ? null : incoming;
|
||||||
}
|
}
|
||||||
if (payload.db_type && payload.total_drivers) {
|
if (payload.total_drivers && !upgradingTotal.value) {
|
||||||
upgradingCurrent.value = drivers.value.find((d) => d.db_type === payload.db_type)?.label ?? payload.db_type;
|
upgradingTotal.value = payload.total_drivers;
|
||||||
upgradingIndex.value = payload.current ?? 0;
|
|
||||||
upgradingTotal.value = payload.total_drivers ?? 0;
|
|
||||||
}
|
}
|
||||||
// During a batch upgrade, refresh the list as soon as each driver finishes
|
// During a batch upgrade, refresh the list as soon as each driver finishes
|
||||||
// (step="done") so its "Update" button disappears immediately instead of
|
// (step="done") so its "Update" button disappears immediately instead of
|
||||||
// staying disabled until the whole batch completes (step="all-done").
|
// staying disabled until the whole batch completes (step="all-done").
|
||||||
// Single-driver installs (upgradingAll=false) are refreshed by runDriverInstall.
|
// Single-driver installs (upgradingAll=false) are refreshed by runDriverInstall.
|
||||||
if (upgradingAll.value && payload.step === "done" && channel === "agent") {
|
if (upgradingAll.value && payload.step === "done" && channel === "agent" && payload.db_type) {
|
||||||
void refreshAgents();
|
void refreshAgents();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1205,7 +1270,7 @@ watch(driverStoreTab, (tab) => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="sm" class="h-7 rounded-md text-xs gap-1 text-muted-foreground" :disabled="importingZip" @click="importOfflineZip">
|
<Button variant="ghost" size="sm" class="h-7 rounded-md text-xs gap-1 text-muted-foreground" :disabled="importingZip || installing !== null || upgradingAll || reinstallingJre !== null || queuedDriverInstalls.length > 0" @click="importOfflineZip">
|
||||||
<FileUp class="h-3.5 w-3.5" />
|
<FileUp class="h-3.5 w-3.5" />
|
||||||
{{ importingZip ? t("driverStore.importing") : t("driverStore.importOfflinePackage") }}
|
{{ importingZip ? t("driverStore.importing") : t("driverStore.importOfflinePackage") }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1256,16 +1321,16 @@ watch(driverStoreTab, (tab) => {
|
|||||||
</span>
|
</span>
|
||||||
<Check v-if="jre.installed" class="h-4 w-4 text-green-600" />
|
<Check v-if="jre.installed" class="h-4 w-4 text-green-600" />
|
||||||
<span v-else class="text-xs text-muted-foreground">{{ t("driverStore.notInstalled") }}</span>
|
<span v-else class="text-xs text-muted-foreground">{{ t("driverStore.notInstalled") }}</span>
|
||||||
<DriverInstallProgressCircle v-if="reinstallingJre === jre.key" :percent="agentProgressNumber" :title="agentProgressTitle(jre.installed ? t('driverStore.reinstalling') : t('driverStore.installing'))" />
|
<DriverInstallProgressCircle v-if="reinstallingJre === jre.key" :percent="getJreReinstallPercent()" :title="getJreReinstallTitle(jre.installed ? t('driverStore.reinstalling') : t('driverStore.installing'))" />
|
||||||
<Button v-else-if="!jre.installed" type="button" variant="default" size="sm" class="h-8 rounded-md text-xs" :disabled="reinstallingJre !== null || installing !== null" @click="reinstallJre(jre.key)">
|
<Button v-else-if="!jre.installed" type="button" variant="default" size="sm" class="h-8 rounded-md text-xs" :disabled="reinstallingJre !== null || installing !== null || importingZip" @click="reinstallJre(jre.key)">
|
||||||
<Download class="h-3.5 w-3.5 mr-1" />
|
<Download class="h-3.5 w-3.5 mr-1" />
|
||||||
{{ t("driverStore.install") }}
|
{{ t("driverStore.install") }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-else-if="jre.installed" type="button" variant="outline" size="sm" class="h-8 rounded-md text-xs" :disabled="reinstallingJre !== null || installing !== null" @click="reinstallJre(jre.key)">
|
<Button v-else-if="jre.installed" type="button" variant="outline" size="sm" class="h-8 rounded-md text-xs" :disabled="reinstallingJre !== null || installing !== null || importingZip" @click="reinstallJre(jre.key)">
|
||||||
<RotateCcw class="h-3.5 w-3.5 mr-1" />
|
<RotateCcw class="h-3.5 w-3.5 mr-1" />
|
||||||
{{ t("driverStore.reinstall") }}
|
{{ t("driverStore.reinstall") }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-if="jre.installed" type="button" variant="ghost" size="sm" class="h-8 rounded-md text-xs text-muted-foreground hover:text-destructive" :disabled="reinstallingJre !== null || installing !== null" @click="uninstallJre(jre.key)">
|
<Button v-if="jre.installed" type="button" variant="ghost" size="sm" class="h-8 rounded-md text-xs text-muted-foreground hover:text-destructive" :disabled="reinstallingJre !== null || installing !== null || importingZip" @click="uninstallJre(jre.key)">
|
||||||
{{ t("driverStore.uninstall") }}
|
{{ t("driverStore.uninstall") }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1291,10 +1356,10 @@ watch(driverStoreTab, (tab) => {
|
|||||||
<div class="text-sm font-semibold">{{ t("driverStore.updatesAvailableTitle") }} ({{ filteredUpdatableCount }})</div>
|
<div class="text-sm font-semibold">{{ t("driverStore.updatesAvailableTitle") }} ({{ filteredUpdatableCount }})</div>
|
||||||
<p class="text-xs text-muted-foreground">{{ t("driverStore.updatesAvailableDescription") }}</p>
|
<p class="text-xs text-muted-foreground">{{ t("driverStore.updatesAvailableDescription") }}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" class="h-7 rounded-md text-xs shrink-0 ml-3" :disabled="installing !== null || upgradingAll" @click="upgradeAll">
|
<Button size="sm" class="h-7 rounded-md text-xs shrink-0 ml-3" :disabled="installing !== null || upgradingAll || importingZip" @click="upgradeAll">
|
||||||
<Loader2 v-if="upgradingAll" class="h-3 w-3 animate-spin mr-1" />
|
<Loader2 v-if="upgradingAll" class="h-3 w-3 animate-spin mr-1" />
|
||||||
<Download v-else class="h-3 w-3 mr-1" />
|
<Download v-else class="h-3 w-3 mr-1" />
|
||||||
{{ upgradingAll ? t("driverStore.upgradingProgress", { current: upgradingIndex, total: upgradingTotal }) : t("driverStore.upgradeAll") }}
|
{{ upgradingAll ? t("driverStore.upgradingProgress", { current: upgradingCompletedCount, total: upgradingTotal }) : t("driverStore.upgradeAll") }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -1318,12 +1383,19 @@ watch(driverStoreTab, (tab) => {
|
|||||||
<span v-if="formatSize(driver.size)" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">{{ formatSize(driver.size) }}</span>
|
<span v-if="formatSize(driver.size)" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">{{ formatSize(driver.size) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="driver-store-agent-actions flex shrink-0 items-center gap-2">
|
<div class="driver-store-agent-actions flex shrink-0 items-center gap-2">
|
||||||
<Button v-if="!driver.installed && isDriverQueued(driver.db_type)" size="sm" variant="outline" class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15" :disabled="upgradingAll" @click="removeQueuedDriverInstall(driver.db_type)">
|
<Button
|
||||||
|
v-if="!driver.installed && isDriverQueued(driver.db_type)"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
|
||||||
|
:disabled="upgradingAll || importingZip"
|
||||||
|
@click="removeQueuedDriverInstall(driver.db_type)"
|
||||||
|
>
|
||||||
<Clock3 class="h-3 w-3 mr-1" />
|
<Clock3 class="h-3 w-3 mr-1" />
|
||||||
{{ t("driverStore.queued") }}
|
{{ t("driverStore.queued") }}
|
||||||
</Button>
|
</Button>
|
||||||
<DriverInstallProgressCircle v-else-if="!driver.installed && isDriverProgressActive(driver.db_type)" :percent="agentProgressNumber" :title="agentProgressTitle(t('driverStore.installing'))" />
|
<DriverInstallProgressCircle v-else-if="!driver.installed && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.installing'))" />
|
||||||
<Button v-else-if="!driver.installed" size="sm" class="h-7 rounded-md text-xs" :disabled="upgradingAll" @click="installDriver(driver.db_type)">
|
<Button v-else-if="!driver.installed" size="sm" class="h-7 rounded-md text-xs" :disabled="upgradingAll || importingZip" @click="installDriver(driver.db_type)">
|
||||||
<Download class="h-3 w-3 mr-1" />
|
<Download class="h-3 w-3 mr-1" />
|
||||||
{{ t("driverStore.install") }}
|
{{ t("driverStore.install") }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1333,7 +1405,7 @@ watch(driverStoreTab, (tab) => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-7 w-7 rounded-md text-xs text-muted-foreground"
|
class="h-7 w-7 rounded-md text-xs text-muted-foreground"
|
||||||
:title="t('driverStore.importLocalJar')"
|
:title="t('driverStore.importLocalJar')"
|
||||||
:disabled="upgradingAll || installing !== null"
|
:disabled="upgradingAll || installing !== null || importingZip"
|
||||||
@click="importDriverFile(driver)"
|
@click="importDriverFile(driver)"
|
||||||
>
|
>
|
||||||
<FileUp class="h-3.5 w-3.5" />
|
<FileUp class="h-3.5 w-3.5" />
|
||||||
@@ -1344,17 +1416,17 @@ watch(driverStoreTab, (tab) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
|
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
|
||||||
:disabled="upgradingAll"
|
:disabled="upgradingAll || importingZip"
|
||||||
@click="removeQueuedDriverInstall(driver.db_type)"
|
@click="removeQueuedDriverInstall(driver.db_type)"
|
||||||
>
|
>
|
||||||
<Clock3 class="h-3 w-3 mr-1" />
|
<Clock3 class="h-3 w-3 mr-1" />
|
||||||
{{ t("driverStore.queued") }}
|
{{ t("driverStore.queued") }}
|
||||||
</Button>
|
</Button>
|
||||||
<DriverInstallProgressCircle v-else-if="driver.installed && driver.update_available && isDriverProgressActive(driver.db_type)" :percent="agentProgressNumber" :title="agentProgressTitle(t('driverStore.updating'))" />
|
<DriverInstallProgressCircle v-else-if="driver.installed && driver.update_available && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.updating'))" />
|
||||||
<Button v-else-if="driver.installed && driver.update_available" size="sm" variant="outline" class="h-7 rounded-md text-xs" :disabled="upgradingAll" @click="installDriver(driver.db_type)">
|
<Button v-else-if="driver.installed && driver.update_available" size="sm" variant="outline" class="h-7 rounded-md text-xs" :disabled="upgradingAll || importingZip" @click="installDriver(driver.db_type)">
|
||||||
{{ t("driverStore.update") }}
|
{{ t("driverStore.update") }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-if="driver.installed" variant="ghost" size="sm" class="h-7 rounded-md text-xs text-muted-foreground hover:text-destructive" :disabled="installing !== null || upgradingAll || isDriverQueued(driver.db_type)" @click="uninstallDriver(driver.db_type)">
|
<Button v-if="driver.installed" variant="ghost" size="sm" class="h-7 rounded-md text-xs text-muted-foreground hover:text-destructive" :disabled="installing !== null || upgradingAll || importingZip || isDriverQueued(driver.db_type)" @click="uninstallDriver(driver.db_type)">
|
||||||
{{ t("driverStore.uninstall") }}
|
{{ t("driverStore.uninstall") }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1384,12 +1456,19 @@ watch(driverStoreTab, (tab) => {
|
|||||||
<span v-if="formatSize(driver.size)" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">{{ formatSize(driver.size) }}</span>
|
<span v-if="formatSize(driver.size)" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">{{ formatSize(driver.size) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="driver-store-agent-actions flex shrink-0 items-center gap-2">
|
<div class="driver-store-agent-actions flex shrink-0 items-center gap-2">
|
||||||
<Button v-if="!driver.installed && isDriverQueued(driver.db_type)" size="sm" variant="outline" class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15" :disabled="upgradingAll" @click="removeQueuedDriverInstall(driver.db_type)">
|
<Button
|
||||||
|
v-if="!driver.installed && isDriverQueued(driver.db_type)"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
|
||||||
|
:disabled="upgradingAll || importingZip"
|
||||||
|
@click="removeQueuedDriverInstall(driver.db_type)"
|
||||||
|
>
|
||||||
<Clock3 class="h-3 w-3 mr-1" />
|
<Clock3 class="h-3 w-3 mr-1" />
|
||||||
{{ t("driverStore.queued") }}
|
{{ t("driverStore.queued") }}
|
||||||
</Button>
|
</Button>
|
||||||
<DriverInstallProgressCircle v-else-if="!driver.installed && isDriverProgressActive(driver.db_type)" :percent="agentProgressNumber" :title="agentProgressTitle(t('driverStore.installing'))" />
|
<DriverInstallProgressCircle v-else-if="!driver.installed && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.installing'))" />
|
||||||
<Button v-else-if="!driver.installed" size="sm" class="h-7 rounded-md text-xs" :disabled="upgradingAll" @click="installDriver(driver.db_type)">
|
<Button v-else-if="!driver.installed" size="sm" class="h-7 rounded-md text-xs" :disabled="upgradingAll || importingZip" @click="installDriver(driver.db_type)">
|
||||||
<Download class="h-3 w-3 mr-1" />
|
<Download class="h-3 w-3 mr-1" />
|
||||||
{{ t("driverStore.install") }}
|
{{ t("driverStore.install") }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1399,7 +1478,7 @@ watch(driverStoreTab, (tab) => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-7 w-7 rounded-md text-xs text-muted-foreground"
|
class="h-7 w-7 rounded-md text-xs text-muted-foreground"
|
||||||
:title="t('driverStore.importLocalJar')"
|
:title="t('driverStore.importLocalJar')"
|
||||||
:disabled="upgradingAll || installing !== null"
|
:disabled="upgradingAll || installing !== null || importingZip"
|
||||||
@click="importDriverFile(driver)"
|
@click="importDriverFile(driver)"
|
||||||
>
|
>
|
||||||
<FileUp class="h-3.5 w-3.5" />
|
<FileUp class="h-3.5 w-3.5" />
|
||||||
@@ -1410,17 +1489,17 @@ watch(driverStoreTab, (tab) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
|
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
|
||||||
:disabled="upgradingAll"
|
:disabled="upgradingAll || importingZip"
|
||||||
@click="removeQueuedDriverInstall(driver.db_type)"
|
@click="removeQueuedDriverInstall(driver.db_type)"
|
||||||
>
|
>
|
||||||
<Clock3 class="h-3 w-3 mr-1" />
|
<Clock3 class="h-3 w-3 mr-1" />
|
||||||
{{ t("driverStore.queued") }}
|
{{ t("driverStore.queued") }}
|
||||||
</Button>
|
</Button>
|
||||||
<DriverInstallProgressCircle v-else-if="driver.installed && driver.update_available && isDriverProgressActive(driver.db_type)" :percent="agentProgressNumber" :title="agentProgressTitle(t('driverStore.updating'))" />
|
<DriverInstallProgressCircle v-else-if="driver.installed && driver.update_available && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.updating'))" />
|
||||||
<Button v-else-if="driver.installed && driver.update_available" size="sm" variant="outline" class="h-7 rounded-md text-xs" :disabled="upgradingAll" @click="installDriver(driver.db_type)">
|
<Button v-else-if="driver.installed && driver.update_available" size="sm" variant="outline" class="h-7 rounded-md text-xs" :disabled="upgradingAll || importingZip" @click="installDriver(driver.db_type)">
|
||||||
{{ t("driverStore.update") }}
|
{{ t("driverStore.update") }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-if="driver.installed" variant="ghost" size="sm" class="h-7 rounded-md text-xs text-muted-foreground hover:text-destructive" :disabled="installing !== null || upgradingAll || isDriverQueued(driver.db_type)" @click="uninstallDriver(driver.db_type)">
|
<Button v-if="driver.installed" variant="ghost" size="sm" class="h-7 rounded-md text-xs text-muted-foreground hover:text-destructive" :disabled="installing !== null || upgradingAll || importingZip || isDriverQueued(driver.db_type)" @click="uninstallDriver(driver.db_type)">
|
||||||
{{ t("driverStore.uninstall") }}
|
{{ t("driverStore.uninstall") }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1440,7 +1519,7 @@ watch(driverStoreTab, (tab) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 items-center gap-3">
|
<div class="flex shrink-0 items-center gap-3">
|
||||||
<DriverInstallProgressCircle v-if="isInstallingJdbcPlugin" :percent="jdbcPluginProgressNumber" :title="jdbcPluginProgressTitle(t('driverStore.progressDownloadJdbcPlugin'))" />
|
<DriverInstallProgressCircle v-if="isInstallingJdbcPlugin" :percent="jdbcPluginProgressNumber" :title="getJdbcPluginProgressTitle(t('driverStore.progressDownloadJdbcPlugin'))" />
|
||||||
<span v-if="jdbcPluginStatus?.installed" class="text-xs" :class="jdbcPluginStatus.compatible ? 'text-green-600' : 'text-destructive'">
|
<span v-if="jdbcPluginStatus?.installed" class="text-xs" :class="jdbcPluginStatus.compatible ? 'text-green-600' : 'text-destructive'">
|
||||||
{{
|
{{
|
||||||
jdbcPluginStatus.compatible
|
jdbcPluginStatus.compatible
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ import { loadConnectionPickerView, saveConnectionPickerView, type DbPickerView }
|
|||||||
import { normalizeRocketmqNamesrvAddr } from "@/lib/connection/rocketmqNamesrv";
|
import { normalizeRocketmqNamesrvAddr } from "@/lib/connection/rocketmqNamesrv";
|
||||||
import { normalizeRabbitmqAddresses } from "@/lib/connection/rabbitmqAddresses";
|
import { normalizeRabbitmqAddresses } from "@/lib/connection/rabbitmqAddresses";
|
||||||
import { detectMqUiAuthKind, isMqAuthKindAllowedForSystem, type MqUiAuthKind } from "@/lib/connection/mqAuth";
|
import { detectMqUiAuthKind, isMqAuthKindAllowedForSystem, type MqUiAuthKind } from "@/lib/connection/mqAuth";
|
||||||
import { driverInstallProgressChannel, driverInstallProgressPercent, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
|
import { driverInstallProgressChannel, driverInstallProgressPercent, isDriverInstallProgressForOperation, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
|
||||||
import { requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityConfig, sqlServerUsesLegacyCompatibility, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
|
import { requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityConfig, sqlServerUsesLegacyCompatibility, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
|
||||||
import { normalizeNacosEndpoint } from "@/lib/nacos/nacosAdmin";
|
import { normalizeNacosEndpoint } from "@/lib/nacos/nacosAdmin";
|
||||||
import {
|
import {
|
||||||
@@ -175,6 +175,7 @@ const savedDatabaseInfoFingerprint = ref("");
|
|||||||
const savedConnectionConfigFingerprint = ref("");
|
const savedConnectionConfigFingerprint = ref("");
|
||||||
const showAgentInstallDialog = ref(false);
|
const showAgentInstallDialog = ref(false);
|
||||||
const agentInstallRunning = ref(false);
|
const agentInstallRunning = ref(false);
|
||||||
|
const agentInstallOperationId = ref<string | null>(null);
|
||||||
const agentInstallDriverKey = ref("");
|
const agentInstallDriverKey = ref("");
|
||||||
const agentInstallLabel = ref("");
|
const agentInstallLabel = ref("");
|
||||||
const agentInstallProgress = ref<DriverInstallProgress | null>(null);
|
const agentInstallProgress = ref<DriverInstallProgress | null>(null);
|
||||||
@@ -1385,6 +1386,7 @@ async function refreshLocalAgentDrivers(): Promise<AgentDriverInstallState[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function beginAgentDriverInstall(driverKey: string, label: string) {
|
function beginAgentDriverInstall(driverKey: string, label: string) {
|
||||||
|
agentInstallOperationId.value = crypto.randomUUID();
|
||||||
agentInstallDriverKey.value = driverKey;
|
agentInstallDriverKey.value = driverKey;
|
||||||
agentInstallLabel.value = label;
|
agentInstallLabel.value = label;
|
||||||
agentInstallProgress.value = null;
|
agentInstallProgress.value = null;
|
||||||
@@ -1394,6 +1396,7 @@ function beginAgentDriverInstall(driverKey: string, label: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function finishAgentDriverInstall() {
|
function finishAgentDriverInstall() {
|
||||||
|
agentInstallOperationId.value = null;
|
||||||
agentInstallRunning.value = false;
|
agentInstallRunning.value = false;
|
||||||
agentInstallProgress.value = null;
|
agentInstallProgress.value = null;
|
||||||
agentInstallError.value = "";
|
agentInstallError.value = "";
|
||||||
@@ -1401,6 +1404,7 @@ function finishAgentDriverInstall() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function failAgentDriverInstall(error: unknown) {
|
function failAgentDriverInstall(error: unknown) {
|
||||||
|
agentInstallOperationId.value = null;
|
||||||
agentInstallRunning.value = false;
|
agentInstallRunning.value = false;
|
||||||
agentInstallError.value = errorMessage(error);
|
agentInstallError.value = errorMessage(error);
|
||||||
showAgentInstallDialog.value = true;
|
showAgentInstallDialog.value = true;
|
||||||
@@ -1420,6 +1424,7 @@ function setAgentInstallDialogOpen(value: boolean) {
|
|||||||
function handleAgentInstallProgress(payload: DriverInstallProgress) {
|
function handleAgentInstallProgress(payload: DriverInstallProgress) {
|
||||||
if (!agentInstallRunning.value || !agentInstallDriverKey.value) return;
|
if (!agentInstallRunning.value || !agentInstallDriverKey.value) return;
|
||||||
if (driverInstallProgressChannel(payload) !== "agent") return;
|
if (driverInstallProgressChannel(payload) !== "agent") return;
|
||||||
|
if (!isDriverInstallProgressForOperation(payload, agentInstallOperationId.value)) return;
|
||||||
if (payload.db_type && payload.db_type !== agentInstallDriverKey.value) return;
|
if (payload.db_type && payload.db_type !== agentInstallDriverKey.value) return;
|
||||||
if (payload.step === "done" || payload.step === "all-done") {
|
if (payload.step === "done" || payload.step === "all-done") {
|
||||||
agentInstallProgress.value = null;
|
agentInstallProgress.value = null;
|
||||||
@@ -1454,7 +1459,7 @@ async function ensureRequiredAgentDriverInstalled(config: ConnectionConfig): Pro
|
|||||||
testResult.value = { ok: true, message: `Installing ${label} driver...` };
|
testResult.value = { ok: true, message: `Installing ${label} driver...` };
|
||||||
beginAgentDriverInstall(driverKey, label);
|
beginAgentDriverInstall(driverKey, label);
|
||||||
try {
|
try {
|
||||||
await api.installAgent(driverKey);
|
await api.installAgent(driverKey, agentInstallOperationId.value ?? undefined);
|
||||||
await refreshLocalAgentDrivers();
|
await refreshLocalAgentDrivers();
|
||||||
finishAgentDriverInstall();
|
finishAgentDriverInstall();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1485,7 +1490,7 @@ async function installSqlServerLegacyCompatibilityComponentIfNeeded(): Promise<b
|
|||||||
const label = t("connection.sqlServerLegacyCompatibilityComponent");
|
const label = t("connection.sqlServerLegacyCompatibilityComponent");
|
||||||
beginAgentDriverInstall(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY, label);
|
beginAgentDriverInstall(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY, label);
|
||||||
try {
|
try {
|
||||||
await api.installAgent(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY);
|
await api.installAgent(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY, agentInstallOperationId.value ?? undefined);
|
||||||
await refreshLocalAgentDrivers();
|
await refreshLocalAgentDrivers();
|
||||||
finishAgentDriverInstall();
|
finishAgentDriverInstall();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -95,25 +95,28 @@ export const clearDriverDownloadCache = forward("clearDriverDownloadCache");
|
|||||||
export const getDriverRuntimeSummary = forward("getDriverRuntimeSummary");
|
export const getDriverRuntimeSummary = forward("getDriverRuntimeSummary");
|
||||||
export const stopDriverRuntime = forward("stopDriverRuntime");
|
export const stopDriverRuntime = forward("stopDriverRuntime");
|
||||||
export const restartDriverRuntime = forward("restartDriverRuntime");
|
export const restartDriverRuntime = forward("restartDriverRuntime");
|
||||||
export async function installAgent(dbType: string) {
|
export async function installAgent(dbType: string, operationId?: string) {
|
||||||
const backend = await getBackend();
|
const backend = await getBackend();
|
||||||
return backend.installAgent(dbType, useSettingsStore().editorSettings.updateDownloadSource);
|
return backend.installAgent(dbType, useSettingsStore().editorSettings.updateDownloadSource, operationId);
|
||||||
}
|
}
|
||||||
export async function upgradeAllAgents() {
|
export async function upgradeAllAgents(operationId?: string) {
|
||||||
const backend = await getBackend();
|
const backend = await getBackend();
|
||||||
return backend.upgradeAllAgents(useSettingsStore().editorSettings.updateDownloadSource);
|
return backend.upgradeAllAgents(useSettingsStore().editorSettings.updateDownloadSource, operationId);
|
||||||
}
|
}
|
||||||
export const checkAgentUpdateBlockers = forward("checkAgentUpdateBlockers");
|
export const checkAgentUpdateBlockers = forward("checkAgentUpdateBlockers");
|
||||||
export const uninstallAgent = forward("uninstallAgent");
|
export const uninstallAgent = forward("uninstallAgent");
|
||||||
export const getAgentJavaRuntimeConfig = forward("getAgentJavaRuntimeConfig");
|
export const getAgentJavaRuntimeConfig = forward("getAgentJavaRuntimeConfig");
|
||||||
export const setAgentJavaRuntimeConfig = forward("setAgentJavaRuntimeConfig");
|
export const setAgentJavaRuntimeConfig = forward("setAgentJavaRuntimeConfig");
|
||||||
export const invalidateAgentRegistryCache = forward("invalidateAgentRegistryCache");
|
export const invalidateAgentRegistryCache = forward("invalidateAgentRegistryCache");
|
||||||
export const importAgentsFromZip = forward("importAgentsFromZip");
|
export async function importAgentsFromZip(fileOrPath: string | File, operationId?: string) {
|
||||||
|
const backend = await getBackend();
|
||||||
|
return backend.importAgentsFromZip(fileOrPath, operationId);
|
||||||
|
}
|
||||||
export const importAgentDriver = forward("importAgentDriver");
|
export const importAgentDriver = forward("importAgentDriver");
|
||||||
export const importAgentJar = importAgentDriver;
|
export const importAgentJar = importAgentDriver;
|
||||||
export async function reinstallJre(jreKey?: string) {
|
export async function reinstallJre(jreKey?: string, operationId?: string) {
|
||||||
const backend = await getBackend();
|
const backend = await getBackend();
|
||||||
return backend.reinstallJre(jreKey, useSettingsStore().editorSettings.updateDownloadSource);
|
return backend.reinstallJre(jreKey, useSettingsStore().editorSettings.updateDownloadSource, operationId);
|
||||||
}
|
}
|
||||||
export const uninstallJre = forward("uninstallJre");
|
export const uninstallJre = forward("uninstallJre");
|
||||||
export const listenAgentInstallProgress = forward("listenAgentInstallProgress");
|
export const listenAgentInstallProgress = forward("listenAgentInstallProgress");
|
||||||
|
|||||||
@@ -437,12 +437,12 @@ export async function restartDriverRuntime(runtimeId: string): Promise<void> {
|
|||||||
await post("/api/agents/runtime/restart", { runtimeId });
|
await post("/api/agents/runtime/restart", { runtimeId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installAgent(dbType: string, _source?: UpdateDownloadSource): Promise<void> {
|
export async function installAgent(dbType: string, _source?: UpdateDownloadSource, operationId?: string): Promise<void> {
|
||||||
await post("/api/agents/install", { dbType });
|
await post("/api/agents/install", { dbType, operationId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function upgradeAllAgents(_source?: UpdateDownloadSource): Promise<UpgradeAllAgentDriversResult> {
|
export async function upgradeAllAgents(_source?: UpdateDownloadSource, operationId?: string): Promise<UpgradeAllAgentDriversResult> {
|
||||||
return post("/api/agents/upgrade-all", {});
|
return post("/api/agents/upgrade-all", { operationId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkAgentUpdateBlockers(_dbTypes: string[]): Promise<AgentUpdateBlocker[]> {
|
export async function checkAgentUpdateBlockers(_dbTypes: string[]): Promise<AgentUpdateBlocker[]> {
|
||||||
@@ -465,11 +465,12 @@ export async function invalidateAgentRegistryCache(): Promise<void> {
|
|||||||
await post("/api/agents/invalidate-registry-cache", {});
|
await post("/api/agents/invalidate-registry-cache", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importAgentsFromZip(fileOrPath: string | File): Promise<number> {
|
export async function importAgentsFromZip(fileOrPath: string | File, operationId?: string): Promise<number> {
|
||||||
if (typeof fileOrPath === "string") {
|
if (typeof fileOrPath === "string") {
|
||||||
throw new Error("Offline ZIP import in web mode requires a File object, not a file path");
|
throw new Error("Offline ZIP import in web mode requires a File object, not a file path");
|
||||||
}
|
}
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
if (operationId) formData.append("operationId", operationId);
|
||||||
formData.append("file", fileOrPath);
|
formData.append("file", fileOrPath);
|
||||||
const res = await fetch(apiUrl("/api/agents/import-offline"), { method: "POST", body: formData });
|
const res = await fetch(apiUrl("/api/agents/import-offline"), { method: "POST", body: formData });
|
||||||
if (!res.ok) throw new Error(await res.text());
|
if (!res.ok) throw new Error(await res.text());
|
||||||
@@ -496,8 +497,8 @@ export async function importAgentDriver(dbType: string, pathOrFile: string | Fil
|
|||||||
|
|
||||||
export const importAgentJar = importAgentDriver;
|
export const importAgentJar = importAgentDriver;
|
||||||
|
|
||||||
export async function reinstallJre(jreKey?: string, _source?: UpdateDownloadSource): Promise<void> {
|
export async function reinstallJre(jreKey?: string, _source?: UpdateDownloadSource, operationId?: string): Promise<void> {
|
||||||
await post("/api/agents/reinstall-jre", { jreKey });
|
await post("/api/agents/reinstall-jre", { jreKey, operationId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uninstallJre(jreKey: string): Promise<void> {
|
export async function uninstallJre(jreKey: string): Promise<void> {
|
||||||
|
|||||||
@@ -318,6 +318,7 @@ export interface DroppedFilePreviewSqlOptions {
|
|||||||
export type XlsxCellValue = string | number | boolean | null;
|
export type XlsxCellValue = string | number | boolean | null;
|
||||||
|
|
||||||
export interface DriverInstallProgress {
|
export interface DriverInstallProgress {
|
||||||
|
operation_id?: string;
|
||||||
step: string;
|
step: string;
|
||||||
downloaded?: number;
|
downloaded?: number;
|
||||||
total?: number;
|
total?: number;
|
||||||
@@ -1364,12 +1365,12 @@ export async function restartDriverRuntime(runtimeId: string): Promise<void> {
|
|||||||
return invoke("restart_driver_runtime", { runtimeId });
|
return invoke("restart_driver_runtime", { runtimeId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installAgent(dbType: string, source?: UpdateDownloadSource): Promise<void> {
|
export async function installAgent(dbType: string, source?: UpdateDownloadSource, operationId?: string): Promise<void> {
|
||||||
return invoke("install_agent", { dbType, source });
|
return invoke("install_agent", { dbType, source, operationId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function upgradeAllAgents(source?: UpdateDownloadSource): Promise<UpgradeAllAgentDriversResult> {
|
export async function upgradeAllAgents(source?: UpdateDownloadSource, operationId?: string): Promise<UpgradeAllAgentDriversResult> {
|
||||||
return invoke("upgrade_all_agents", { source });
|
return invoke("upgrade_all_agents", { source, operationId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkAgentUpdateBlockers(dbTypes: string[]): Promise<AgentUpdateBlocker[]> {
|
export async function checkAgentUpdateBlockers(dbTypes: string[]): Promise<AgentUpdateBlocker[]> {
|
||||||
@@ -1392,11 +1393,11 @@ export async function invalidateAgentRegistryCache(): Promise<void> {
|
|||||||
return invoke("invalidate_agent_registry_cache");
|
return invoke("invalidate_agent_registry_cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importAgentsFromZip(path: string | File): Promise<number> {
|
export async function importAgentsFromZip(path: string | File, operationId?: string): Promise<number> {
|
||||||
if (typeof path !== "string") {
|
if (typeof path !== "string") {
|
||||||
throw new Error("Desktop offline ZIP import requires a local file path");
|
throw new Error("Desktop offline ZIP import requires a local file path");
|
||||||
}
|
}
|
||||||
return invoke("import_agents_from_zip", { path });
|
return invoke("import_agents_from_zip", { path, operationId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importAgentDriver(dbType: string, path: string | File): Promise<void> {
|
export async function importAgentDriver(dbType: string, path: string | File): Promise<void> {
|
||||||
@@ -1408,8 +1409,8 @@ export async function importAgentDriver(dbType: string, path: string | File): Pr
|
|||||||
|
|
||||||
export const importAgentJar = importAgentDriver;
|
export const importAgentJar = importAgentDriver;
|
||||||
|
|
||||||
export async function reinstallJre(jreKey?: string, source?: UpdateDownloadSource): Promise<void> {
|
export async function reinstallJre(jreKey?: string, source?: UpdateDownloadSource, operationId?: string): Promise<void> {
|
||||||
return invoke("reinstall_jre", { jreKey, source });
|
return invoke("reinstall_jre", { jreKey, source, operationId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uninstallJre(jreKey: string): Promise<void> {
|
export async function uninstallJre(jreKey: string): Promise<void> {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { driverInstallProgressChannel, updateDriverInstallProgress, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
|
import { driverInstallProgressChannel, isDriverInstallProgressForOperation, isDriverInstallProgressTarget, updateDriverInstallProgress, updatePerDriverProgress, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
|
||||||
|
|
||||||
describe("driver install progress channels", () => {
|
describe("driver install progress channels", () => {
|
||||||
it("classifies agent and JDBC plugin progress independently", () => {
|
it("classifies agent and JDBC plugin progress independently", () => {
|
||||||
@@ -17,6 +17,16 @@ describe("driver install progress channels", () => {
|
|||||||
expect(updateDriverInstallProgress(jdbcProgress, agentProgress, "jdbc-plugin")).toBe(jdbcProgress);
|
expect(updateDriverInstallProgress(jdbcProgress, agentProgress, "jdbc-plugin")).toBe(jdbcProgress);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects progress and terminal events from another operation", () => {
|
||||||
|
expect(isDriverInstallProgressForOperation({ operation_id: "upgrade-b", step: "driver", db_type: "mysql" }, "upgrade-a")).toBe(false);
|
||||||
|
expect(isDriverInstallProgressForOperation({ operation_id: "upgrade-b", step: "all-done" }, "upgrade-a")).toBe(false);
|
||||||
|
expect(isDriverInstallProgressForOperation({ operation_id: "upgrade-a", step: "all-done" }, "upgrade-a")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps legacy progress compatible when no operation id is emitted", () => {
|
||||||
|
expect(isDriverInstallProgressForOperation({ step: "driver", db_type: "mysql" }, "upgrade-a")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("allows a built-in driver backed by JDBC to consume JDBC progress explicitly", () => {
|
it("allows a built-in driver backed by JDBC to consume JDBC progress explicitly", () => {
|
||||||
const jdbcProgress: DriverInstallProgress = { step: "jdbc-plugin", downloaded: 40, total: 100 };
|
const jdbcProgress: DriverInstallProgress = { step: "jdbc-plugin", downloaded: 40, total: 100 };
|
||||||
|
|
||||||
@@ -41,4 +51,27 @@ describe("driver install progress channels", () => {
|
|||||||
expect(updateDriverInstallProgress(agentProgress, ambiguousDone, "agent")).toBe(agentProgress);
|
expect(updateDriverInstallProgress(agentProgress, ambiguousDone, "agent")).toBe(agentProgress);
|
||||||
expect(updateDriverInstallProgress(jdbcProgress, ambiguousDone, "jdbc-plugin")).toBe(jdbcProgress);
|
expect(updateDriverInstallProgress(jdbcProgress, ambiguousDone, "jdbc-plugin")).toBe(jdbcProgress);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps other drivers visible when one concurrent update completes", () => {
|
||||||
|
const progressByDbType: Record<string, DriverInstallProgress | null | undefined> = {};
|
||||||
|
updatePerDriverProgress(progressByDbType, { step: "driver", db_type: "mysql", downloaded: 50, total: 100 });
|
||||||
|
updatePerDriverProgress(progressByDbType, { step: "driver", db_type: "oracle", downloaded: 25, total: 100 });
|
||||||
|
updatePerDriverProgress(progressByDbType, { step: "done", db_type: "mysql" });
|
||||||
|
|
||||||
|
expect(progressByDbType.mysql).toBeNull();
|
||||||
|
expect(progressByDbType.oracle).toMatchObject({ step: "driver", downloaded: 25 });
|
||||||
|
expect(isDriverInstallProgressTarget("oracle", { installing: null, upgradingAll: true, progressMap: progressByDbType })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears all tracked progress on a batch completion event", () => {
|
||||||
|
const progressByDbType: Record<string, DriverInstallProgress | null | undefined> = {
|
||||||
|
mysql: { step: "driver", db_type: "mysql", downloaded: 50, total: 100 },
|
||||||
|
oracle: { step: "driver", db_type: "oracle", downloaded: 25, total: 100 },
|
||||||
|
};
|
||||||
|
|
||||||
|
updatePerDriverProgress(progressByDbType, { step: "all-done" });
|
||||||
|
|
||||||
|
expect(progressByDbType.mysql).toBeNull();
|
||||||
|
expect(progressByDbType.oracle).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,20 +1,31 @@
|
|||||||
export interface DriverInstallProgress {
|
export interface DriverInstallProgress {
|
||||||
|
operation_id?: string;
|
||||||
step: string;
|
step: string;
|
||||||
downloaded?: number;
|
downloaded?: number;
|
||||||
total?: number;
|
total?: number;
|
||||||
db_type?: string;
|
db_type?: string;
|
||||||
|
/** Number of drivers completed so far during a batch upgrade (1-based). */
|
||||||
|
completed?: number;
|
||||||
|
/** Total drivers in the batch. */
|
||||||
|
total_drivers?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DriverInstallProgressTargetState {
|
export interface DriverInstallProgressTargetState {
|
||||||
installing: string | null;
|
installing: string | null;
|
||||||
upgradingAll: boolean;
|
upgradingAll: boolean;
|
||||||
progress: DriverInstallProgress | null;
|
progressMap: Record<string, DriverInstallProgress | null | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DriverInstallProgressChannel = "agent" | "jdbc-plugin";
|
export type DriverInstallProgressChannel = "agent" | "jdbc-plugin";
|
||||||
|
|
||||||
const AGENT_PROGRESS_STEPS = new Set(["driver", "jre", "jre-extract", "all-done"]);
|
const AGENT_PROGRESS_STEPS = new Set(["driver", "jre", "jre-extract", "all-done"]);
|
||||||
|
|
||||||
|
export function isDriverInstallProgressForOperation(progress: DriverInstallProgress, operationId: string | null): boolean {
|
||||||
|
// Keep accepting legacy unscoped events, while preventing another active
|
||||||
|
// install's terminal/progress event from mutating this dialog.
|
||||||
|
return !progress.operation_id || progress.operation_id === operationId;
|
||||||
|
}
|
||||||
|
|
||||||
export function driverInstallProgressChannel(progress: DriverInstallProgress): DriverInstallProgressChannel | null {
|
export function driverInstallProgressChannel(progress: DriverInstallProgress): DriverInstallProgressChannel | null {
|
||||||
if (progress.step === "jdbc-plugin" || progress.step === "jdbc-plugin-extract") return "jdbc-plugin";
|
if (progress.step === "jdbc-plugin" || progress.step === "jdbc-plugin-extract") return "jdbc-plugin";
|
||||||
if (progress.db_type || AGENT_PROGRESS_STEPS.has(progress.step)) return "agent";
|
if (progress.db_type || AGENT_PROGRESS_STEPS.has(progress.step)) return "agent";
|
||||||
@@ -28,7 +39,26 @@ export function updateDriverInstallProgress(current: DriverInstallProgress | nul
|
|||||||
return incoming;
|
return incoming;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function driverInstallProgressPercent(progress: DriverInstallProgress | null): number | null {
|
/**
|
||||||
|
* Update a per-driver progress map with an incoming event.
|
||||||
|
* Returns the updated map (mutated in-place — returned for convenience).
|
||||||
|
*/
|
||||||
|
export function updatePerDriverProgress(progressMap: Record<string, DriverInstallProgress | null | undefined>, incoming: DriverInstallProgress): Record<string, DriverInstallProgress | null | undefined> {
|
||||||
|
if (incoming.step === "all-done") {
|
||||||
|
// Batch completion has no db_type, so it must be handled before routing.
|
||||||
|
for (const key of Object.keys(progressMap)) {
|
||||||
|
progressMap[key] = null;
|
||||||
|
}
|
||||||
|
return progressMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbType = incoming.db_type;
|
||||||
|
if (!dbType) return progressMap;
|
||||||
|
progressMap[dbType] = incoming.step === "done" ? null : incoming;
|
||||||
|
return progressMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function driverInstallProgressPercent(progress: DriverInstallProgress | null | undefined): number | null {
|
||||||
if (!progress?.total || progress.total <= 0) return null;
|
if (!progress?.total || progress.total <= 0) return null;
|
||||||
const percent = Math.round(((progress.downloaded ?? 0) / progress.total) * 100);
|
const percent = Math.round(((progress.downloaded ?? 0) / progress.total) * 100);
|
||||||
return Math.min(100, Math.max(0, percent));
|
return Math.min(100, Math.max(0, percent));
|
||||||
@@ -36,7 +66,11 @@ export function driverInstallProgressPercent(progress: DriverInstallProgress | n
|
|||||||
|
|
||||||
export function isDriverInstallProgressTarget(dbType: string, state: DriverInstallProgressTargetState): boolean {
|
export function isDriverInstallProgressTarget(dbType: string, state: DriverInstallProgressTargetState): boolean {
|
||||||
if (state.installing === dbType) return true;
|
if (state.installing === dbType) return true;
|
||||||
return state.upgradingAll && state.progress?.db_type === dbType;
|
if (!state.upgradingAll) return false;
|
||||||
|
// During batch upgrade, check the per-driver map — the driver is "active"
|
||||||
|
// if it has a (non-null, non-"done") progress entry.
|
||||||
|
const progress = state.progressMap[dbType];
|
||||||
|
return progress !== null && progress !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addDriverInstallQueue(queue: string[], dbType: string, activeDbType: string | null): string[] {
|
export function addDriverInstallQueue(queue: string[], dbType: string, activeDbType: string | null): string[] {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::ffi::OsStr;
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, Mutex as StdMutex};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
@@ -435,6 +436,18 @@ pub struct AgentManager {
|
|||||||
pub(crate) connection_runtimes: Mutex<
|
pub(crate) connection_runtimes: Mutex<
|
||||||
std::collections::HashMap<String, std::sync::Arc<tokio::sync::OnceCell<std::sync::Arc<AgentRuntimeClient>>>>,
|
std::collections::HashMap<String, std::sync::Arc<tokio::sync::OnceCell<std::sync::Arc<AgentRuntimeClient>>>>,
|
||||||
>,
|
>,
|
||||||
|
/// Serializes `load_state` → modify → `save_state` to prevent lost updates
|
||||||
|
/// when multiple driver installs run concurrently.
|
||||||
|
pub(crate) state_lock: StdMutex<()>,
|
||||||
|
/// Per-JRE-key install locks so that concurrent driver installs sharing the
|
||||||
|
/// same JRE download it only once (DCL pattern: lock → re-check installed → download).
|
||||||
|
pub(crate) jre_install_locks: Mutex<std::collections::HashMap<String, Arc<Mutex<()>>>>,
|
||||||
|
/// Per-driver locks serialize install, import, and uninstall operations
|
||||||
|
/// targeting the same on-disk agent files.
|
||||||
|
pub(crate) driver_operation_locks: Mutex<std::collections::HashMap<String, Arc<Mutex<()>>>>,
|
||||||
|
/// Driver operations may run concurrently, but JRE replacement/removal
|
||||||
|
/// must exclude them until their dependent driver state is persisted.
|
||||||
|
pub(crate) installation_operation_lock: tokio::sync::RwLock<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AgentManager {
|
impl Default for AgentManager {
|
||||||
@@ -460,6 +473,10 @@ impl AgentManager {
|
|||||||
app_version: app_version.into(),
|
app_version: app_version.into(),
|
||||||
daemons: Mutex::new(std::collections::HashMap::new()),
|
daemons: Mutex::new(std::collections::HashMap::new()),
|
||||||
connection_runtimes: Mutex::new(std::collections::HashMap::new()),
|
connection_runtimes: Mutex::new(std::collections::HashMap::new()),
|
||||||
|
state_lock: StdMutex::new(()),
|
||||||
|
jre_install_locks: Mutex::new(std::collections::HashMap::new()),
|
||||||
|
driver_operation_locks: Mutex::new(std::collections::HashMap::new()),
|
||||||
|
installation_operation_lock: tokio::sync::RwLock::new(()),
|
||||||
};
|
};
|
||||||
mgr.migrate_legacy_jre();
|
mgr.migrate_legacy_jre();
|
||||||
mgr.cleanup_pending_jre_dirs();
|
mgr.cleanup_pending_jre_dirs();
|
||||||
@@ -467,6 +484,19 @@ impl AgentManager {
|
|||||||
mgr
|
mgr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Atomically load, modify, and persist the installation state.
|
||||||
|
///
|
||||||
|
/// Keep the closure free of async work: this lock exists specifically to
|
||||||
|
/// prevent one installation operation from saving an older snapshot over
|
||||||
|
/// another operation's successful update.
|
||||||
|
pub fn mutate_state<T>(&self, mutate: impl FnOnce(&mut AgentState) -> T) -> Result<T, String> {
|
||||||
|
let _guard = self.state_lock.lock().map_err(|_| "Agent installation state lock was poisoned".to_string())?;
|
||||||
|
let mut state = self.load_state();
|
||||||
|
let result = mutate(&mut state);
|
||||||
|
self.save_state(&state)?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
fn migrate_legacy_jre(&self) {
|
fn migrate_legacy_jre(&self) {
|
||||||
let legacy = self.base_dir.join("jre");
|
let legacy = self.base_dir.join("jre");
|
||||||
let versioned = self.jre_dir(DEFAULT_JRE_KEY);
|
let versioned = self.jre_dir(DEFAULT_JRE_KEY);
|
||||||
@@ -480,25 +510,26 @@ impl AgentManager {
|
|||||||
/// removals are pruned from the persisted state. Failures are kept for the
|
/// removals are pruned from the persisted state. Failures are kept for the
|
||||||
/// next launch and never block startup. (Issue #1100, D6.)
|
/// next launch and never block startup. (Issue #1100, D6.)
|
||||||
fn cleanup_pending_jre_dirs(&self) {
|
fn cleanup_pending_jre_dirs(&self) {
|
||||||
let mut state = self.load_state();
|
if self.load_state().pending_jre_cleanup.is_empty() {
|
||||||
if state.pending_jre_cleanup.is_empty() {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut remaining = Vec::new();
|
|
||||||
for path in std::mem::take(&mut state.pending_jre_cleanup) {
|
if let Err(err) = self.mutate_state(|state| {
|
||||||
if !path.exists() {
|
let mut remaining = Vec::new();
|
||||||
continue;
|
for path in std::mem::take(&mut state.pending_jre_cleanup) {
|
||||||
}
|
if !path.exists() {
|
||||||
match std::fs::remove_dir_all(&path) {
|
continue;
|
||||||
Ok(()) => log::info!("Cleaned up pending JRE stash: {}", path.display()),
|
}
|
||||||
Err(err) => {
|
match std::fs::remove_dir_all(&path) {
|
||||||
log::warn!("Pending JRE cleanup failed for {}: {err}", path.display());
|
Ok(()) => log::info!("Cleaned up pending JRE stash: {}", path.display()),
|
||||||
remaining.push(path);
|
Err(err) => {
|
||||||
|
log::warn!("Pending JRE cleanup failed for {}: {err}", path.display());
|
||||||
|
remaining.push(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
state.pending_jre_cleanup = remaining;
|
||||||
state.pending_jre_cleanup = remaining;
|
}) {
|
||||||
if let Err(err) = self.save_state(&state) {
|
|
||||||
log::warn!("Failed to persist post-cleanup AgentState: {err}");
|
log::warn!("Failed to persist post-cleanup AgentState: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::io::{Read, Seek, SeekFrom};
|
use std::io::{Read, Seek, SeekFrom};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use futures::{stream, StreamExt};
|
||||||
|
|
||||||
use crate::agent_catalog;
|
use crate::agent_catalog;
|
||||||
use crate::agent_manager::{
|
use crate::agent_manager::{
|
||||||
AgentDriverInfo, AgentManager, AgentRegistry, InstalledDriver, JavaRuntimeMode, DEFAULT_JRE_KEY,
|
AgentDriverInfo, AgentManager, AgentRegistry, InstalledDriver, JavaRuntimeMode, DEFAULT_JRE_KEY,
|
||||||
@@ -18,6 +21,10 @@ const JRE_REMOVE_ATTEMPTS: usize = if cfg!(windows) { 6 } else { 1 };
|
|||||||
/// Exponential-ish backoff between retries. Total wait ≈ 1.55s on Windows.
|
/// Exponential-ish backoff between retries. Total wait ≈ 1.55s on Windows.
|
||||||
const JRE_REMOVE_BACKOFF_MS: &[u64] = &[50, 100, 200, 400, 400, 400];
|
const JRE_REMOVE_BACKOFF_MS: &[u64] = &[50, 100, 200, 400, 400, 400];
|
||||||
|
|
||||||
|
/// Keep batch updates concurrent without allowing a large registry to exhaust
|
||||||
|
/// the download server, local disk, or the application's file descriptors.
|
||||||
|
const MAX_CONCURRENT_AGENT_UPDATES: usize = 4;
|
||||||
|
|
||||||
/// Delete an old JRE directory, retrying on Windows to cover the daemon-exit
|
/// Delete an old JRE directory, retrying on Windows to cover the daemon-exit
|
||||||
/// and AV-scan release window. Returns the original `std::io::Error` when all
|
/// and AV-scan release window. Returns the original `std::io::Error` when all
|
||||||
/// retries fail so callers can decide whether to fall back to rename-stash.
|
/// retries fail so callers can decide whether to fall back to rename-stash.
|
||||||
@@ -93,7 +100,7 @@ fn stash_old_jre_dir(path: &Path) -> std::io::Result<PathBuf> {
|
|||||||
/// path (Some) if the rename fallback was used so the caller can persist it
|
/// path (Some) if the rename fallback was used so the caller can persist it
|
||||||
/// for startup cleanup, or None if the directory was deleted outright (or
|
/// for startup cleanup, or None if the directory was deleted outright (or
|
||||||
/// did not exist).
|
/// did not exist).
|
||||||
fn replace_old_jre_dir(am: &AgentManager, path: &Path) -> Result<Option<PathBuf>, String> {
|
fn replace_old_jre_dir(path: &Path) -> Result<Option<PathBuf>, String> {
|
||||||
match remove_jre_dir_with_retry(path) {
|
match remove_jre_dir_with_retry(path) {
|
||||||
Ok(()) => Ok(None),
|
Ok(()) => Ok(None),
|
||||||
Err(remove_err) => {
|
Err(remove_err) => {
|
||||||
@@ -102,13 +109,8 @@ fn replace_old_jre_dir(am: &AgentManager, path: &Path) -> Result<Option<PathBuf>
|
|||||||
match stash_old_jre_dir(path) {
|
match stash_old_jre_dir(path) {
|
||||||
Ok(stash) => {
|
Ok(stash) => {
|
||||||
log::warn!("remove_dir_all failed, stashed old JRE at {} ({remove_err})", stash.display());
|
log::warn!("remove_dir_all failed, stashed old JRE at {} ({remove_err})", stash.display());
|
||||||
// Persist immediately so a crash before extraction
|
// The caller will persist this stash under
|
||||||
// still leaves the stash recorded for cleanup.
|
// state_lock after extraction succeeds.
|
||||||
let mut state = am.load_state();
|
|
||||||
state.pending_jre_cleanup.push(stash.clone());
|
|
||||||
if let Err(save_err) = am.save_state(&state) {
|
|
||||||
log::warn!("Failed to persist pending_jre_cleanup: {save_err}");
|
|
||||||
}
|
|
||||||
Ok(Some(stash))
|
Ok(Some(stash))
|
||||||
}
|
}
|
||||||
Err(rename_err) => {
|
Err(rename_err) => {
|
||||||
@@ -122,7 +124,6 @@ fn replace_old_jre_dir(am: &AgentManager, path: &Path) -> Result<Option<PathBuf>
|
|||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
let _ = am; // silence unused warning on POSIX
|
|
||||||
Err(format_jre_dir_remove_error(path, &remove_err))
|
Err(format_jre_dir_remove_error(path, &remove_err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,6 +139,8 @@ static REGISTRY_CACHE: std::sync::LazyLock<
|
|||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||||
pub struct AgentProgressEvent {
|
pub struct AgentProgressEvent {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub operation_id: Option<String>,
|
||||||
pub step: String,
|
pub step: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub downloaded: Option<u64>,
|
pub downloaded: Option<u64>,
|
||||||
@@ -165,7 +168,15 @@ pub struct UpgradeAllAgentDriversResult {
|
|||||||
|
|
||||||
impl AgentProgressEvent {
|
impl AgentProgressEvent {
|
||||||
pub fn step(step: impl Into<String>) -> Self {
|
pub fn step(step: impl Into<String>) -> Self {
|
||||||
Self { step: step.into(), downloaded: None, total: None, db_type: None, current: None, total_drivers: None }
|
Self {
|
||||||
|
operation_id: None,
|
||||||
|
step: step.into(),
|
||||||
|
downloaded: None,
|
||||||
|
total: None,
|
||||||
|
db_type: None,
|
||||||
|
current: None,
|
||||||
|
total_drivers: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn transfer(step: impl Into<String>, downloaded: u64, total: u64) -> Self {
|
pub fn transfer(step: impl Into<String>, downloaded: u64, total: u64) -> Self {
|
||||||
@@ -178,6 +189,11 @@ impl AgentProgressEvent {
|
|||||||
self.total_drivers = total_drivers;
|
self.total_drivers = total_drivers;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_operation_id(mut self, operation_id: &str) -> Self {
|
||||||
|
self.operation_id = Some(operation_id.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_agent_list(am: &AgentManager, registry: Option<&AgentRegistry>) -> Vec<AgentDriverInfo> {
|
pub fn build_agent_list(am: &AgentManager, registry: Option<&AgentRegistry>) -> Vec<AgentDriverInfo> {
|
||||||
@@ -303,27 +319,32 @@ pub fn find_local_agent_jar(db_type: &str) -> Option<PathBuf> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn install_local_agent(am: &AgentManager, db_type: &str, source: PathBuf) -> Result<(), String> {
|
pub fn install_local_agent(am: &AgentManager, db_type: &str, source: PathBuf) -> Result<(), String> {
|
||||||
|
install_local_agent_file(am, db_type, &source)?;
|
||||||
|
am.mutate_state(|state| record_local_agent_install(state, db_type, DEFAULT_JRE_KEY))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_local_agent_file(am: &AgentManager, db_type: &str, source: &Path) -> Result<(), String> {
|
||||||
let jar_path = am.driver_jar_path(db_type);
|
let jar_path = am.driver_jar_path(db_type);
|
||||||
let parent = jar_path.parent().ok_or_else(|| format!("Invalid driver path: {}", jar_path.display()))?;
|
let parent = jar_path.parent().ok_or_else(|| format!("Invalid driver path: {}", jar_path.display()))?;
|
||||||
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||||
let staging_path = parent.join(format!(".agent-jar-import-{}", uuid::Uuid::new_v4()));
|
let staging_path = parent.join(format!(".agent-jar-import-{}", uuid::Uuid::new_v4()));
|
||||||
std::fs::copy(&source, &staging_path).map_err(|e| format!("Failed to copy local agent jar: {e}"))?;
|
std::fs::copy(source, &staging_path).map_err(|e| format!("Failed to copy local agent jar: {e}"))?;
|
||||||
if !is_valid_agent_jar(&staging_path) {
|
if !is_valid_agent_jar(&staging_path) {
|
||||||
std::fs::remove_file(&staging_path).ok();
|
std::fs::remove_file(&staging_path).ok();
|
||||||
return Err(format!("Local agent jar is invalid or corrupt: {}", source.display()));
|
return Err(format!("Local agent jar is invalid or corrupt: {}", source.display()));
|
||||||
}
|
}
|
||||||
replace_imported_agent_file(&staging_path, &jar_path)?;
|
replace_imported_agent_file(&staging_path, &jar_path)
|
||||||
|
}
|
||||||
|
|
||||||
let mut local_state = am.load_state();
|
fn record_local_agent_install(state: &mut crate::agent_manager::AgentState, db_type: &str, jre_key: &str) {
|
||||||
local_state.installed_drivers.insert(
|
state.installed_drivers.insert(
|
||||||
db_type.to_string(),
|
db_type.to_string(),
|
||||||
InstalledDriver {
|
InstalledDriver {
|
||||||
version: "0.1.0-local".to_string(),
|
version: "0.1.0-local".to_string(),
|
||||||
installed_at: chrono::Utc::now().to_rfc3339(),
|
installed_at: chrono::Utc::now().to_rfc3339(),
|
||||||
jre: DEFAULT_JRE_KEY.to_string(),
|
jre: jre_key.to_string(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
am.save_state(&local_state)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_valid_agent_jar(path: &Path) -> bool {
|
fn is_valid_agent_jar(path: &Path) -> bool {
|
||||||
@@ -423,27 +444,45 @@ pub async fn upgrade_all_agent_drivers_from(
|
|||||||
progress: impl Fn(AgentProgressEvent),
|
progress: impl Fn(AgentProgressEvent),
|
||||||
) -> Result<UpgradeAllAgentDriversResult, String> {
|
) -> Result<UpgradeAllAgentDriversResult, String> {
|
||||||
let registry = fetch_registry_from(source).await?;
|
let registry = fetch_registry_from(source).await?;
|
||||||
let agents = build_agent_list(am, Some(®istry));
|
upgrade_all_agent_drivers_with_registry(am, ®istry, source, &progress).await
|
||||||
let updatable: Vec<&AgentDriverInfo> = agents.iter().filter(|agent| agent.update_available).collect();
|
}
|
||||||
let total = updatable.len() as u32;
|
|
||||||
let mut result = UpgradeAllAgentDriversResult::default();
|
|
||||||
|
|
||||||
for (index, agent) in updatable.iter().enumerate() {
|
async fn upgrade_all_agent_drivers_with_registry(
|
||||||
match install_agent_driver_from_registry(
|
am: &AgentManager,
|
||||||
|
registry: &AgentRegistry,
|
||||||
|
source: DownloadSource,
|
||||||
|
progress: &impl Fn(AgentProgressEvent),
|
||||||
|
) -> Result<UpgradeAllAgentDriversResult, String> {
|
||||||
|
let agents = build_agent_list(am, Some(registry));
|
||||||
|
let updatable: Vec<String> =
|
||||||
|
agents.iter().filter(|agent| agent.update_available).map(|agent| agent.db_type.clone()).collect();
|
||||||
|
let total = updatable.len() as u32;
|
||||||
|
|
||||||
|
// Run independent driver installs concurrently, with a fixed upper bound
|
||||||
|
// so a large registry cannot saturate download and file-system resources.
|
||||||
|
let installs = updatable.into_iter().enumerate().map(|(index, db_type)| async move {
|
||||||
|
let result = install_agent_driver_from_registry_locked(
|
||||||
am,
|
am,
|
||||||
®istry,
|
registry,
|
||||||
source,
|
source,
|
||||||
&agent.db_type,
|
&db_type,
|
||||||
&progress,
|
progress,
|
||||||
Some((index + 1) as u32),
|
Some((index + 1) as u32),
|
||||||
Some(total),
|
Some(total),
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
{
|
(db_type, result)
|
||||||
|
});
|
||||||
|
|
||||||
|
let outcomes = stream::iter(installs).buffer_unordered(MAX_CONCURRENT_AGENT_UPDATES).collect::<Vec<_>>().await;
|
||||||
|
|
||||||
|
let mut result = UpgradeAllAgentDriversResult::default();
|
||||||
|
for (db_type, outcome) in outcomes {
|
||||||
|
match outcome {
|
||||||
Ok(()) => result.upgraded += 1,
|
Ok(()) => result.upgraded += 1,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log::warn!("Failed to update {} agent driver: {}", agent.db_type, error);
|
log::warn!("Failed to update {} agent driver: {}", db_type, error);
|
||||||
result.failed.push(AgentDriverUpdateIssue { db_type: agent.db_type.clone(), error });
|
result.failed.push(AgentDriverUpdateIssue { db_type, error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,7 +491,20 @@ pub async fn upgrade_all_agent_drivers_from(
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn driver_operation_lock(am: &AgentManager, db_type: &str) -> Arc<tokio::sync::Mutex<()>> {
|
||||||
|
let mut locks = am.driver_operation_locks.lock().await;
|
||||||
|
locks.entry(db_type.to_string()).or_insert_with(|| Arc::new(tokio::sync::Mutex::new(()))).clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn jre_operation_lock(am: &AgentManager, jre_key: &str) -> Arc<tokio::sync::Mutex<()>> {
|
||||||
|
let mut locks = am.jre_install_locks.lock().await;
|
||||||
|
locks.entry(jre_key.to_string()).or_insert_with(|| Arc::new(tokio::sync::Mutex::new(()))).clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn uninstall_agent_driver(am: &AgentManager, db_type: &str) -> Result<(), String> {
|
pub async fn uninstall_agent_driver(am: &AgentManager, db_type: &str) -> Result<(), String> {
|
||||||
|
let _installation_guard = am.installation_operation_lock.read().await;
|
||||||
|
let driver_lock = driver_operation_lock(am, db_type).await;
|
||||||
|
let _driver_guard = driver_lock.lock().await;
|
||||||
prune_driver_download_cache(am, db_type)?;
|
prune_driver_download_cache(am, db_type)?;
|
||||||
let jar_path = am.driver_jar_path(db_type);
|
let jar_path = am.driver_jar_path(db_type);
|
||||||
if jar_path.exists() {
|
if jar_path.exists() {
|
||||||
@@ -463,9 +515,7 @@ pub async fn uninstall_agent_driver(am: &AgentManager, db_type: &str) -> Result<
|
|||||||
std::fs::remove_dir_all(driver_dir).map_err(|err| err.to_string())?;
|
std::fs::remove_dir_all(driver_dir).map_err(|err| err.to_string())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut local_state = am.load_state();
|
am.mutate_state(|state| state.installed_drivers.remove(db_type))?;
|
||||||
local_state.installed_drivers.remove(db_type);
|
|
||||||
am.save_state(&local_state)?;
|
|
||||||
am.stop_daemon_by_key(db_type).await;
|
am.stop_daemon_by_key(db_type).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -475,6 +525,11 @@ pub fn clear_agent_download_cache(am: &AgentManager) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn uninstall_agent_jre(am: &AgentManager, jre_key: &str) -> Result<(), String> {
|
pub async fn uninstall_agent_jre(am: &AgentManager, jre_key: &str) -> Result<(), String> {
|
||||||
|
// Keep the dependency check and removal atomic with respect to driver
|
||||||
|
// installs/uninstalls that may add or remove a dependency on this JRE.
|
||||||
|
let _installation_guard = am.installation_operation_lock.write().await;
|
||||||
|
let jre_lock = jre_operation_lock(am, jre_key).await;
|
||||||
|
let _jre_guard = jre_lock.lock().await;
|
||||||
let local_state = am.load_state();
|
let local_state = am.load_state();
|
||||||
let dependents: Vec<&str> = local_state
|
let dependents: Vec<&str> = local_state
|
||||||
.installed_drivers
|
.installed_drivers
|
||||||
@@ -492,9 +547,7 @@ pub async fn uninstall_agent_jre(am: &AgentManager, jre_key: &str) -> Result<(),
|
|||||||
if let Err(err) = remove_jre_dir_with_retry(&jre_dir) {
|
if let Err(err) = remove_jre_dir_with_retry(&jre_dir) {
|
||||||
return Err(format_jre_dir_remove_error(&jre_dir, &err));
|
return Err(format_jre_dir_remove_error(&jre_dir, &err));
|
||||||
}
|
}
|
||||||
let mut local_state = am.load_state();
|
am.mutate_state(|state| state.jre_versions.remove(jre_key))?;
|
||||||
local_state.jre_versions.remove(jre_key);
|
|
||||||
am.save_state(&local_state)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,6 +565,11 @@ pub async fn reinstall_agent_jre_from(
|
|||||||
source: DownloadSource,
|
source: DownloadSource,
|
||||||
progress: impl Fn(AgentProgressEvent),
|
progress: impl Fn(AgentProgressEvent),
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
// Replacing a JRE must not race a driver operation that is using or about
|
||||||
|
// to persist a dependency on the same runtime.
|
||||||
|
let _installation_guard = am.installation_operation_lock.write().await;
|
||||||
|
let jre_lock = jre_operation_lock(am, jre_key).await;
|
||||||
|
let _jre_guard = jre_lock.lock().await;
|
||||||
let registry = fetch_registry_from(source).await?;
|
let registry = fetch_registry_from(source).await?;
|
||||||
let jre_info = registry.resolve_jre(jre_key).ok_or_else(|| format!("No JRE definition for version: {jre_key}"))?;
|
let jre_info = registry.resolve_jre(jre_key).ok_or_else(|| format!("No JRE definition for version: {jre_key}"))?;
|
||||||
let platform = AgentManager::current_platform();
|
let platform = AgentManager::current_platform();
|
||||||
@@ -540,32 +598,33 @@ pub async fn reinstall_agent_jre_from(
|
|||||||
// handles on Windows (Issue #1100). Falls back to a rename-stash if the
|
// handles on Windows (Issue #1100). Falls back to a rename-stash if the
|
||||||
// directory still cannot be removed.
|
// directory still cannot be removed.
|
||||||
am.stop_daemons().await;
|
am.stop_daemons().await;
|
||||||
replace_old_jre_dir(am, &jre_dir)?;
|
let stash = replace_old_jre_dir(&jre_dir)?;
|
||||||
|
persist_pending_jre_cleanup(am, stash.as_ref()).await?;
|
||||||
extract_tar_gz(&jre_archive, &jre_dir)?;
|
extract_tar_gz(&jre_archive, &jre_dir)?;
|
||||||
std::fs::remove_file(&jre_archive).ok();
|
std::fs::remove_file(&jre_archive).ok();
|
||||||
let mut local_state = am.load_state();
|
am.mutate_state(|state| state.jre_versions.insert(jre_key.to_string(), jre_info.version.clone()))?;
|
||||||
local_state.jre_versions.insert(jre_key.to_string(), jre_info.version.clone());
|
|
||||||
am.save_state(&local_state)?;
|
|
||||||
cleanup_jre_download_cache_after_success(am, jre_key);
|
cleanup_jre_download_cache_after_success(am, jre_key);
|
||||||
progress(AgentProgressEvent::step("done"));
|
progress(AgentProgressEvent::step("done"));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn import_agents_from_zip(
|
pub async fn import_agents_from_zip(
|
||||||
am: &AgentManager,
|
am: &AgentManager,
|
||||||
zip_path: &Path,
|
zip_path: &Path,
|
||||||
progress: impl Fn(AgentProgressEvent),
|
progress: impl Fn(AgentProgressEvent),
|
||||||
) -> Result<OfflineImportResult, String> {
|
) -> Result<OfflineImportResult, String> {
|
||||||
import_offline_zip(am, zip_path, |p| {
|
import_offline_zip(am, zip_path, |p| {
|
||||||
progress(AgentProgressEvent {
|
progress(AgentProgressEvent {
|
||||||
|
operation_id: None,
|
||||||
step: p.step,
|
step: p.step,
|
||||||
downloaded: Some(p.current as u64),
|
downloaded: Some(p.current as u64),
|
||||||
total: Some(p.total as u64),
|
total: Some(p.total as u64),
|
||||||
db_type: Some(p.label),
|
db_type: p.db_type,
|
||||||
current: Some(p.current),
|
current: Some(p.current),
|
||||||
total_drivers: Some(p.total),
|
total_drivers: Some(p.total),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn install_agent_driver_with_batch(
|
async fn install_agent_driver_with_batch(
|
||||||
@@ -575,6 +634,35 @@ async fn install_agent_driver_with_batch(
|
|||||||
progress: &impl Fn(AgentProgressEvent),
|
progress: &impl Fn(AgentProgressEvent),
|
||||||
current: Option<u32>,
|
current: Option<u32>,
|
||||||
total_drivers: Option<u32>,
|
total_drivers: Option<u32>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let _installation_guard = am.installation_operation_lock.read().await;
|
||||||
|
let driver_lock = driver_operation_lock(am, db_type).await;
|
||||||
|
let _driver_guard = driver_lock.lock().await;
|
||||||
|
install_agent_driver_with_batch_unlocked(am, db_type, source, progress, current, total_drivers).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install_agent_driver_from_registry_locked(
|
||||||
|
am: &AgentManager,
|
||||||
|
registry: &AgentRegistry,
|
||||||
|
source: DownloadSource,
|
||||||
|
db_type: &str,
|
||||||
|
progress: &impl Fn(AgentProgressEvent),
|
||||||
|
current: Option<u32>,
|
||||||
|
total_drivers: Option<u32>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let _installation_guard = am.installation_operation_lock.read().await;
|
||||||
|
let driver_lock = driver_operation_lock(am, db_type).await;
|
||||||
|
let _driver_guard = driver_lock.lock().await;
|
||||||
|
install_agent_driver_from_registry(am, registry, source, db_type, progress, current, total_drivers).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install_agent_driver_with_batch_unlocked(
|
||||||
|
am: &AgentManager,
|
||||||
|
db_type: &str,
|
||||||
|
source: DownloadSource,
|
||||||
|
progress: &impl Fn(AgentProgressEvent),
|
||||||
|
current: Option<u32>,
|
||||||
|
total_drivers: Option<u32>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
match fetch_registry_from(source).await {
|
match fetch_registry_from(source).await {
|
||||||
Ok(registry) => {
|
Ok(registry) => {
|
||||||
@@ -623,13 +711,30 @@ async fn ensure_jre_from_registry(
|
|||||||
current: Option<u32>,
|
current: Option<u32>,
|
||||||
total_drivers: Option<u32>,
|
total_drivers: Option<u32>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
// Fast path: already installed — return immediately without acquiring the
|
||||||
|
// per-JRE lock. The lock is only needed when a download + extract may be
|
||||||
|
// required.
|
||||||
|
if !jre_needs_install(am, registry, jre_key) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acquire (or create) the per-JRE-key mutex so that concurrent driver
|
||||||
|
// installs sharing the same JRE download it exactly once.
|
||||||
|
let lock = jre_operation_lock(am, jre_key).await;
|
||||||
|
let _jre_guard = lock.lock().await;
|
||||||
|
|
||||||
|
// Double-check: the previous lock holder may have already installed.
|
||||||
|
if !jre_needs_install(am, registry, jre_key) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let jre_info = registry.resolve_jre(jre_key).ok_or_else(|| format!("No JRE definition for version: {jre_key}"))?;
|
let jre_info = registry.resolve_jre(jre_key).ok_or_else(|| format!("No JRE definition for version: {jre_key}"))?;
|
||||||
let platform = AgentManager::current_platform();
|
let platform = AgentManager::current_platform();
|
||||||
let platform_jre = jre_info
|
let platform_jre = jre_info
|
||||||
.platforms
|
.platforms
|
||||||
.get(platform)
|
.get(platform)
|
||||||
.ok_or_else(|| format!("No JRE {jre_key} available for platform: {platform}"))?;
|
.ok_or_else(|| format!("No JRE {jre_key} available for platform: {platform}"))?;
|
||||||
let jre_archive = am.base_dir().join("jre-download.tar.gz");
|
let jre_archive = am.base_dir().join(format!("jre-{}-download.tar.gz", jre_key));
|
||||||
progress(AgentProgressEvent::transfer("jre", 0, platform_jre.size).with_batch(
|
progress(AgentProgressEvent::transfer("jre", 0, platform_jre.size).with_batch(
|
||||||
Some(db_type),
|
Some(db_type),
|
||||||
current,
|
current,
|
||||||
@@ -652,15 +757,67 @@ async fn ensure_jre_from_registry(
|
|||||||
.await?;
|
.await?;
|
||||||
progress(AgentProgressEvent::transfer("jre-extract", 0, 0).with_batch(Some(db_type), current, total_drivers));
|
progress(AgentProgressEvent::transfer("jre-extract", 0, 0).with_batch(Some(db_type), current, total_drivers));
|
||||||
let jre_dir = am.jre_dir(jre_key);
|
let jre_dir = am.jre_dir(jre_key);
|
||||||
// Stop daemons first (Windows ERROR_ACCESS_DENIED, Issue #1100).
|
// Stop only daemons that use this JRE before replacing its directory
|
||||||
am.stop_daemons().await;
|
// (Windows ERROR_ACCESS_DENIED, Issue #1100). In a concurrent
|
||||||
replace_old_jre_dir(am, &jre_dir)?;
|
// upgrade-all this avoids killing unrelated daemons mid-install.
|
||||||
|
stop_daemons_using_jre(am, jre_key).await;
|
||||||
|
let stash = replace_old_jre_dir(&jre_dir)?;
|
||||||
|
|
||||||
|
// Persist the stash path *before* extraction so that a crash during
|
||||||
|
// extract_tar_gz (or a process kill) doesn't leave the renamed-stash
|
||||||
|
// directory as an orphan that never gets cleaned up.
|
||||||
|
persist_pending_jre_cleanup(am, stash.as_ref()).await?;
|
||||||
|
|
||||||
extract_tar_gz(&jre_archive, &jre_dir)?;
|
extract_tar_gz(&jre_archive, &jre_dir)?;
|
||||||
std::fs::remove_file(&jre_archive).ok();
|
std::fs::remove_file(&jre_archive).ok();
|
||||||
cleanup_jre_download_cache_after_success(am, jre_key);
|
cleanup_jre_download_cache_after_success(am, jre_key);
|
||||||
|
|
||||||
|
// Persist the JRE version after extraction succeeds, while still holding
|
||||||
|
// the per-JRE lock. This guarantees the DCL in another task's
|
||||||
|
// jre_needs_install() sees the installed version and skips download.
|
||||||
|
am.mutate_state(|state| state.jre_versions.insert(jre_key.to_string(), jre_info.version.clone()))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stop daemons whose installed driver lists `jre_key` as its runtime.
|
||||||
|
async fn stop_daemons_using_jre(am: &AgentManager, jre_key: &str) {
|
||||||
|
let state = am.load_state();
|
||||||
|
for (db_type, driver) in &state.installed_drivers {
|
||||||
|
if driver.jre == jre_key {
|
||||||
|
am.stop_daemon_by_key(db_type).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record a rename-stashed JRE before extraction so startup can clean it up
|
||||||
|
/// even if the process exits mid-install.
|
||||||
|
async fn persist_pending_jre_cleanup(am: &AgentManager, stash: Option<&PathBuf>) -> Result<(), String> {
|
||||||
|
let Some(stash_path) = stash else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
am.mutate_state(|state| {
|
||||||
|
if !state.pending_jre_cleanup.contains(stash_path) {
|
||||||
|
state.pending_jre_cleanup.push(stash_path.clone());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn persist_local_agent_install_state(
|
||||||
|
am: &AgentManager,
|
||||||
|
db_type: &str,
|
||||||
|
jre_key: &str,
|
||||||
|
jre_version: Option<&str>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
am.mutate_state(|state| {
|
||||||
|
if let Some(version) = jre_version {
|
||||||
|
state.jre_versions.insert(jre_key.to_string(), version.to_string());
|
||||||
|
}
|
||||||
|
record_local_agent_install(state, db_type, jre_key);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn install_local_agent_with_registry_jre(
|
async fn install_local_agent_with_registry_jre(
|
||||||
am: &AgentManager,
|
am: &AgentManager,
|
||||||
registry: &AgentRegistry,
|
registry: &AgentRegistry,
|
||||||
@@ -675,12 +832,16 @@ async fn install_local_agent_with_registry_jre(
|
|||||||
if jre_needs_install(am, registry, jre_key) {
|
if jre_needs_install(am, registry, jre_key) {
|
||||||
ensure_jre_from_registry(am, registry, source, jre_key, db_type, progress, current, total_drivers).await?;
|
ensure_jre_from_registry(am, registry, source, jre_key, db_type, progress, current, total_drivers).await?;
|
||||||
}
|
}
|
||||||
install_local_agent(am, db_type, local_jar)?;
|
install_local_agent_file(am, db_type, &local_jar)?;
|
||||||
if let Some(jre_info) = registry.resolve_jre(jre_key) {
|
// This fallback can run for several drivers during upgrade-all. Keep its
|
||||||
let mut local_state = am.load_state();
|
// driver and JRE updates in one state_lock-protected transaction.
|
||||||
local_state.jre_versions.insert(jre_key.to_string(), jre_info.version.clone());
|
persist_local_agent_install_state(
|
||||||
am.save_state(&local_state)?;
|
am,
|
||||||
}
|
db_type,
|
||||||
|
jre_key,
|
||||||
|
registry.resolve_jre(jre_key).map(|jre| jre.version.as_str()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
am.stop_daemon_by_key(db_type).await;
|
am.stop_daemon_by_key(db_type).await;
|
||||||
progress(AgentProgressEvent::step("done").with_batch(Some(db_type), current, total_drivers));
|
progress(AgentProgressEvent::step("done").with_batch(Some(db_type), current, total_drivers));
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -765,21 +926,21 @@ async fn install_agent_driver_from_registry(
|
|||||||
std::fs::remove_file(am.driver_native_path(db_type)).ok();
|
std::fs::remove_file(am.driver_native_path(db_type)).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut local_state = am.load_state();
|
am.mutate_state(|state| {
|
||||||
if requires_java_runtime {
|
if requires_java_runtime {
|
||||||
if let Some(jre_info) = registry.resolve_jre(jre_key) {
|
if let Some(jre_info) = registry.resolve_jre(jre_key) {
|
||||||
local_state.jre_versions.insert(jre_key.clone(), jre_info.version.clone());
|
state.jre_versions.insert(jre_key.clone(), jre_info.version.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
state.installed_drivers.insert(
|
||||||
local_state.installed_drivers.insert(
|
db_type.to_string(),
|
||||||
db_type.to_string(),
|
InstalledDriver {
|
||||||
InstalledDriver {
|
version: driver.version.clone(),
|
||||||
version: driver.version.clone(),
|
installed_at: chrono::Utc::now().to_rfc3339(),
|
||||||
installed_at: chrono::Utc::now().to_rfc3339(),
|
jre: jre_key.clone(),
|
||||||
jre: jre_key.clone(),
|
},
|
||||||
},
|
);
|
||||||
);
|
})?;
|
||||||
am.save_state(&local_state)?;
|
|
||||||
am.stop_daemon_by_key(db_type).await;
|
am.stop_daemon_by_key(db_type).await;
|
||||||
cleanup_driver_download_cache_after_success(am, db_type);
|
cleanup_driver_download_cache_after_success(am, db_type);
|
||||||
progress(AgentProgressEvent::step("done").with_batch(Some(db_type), current, total_drivers));
|
progress(AgentProgressEvent::step("done").with_batch(Some(db_type), current, total_drivers));
|
||||||
@@ -1234,7 +1395,12 @@ pub struct OfflineImportProgress {
|
|||||||
pub step: String,
|
pub step: String,
|
||||||
pub current: u32,
|
pub current: u32,
|
||||||
pub total: u32,
|
pub total: u32,
|
||||||
|
/// Display label for the current item (e.g. "MySQL", "JRE 21.0.12").
|
||||||
pub label: String,
|
pub label: String,
|
||||||
|
/// The real database-type key (e.g. "mysql"), used by the frontend for
|
||||||
|
/// per-driver progress routing. `None` for JRE-only steps.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub db_type: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -1265,11 +1431,16 @@ pub fn inspect_offline_zip(zip_path: &Path) -> Result<OfflineImportPlan, String>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn import_offline_zip(
|
pub async fn import_offline_zip(
|
||||||
am: &AgentManager,
|
am: &AgentManager,
|
||||||
zip_path: &Path,
|
zip_path: &Path,
|
||||||
progress: impl Fn(OfflineImportProgress),
|
progress: impl Fn(OfflineImportProgress),
|
||||||
) -> Result<OfflineImportResult, String> {
|
) -> Result<OfflineImportResult, String> {
|
||||||
|
// Offline import can touch both JRE and driver directories — hold an
|
||||||
|
// exclusive installation-operation lock so that concurrent driver installs,
|
||||||
|
// JRE installs, Upgrade All, and uninstall operations are serialised.
|
||||||
|
let _installation_guard = am.installation_operation_lock.write().await;
|
||||||
|
|
||||||
let file = std::fs::File::open(zip_path).map_err(|e| format!("Failed to open ZIP file: {e}"))?;
|
let file = std::fs::File::open(zip_path).map_err(|e| format!("Failed to open ZIP file: {e}"))?;
|
||||||
let mut archive = zip::ZipArchive::new(file).map_err(|e| format!("Invalid ZIP file: {e}"))?;
|
let mut archive = zip::ZipArchive::new(file).map_err(|e| format!("Invalid ZIP file: {e}"))?;
|
||||||
|
|
||||||
@@ -1298,7 +1469,13 @@ pub fn import_offline_zip(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress(OfflineImportProgress { step: "jre-extract".into(), current, total, label: format!("JRE {jre_key}") });
|
progress(OfflineImportProgress {
|
||||||
|
step: "jre-extract".into(),
|
||||||
|
current,
|
||||||
|
total,
|
||||||
|
label: format!("JRE {jre_key}"),
|
||||||
|
db_type: None,
|
||||||
|
});
|
||||||
|
|
||||||
let mut entry = archive.by_name(entry_name).map_err(|e| format!("Failed to read {entry_name}: {e}"))?;
|
let mut entry = archive.by_name(entry_name).map_err(|e| format!("Failed to read {entry_name}: {e}"))?;
|
||||||
let tmp_archive = am.base_dir().join(format!("jre-offline-{jre_key}.tar.gz"));
|
let tmp_archive = am.base_dir().join(format!("jre-offline-{jre_key}.tar.gz"));
|
||||||
@@ -1352,6 +1529,7 @@ pub fn import_offline_zip(
|
|||||||
current,
|
current,
|
||||||
total,
|
total,
|
||||||
label: agent_catalog::label_for_key(db_type).unwrap_or(db_type).to_string(),
|
label: agent_catalog::label_for_key(db_type).unwrap_or(db_type).to_string(),
|
||||||
|
db_type: Some(db_type.clone()),
|
||||||
});
|
});
|
||||||
|
|
||||||
let driver_path = if *is_native { am.driver_native_path(db_type) } else { am.driver_jar_path(db_type) };
|
let driver_path = if *is_native { am.driver_native_path(db_type) } else { am.driver_jar_path(db_type) };
|
||||||
@@ -1396,7 +1574,23 @@ pub fn import_offline_zip(
|
|||||||
result.drivers_installed.push(db_type.clone());
|
result.drivers_installed.push(db_type.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
am.save_state(&local_state)?;
|
am.mutate_state(|state| {
|
||||||
|
for jre_key in &result.jre_installed {
|
||||||
|
if let Some(version) = local_state.jre_versions.get(jre_key) {
|
||||||
|
state.jre_versions.insert(jre_key.clone(), version.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for path in &local_state.pending_jre_cleanup {
|
||||||
|
if !state.pending_jre_cleanup.contains(path) {
|
||||||
|
state.pending_jre_cleanup.push(path.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for db_type in &result.drivers_installed {
|
||||||
|
if let Some(driver) = local_state.installed_drivers.get(db_type) {
|
||||||
|
state.installed_drivers.insert(db_type.clone(), driver.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1541,7 +1735,14 @@ fn extract_tar_gz(archive: &Path, dest: &Path) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn import_agent_driver(am: &AgentManager, db_type: &str, source_path: &Path) -> Result<(), String> {
|
pub async fn import_agent_driver(am: &AgentManager, db_type: &str, source_path: &Path) -> Result<(), String> {
|
||||||
|
// Manual imports replace the same artifact paths as downloads. Reuse the
|
||||||
|
// install operation and per-driver locks so an import cannot race an
|
||||||
|
// install, Upgrade All, or uninstall for this driver.
|
||||||
|
let _installation_guard = am.installation_operation_lock.read().await;
|
||||||
|
let driver_lock = driver_operation_lock(am, db_type).await;
|
||||||
|
let _driver_guard = driver_lock.lock().await;
|
||||||
|
|
||||||
if !source_path.is_file() {
|
if !source_path.is_file() {
|
||||||
return Err(format!("File not found: {}", source_path.display()));
|
return Err(format!("File not found: {}", source_path.display()));
|
||||||
}
|
}
|
||||||
@@ -1562,20 +1763,20 @@ pub fn import_agent_driver(am: &AgentManager, db_type: &str, source_path: &Path)
|
|||||||
replace_imported_agent_file(&staging_path, &native_path)?;
|
replace_imported_agent_file(&staging_path, &native_path)?;
|
||||||
std::fs::remove_file(am.driver_jar_path(db_type)).ok();
|
std::fs::remove_file(am.driver_jar_path(db_type)).ok();
|
||||||
|
|
||||||
let mut local_state = am.load_state();
|
am.mutate_state(|state| {
|
||||||
local_state.installed_drivers.insert(
|
state.installed_drivers.insert(
|
||||||
db_type.to_string(),
|
db_type.to_string(),
|
||||||
InstalledDriver {
|
InstalledDriver {
|
||||||
version: "0.1.0-local".to_string(),
|
version: "0.1.0-local".to_string(),
|
||||||
installed_at: chrono::Utc::now().to_rfc3339(),
|
installed_at: chrono::Utc::now().to_rfc3339(),
|
||||||
jre: DEFAULT_JRE_KEY.to_string(),
|
jre: DEFAULT_JRE_KEY.to_string(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
am.save_state(&local_state)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn import_agent_jar(am: &AgentManager, db_type: &str, jar_path: &Path) -> Result<(), String> {
|
pub async fn import_agent_jar(am: &AgentManager, db_type: &str, jar_path: &Path) -> Result<(), String> {
|
||||||
import_agent_driver(am, db_type, jar_path)
|
import_agent_driver(am, db_type, jar_path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_imported_agent_file(staging_path: &Path, target_path: &Path) -> Result<(), String> {
|
fn replace_imported_agent_file(staging_path: &Path, target_path: &Path) -> Result<(), String> {
|
||||||
@@ -1804,13 +2005,23 @@ mod agent_download_url_tests {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod agent_registry_install_tests {
|
mod agent_registry_install_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::agent_manager::{ArtifactInfo, DriverInfo, JavaRuntimeConfig};
|
use crate::agent_manager::{ArtifactInfo, DriverInfo, InstalledDriver, JavaRuntimeConfig, JreInfo};
|
||||||
|
|
||||||
fn test_manager(name: &str) -> AgentManager {
|
fn test_manager(name: &str) -> AgentManager {
|
||||||
let dir = std::env::temp_dir().join(format!("dbx-agent-registry-install-{name}-{}", uuid::Uuid::new_v4()));
|
let dir = std::env::temp_dir().join(format!("dbx-agent-registry-install-{name}-{}", uuid::Uuid::new_v4()));
|
||||||
AgentManager::new_with_base_dir(dir)
|
AgentManager::new_with_base_dir(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_test_agent_jar(path: &Path) {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
let file = std::fs::File::create(path).unwrap();
|
||||||
|
let mut archive = zip::ZipWriter::new(file);
|
||||||
|
archive.start_file("META-INF/MANIFEST.MF", zip::write::SimpleFileOptions::default()).unwrap();
|
||||||
|
archive.write_all(b"Manifest-Version: 1.0\nMain-Class: com.dbx.Agent\n").unwrap();
|
||||||
|
archive.finish().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
fn registry_with_native_and_legacy_jar(
|
fn registry_with_native_and_legacy_jar(
|
||||||
db_type: &str,
|
db_type: &str,
|
||||||
version: &str,
|
version: &str,
|
||||||
@@ -1871,6 +2082,57 @@ mod agent_registry_install_tests {
|
|||||||
cache_path
|
cache_path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn registry_with_jre(jre_key: &str, version: &str, url: &str, size: u64) -> AgentRegistry {
|
||||||
|
AgentRegistry {
|
||||||
|
jre: None,
|
||||||
|
jres: [(
|
||||||
|
jre_key.to_string(),
|
||||||
|
JreInfo {
|
||||||
|
version: version.to_string(),
|
||||||
|
platforms: [(
|
||||||
|
AgentManager::current_platform().to_string(),
|
||||||
|
ArtifactInfo { url: url.to_string(), size },
|
||||||
|
)]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
drivers: std::collections::HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_jre_archive(am: &AgentManager, jre_key: &str) -> Vec<u8> {
|
||||||
|
let archive_root = am.base_dir().join("jre-test-archive");
|
||||||
|
let payload = archive_root.join("payload");
|
||||||
|
let java_path = am.jre_java_path(jre_key);
|
||||||
|
let relative_java_path = java_path.strip_prefix(am.jre_dir(jre_key)).unwrap();
|
||||||
|
let java_path = payload.join(relative_java_path);
|
||||||
|
std::fs::create_dir_all(java_path.parent().unwrap()).unwrap();
|
||||||
|
std::fs::write(java_path, b"java").unwrap();
|
||||||
|
let archive = archive_root.join("runtime.tar.gz");
|
||||||
|
let status = crate::process::new_std_command("tar")
|
||||||
|
.args(["czf", &archive.to_string_lossy(), "-C", &archive_root.to_string_lossy(), "payload"])
|
||||||
|
.status()
|
||||||
|
.unwrap();
|
||||||
|
assert!(status.success());
|
||||||
|
std::fs::read(archive).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_cached_jre_download(am: &AgentManager, jre_key: &str, version: &str, url: &str, archive: &[u8]) {
|
||||||
|
let dest = am.base_dir().join(format!("jre-{jre_key}-download.tar.gz"));
|
||||||
|
let cache_path = cached_download_path(
|
||||||
|
am,
|
||||||
|
url,
|
||||||
|
archive.len() as u64,
|
||||||
|
Some(CacheIdentity::Jre { key: jre_key, version }),
|
||||||
|
&dest,
|
||||||
|
);
|
||||||
|
std::fs::create_dir_all(cache_path.parent().unwrap()).unwrap();
|
||||||
|
std::fs::write(cache_path, archive).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn registry_install_accepts_native_driver_with_legacy_jar_fallback() {
|
async fn registry_install_accepts_native_driver_with_legacy_jar_fallback() {
|
||||||
let manager = test_manager("native-with-jar-fallback");
|
let manager = test_manager("native-with-jar-fallback");
|
||||||
@@ -1908,6 +2170,263 @@ mod agent_registry_install_tests {
|
|||||||
.any(|event| event.step == "done" && event.db_type.as_deref() == Some(db_type)));
|
.any(|event| event.step == "done" && event.db_type.as_deref() == Some(db_type)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn batch_upgrade_preserves_successful_state_and_reports_independent_failure() {
|
||||||
|
let manager = test_manager("batch-upgrade");
|
||||||
|
let oracle_url = "https://example.com/dbx-agent-oracle";
|
||||||
|
let dameng_url = "https://example.com/dbx-agent-dameng";
|
||||||
|
let kingbase_url = "https://example.com/dbx-agent-kingbase.jar";
|
||||||
|
let oracle_bytes = b"oracle-native-agent";
|
||||||
|
let dameng_bytes = b"dameng-native-agent";
|
||||||
|
let corrupt_jar = b"not-a-jar";
|
||||||
|
|
||||||
|
let mut registry =
|
||||||
|
registry_with_native_and_legacy_jar("oracle", "2.0.0", oracle_url, oracle_bytes.len() as u64);
|
||||||
|
registry.drivers.extend(
|
||||||
|
registry_with_native_and_legacy_jar("dameng", "2.0.0", dameng_url, dameng_bytes.len() as u64).drivers,
|
||||||
|
);
|
||||||
|
registry.drivers.extend(registry_with_jar("kingbase", "2.0.0", kingbase_url, corrupt_jar.len() as u64).drivers);
|
||||||
|
|
||||||
|
let mut state = manager.load_state();
|
||||||
|
state.java_runtime = JavaRuntimeConfig { mode: JavaRuntimeMode::System, custom_java_path: None };
|
||||||
|
for (db_type, version) in [("oracle", "1.0.0"), ("dameng", "1.0.0"), ("kingbase", "1.0.0")] {
|
||||||
|
state.installed_drivers.insert(
|
||||||
|
db_type.to_string(),
|
||||||
|
InstalledDriver {
|
||||||
|
version: version.to_string(),
|
||||||
|
installed_at: "2026-01-01T00:00:00Z".to_string(),
|
||||||
|
jre: DEFAULT_JRE_KEY.to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
manager.save_state(&state).unwrap();
|
||||||
|
|
||||||
|
write_cached_driver_download(
|
||||||
|
&manager,
|
||||||
|
"oracle",
|
||||||
|
"2.0.0",
|
||||||
|
oracle_url,
|
||||||
|
&manager.driver_native_path("oracle"),
|
||||||
|
oracle_bytes,
|
||||||
|
);
|
||||||
|
write_cached_driver_download(
|
||||||
|
&manager,
|
||||||
|
"dameng",
|
||||||
|
"2.0.0",
|
||||||
|
dameng_url,
|
||||||
|
&manager.driver_native_path("dameng"),
|
||||||
|
dameng_bytes,
|
||||||
|
);
|
||||||
|
write_cached_driver_download(
|
||||||
|
&manager,
|
||||||
|
"kingbase",
|
||||||
|
"2.0.0",
|
||||||
|
kingbase_url,
|
||||||
|
&manager.driver_jar_path("kingbase"),
|
||||||
|
corrupt_jar,
|
||||||
|
);
|
||||||
|
let events = std::sync::Mutex::new(Vec::new());
|
||||||
|
let progress = |event| events.lock().unwrap().push(event);
|
||||||
|
|
||||||
|
let result = upgrade_all_agent_drivers_with_registry(&manager, ®istry, DownloadSource::Official, &progress)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.upgraded, 2);
|
||||||
|
assert_eq!(result.failed.len(), 1);
|
||||||
|
assert_eq!(result.failed[0].db_type, "kingbase");
|
||||||
|
let state = manager.load_state();
|
||||||
|
assert_eq!(state.installed_drivers["oracle"].version, "2.0.0");
|
||||||
|
assert_eq!(state.installed_drivers["dameng"].version, "2.0.0");
|
||||||
|
assert_eq!(state.installed_drivers["kingbase"].version, "1.0.0");
|
||||||
|
assert_eq!(events.lock().unwrap().iter().filter(|event| event.step == "done").count(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn shared_jre_is_not_downloaded_again_after_the_first_install_persists_its_version() {
|
||||||
|
let manager = test_manager("shared-jre-deduplication");
|
||||||
|
let jre_key = DEFAULT_JRE_KEY;
|
||||||
|
let version = "21.0.12";
|
||||||
|
let url = "https://example.com/dbx-jre.tar.gz";
|
||||||
|
let archive = build_jre_archive(&manager, jre_key);
|
||||||
|
let registry = registry_with_jre(jre_key, version, url, archive.len() as u64);
|
||||||
|
let events = std::sync::Mutex::new(Vec::new());
|
||||||
|
let progress = |event| events.lock().unwrap().push(event);
|
||||||
|
|
||||||
|
write_cached_jre_download(&manager, jre_key, version, url, &archive);
|
||||||
|
ensure_jre_from_registry(
|
||||||
|
&manager,
|
||||||
|
®istry,
|
||||||
|
DownloadSource::Official,
|
||||||
|
jre_key,
|
||||||
|
"oracle",
|
||||||
|
&progress,
|
||||||
|
Some(1),
|
||||||
|
Some(2),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// The successful install cleans its cache. Re-add it so the old
|
||||||
|
// implementation fails deterministically by consuming it again.
|
||||||
|
write_cached_jre_download(&manager, jre_key, version, url, &archive);
|
||||||
|
ensure_jre_from_registry(
|
||||||
|
&manager,
|
||||||
|
®istry,
|
||||||
|
DownloadSource::Official,
|
||||||
|
jre_key,
|
||||||
|
"dameng",
|
||||||
|
&progress,
|
||||||
|
Some(2),
|
||||||
|
Some(2),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(manager.load_state().jre_versions[jre_key], version);
|
||||||
|
assert!(events
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.any(|event| event.step == "jre" && event.db_type.as_deref() == Some("oracle")));
|
||||||
|
assert!(!events
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.any(|event| event.step == "jre" && event.db_type.as_deref() == Some("dameng")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn stash_is_recorded_before_jre_extraction() {
|
||||||
|
let manager = test_manager("stash-before-extract");
|
||||||
|
let stash = manager.base_dir().join("jre-21.old-test");
|
||||||
|
|
||||||
|
persist_pending_jre_cleanup(&manager, Some(&stash)).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(manager.load_state().pending_jre_cleanup, vec![stash]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn concurrent_local_agent_state_updates_preserve_each_driver() {
|
||||||
|
let manager = test_manager("concurrent-local-agent-state");
|
||||||
|
let start = std::sync::Arc::new(std::sync::Barrier::new(3));
|
||||||
|
std::thread::scope(|scope| {
|
||||||
|
for db_type in ["oracle", "dameng"] {
|
||||||
|
let start = start.clone();
|
||||||
|
let manager = &manager;
|
||||||
|
scope.spawn(move || {
|
||||||
|
start.wait();
|
||||||
|
manager
|
||||||
|
.mutate_state(|state| {
|
||||||
|
state.jre_versions.insert(DEFAULT_JRE_KEY.to_string(), "21.0.12".to_string());
|
||||||
|
record_local_agent_install(state, db_type, DEFAULT_JRE_KEY);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
start.wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
let state = manager.load_state();
|
||||||
|
assert!(state.installed_drivers.contains_key("oracle"));
|
||||||
|
assert!(state.installed_drivers.contains_key("dameng"));
|
||||||
|
assert_eq!(state.jre_versions[DEFAULT_JRE_KEY], "21.0.12");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn batch_registry_install_waits_for_an_existing_driver_operation() {
|
||||||
|
let manager = test_manager("batch-driver-operation-lock");
|
||||||
|
let db_type = "oracle";
|
||||||
|
let version = "0.1.31";
|
||||||
|
let native_url = "https://example.com/dbx-agent-oracle";
|
||||||
|
let native_bytes = b"native-agent";
|
||||||
|
let registry = registry_with_native_and_legacy_jar(db_type, version, native_url, native_bytes.len() as u64);
|
||||||
|
write_cached_driver_download(
|
||||||
|
&manager,
|
||||||
|
db_type,
|
||||||
|
version,
|
||||||
|
native_url,
|
||||||
|
&manager.driver_native_path(db_type),
|
||||||
|
native_bytes,
|
||||||
|
);
|
||||||
|
let first_lock = driver_operation_lock(&manager, "oracle").await;
|
||||||
|
let first_guard = first_lock.lock().await;
|
||||||
|
let progress = |_| {};
|
||||||
|
|
||||||
|
let blocked = tokio::time::timeout(std::time::Duration::from_millis(50), async {
|
||||||
|
install_agent_driver_from_registry_locked(
|
||||||
|
&manager,
|
||||||
|
®istry,
|
||||||
|
DownloadSource::Official,
|
||||||
|
db_type,
|
||||||
|
&progress,
|
||||||
|
Some(1),
|
||||||
|
Some(1),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(blocked.is_err(), "batch install entered while another operation owned the driver files");
|
||||||
|
|
||||||
|
drop(first_guard);
|
||||||
|
tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
install_agent_driver_from_registry_locked(
|
||||||
|
&manager,
|
||||||
|
®istry,
|
||||||
|
DownloadSource::Official,
|
||||||
|
db_type,
|
||||||
|
&progress,
|
||||||
|
Some(1),
|
||||||
|
Some(1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("batch install did not resume after the driver lock was released")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(std::fs::read(manager.driver_native_path(db_type)).unwrap(), native_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn manual_import_waits_for_an_existing_driver_operation() {
|
||||||
|
let manager = test_manager("manual-import-driver-operation-lock");
|
||||||
|
let db_type = "h2";
|
||||||
|
let source = manager.base_dir().join("dbx-agent-h2.jar");
|
||||||
|
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
|
||||||
|
write_test_agent_jar(&source);
|
||||||
|
let lock = driver_operation_lock(&manager, db_type).await;
|
||||||
|
let first_guard = lock.lock().await;
|
||||||
|
|
||||||
|
let blocked =
|
||||||
|
tokio::time::timeout(std::time::Duration::from_millis(50), import_agent_driver(&manager, db_type, &source))
|
||||||
|
.await;
|
||||||
|
assert!(blocked.is_err(), "manual import entered while another operation owned the driver files");
|
||||||
|
|
||||||
|
drop(first_guard);
|
||||||
|
tokio::time::timeout(std::time::Duration::from_secs(1), import_agent_driver(&manager, db_type, &source))
|
||||||
|
.await
|
||||||
|
.expect("manual import did not resume after the driver lock was released")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(std::fs::read(manager.driver_jar_path(db_type)).unwrap(), std::fs::read(source).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn jre_exclusive_operation_waits_for_in_flight_driver_operation() {
|
||||||
|
let manager = test_manager("jre-exclusive-operation-lock");
|
||||||
|
let driver_guard = manager.installation_operation_lock.read().await;
|
||||||
|
|
||||||
|
let blocked =
|
||||||
|
tokio::time::timeout(std::time::Duration::from_millis(50), manager.installation_operation_lock.write())
|
||||||
|
.await;
|
||||||
|
assert!(blocked.is_err(), "JRE replacement entered before an in-flight driver operation completed");
|
||||||
|
|
||||||
|
drop(driver_guard);
|
||||||
|
let _jre_guard =
|
||||||
|
tokio::time::timeout(std::time::Duration::from_secs(1), manager.installation_operation_lock.write())
|
||||||
|
.await
|
||||||
|
.expect("JRE replacement did not resume after driver operations completed");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn registry_install_rejects_corrupt_downloaded_jar() {
|
async fn registry_install_rejects_corrupt_downloaded_jar() {
|
||||||
let manager = test_manager("corrupt-jar");
|
let manager = test_manager("corrupt-jar");
|
||||||
@@ -1943,6 +2462,24 @@ mod agent_registry_install_tests {
|
|||||||
assert!(!jar_path.exists());
|
assert!(!jar_path.exists());
|
||||||
assert!(!manager.load_state().installed_drivers.contains_key(db_type));
|
assert!(!manager.load_state().installed_drivers.contains_key(db_type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn offline_import_exclusive_lock_waits_for_in_flight_driver_operation() {
|
||||||
|
let manager = test_manager("offline-import-lock");
|
||||||
|
// Simulate an in-flight driver operation holding a read lock.
|
||||||
|
let driver_guard = manager.installation_operation_lock.read().await;
|
||||||
|
// import_offline_zip acquires the write lock — it must wait.
|
||||||
|
let blocked =
|
||||||
|
tokio::time::timeout(std::time::Duration::from_millis(50), manager.installation_operation_lock.write())
|
||||||
|
.await;
|
||||||
|
assert!(blocked.is_err(), "offline import entered before an in-flight driver operation completed");
|
||||||
|
|
||||||
|
drop(driver_guard);
|
||||||
|
let _offline_guard =
|
||||||
|
tokio::time::timeout(std::time::Duration::from_secs(1), manager.installation_operation_lock.write())
|
||||||
|
.await
|
||||||
|
.expect("offline import did not resume after driver operations completed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -163,11 +163,11 @@ async fn connect_and_authenticate(
|
|||||||
async fn try_authenticate_with_agent(
|
async fn try_authenticate_with_agent(
|
||||||
session: &mut Handle<SshClient>,
|
session: &mut Handle<SshClient>,
|
||||||
ssh_user: &str,
|
ssh_user: &str,
|
||||||
ssh_agent_sock_path: &str,
|
_ssh_agent_sock_path: &str,
|
||||||
connect_timeout: &Duration,
|
connect_timeout: &Duration,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let mut agent = if ssh_agent_sock_path.is_empty() {
|
let mut agent = if _ssh_agent_sock_path.is_empty() {
|
||||||
match AgentClient::connect_env().await {
|
match AgentClient::connect_env().await {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -175,12 +175,12 @@ async fn try_authenticate_with_agent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match AgentClient::connect_uds(ssh_agent_sock_path).await {
|
match AgentClient::connect_uds(_ssh_agent_sock_path).await {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"No SSH password or key provided, and ssh-agent at '{}' is unavailable: {e}",
|
"No SSH password or key provided, and ssh-agent at '{}' is unavailable: {e}",
|
||||||
ssh_agent_sock_path
|
_ssh_agent_sock_path
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -364,7 +364,9 @@ fn atomic_replace_moves_download_into_place() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn agent_progress_event_serializes_backward_compatible_fields() {
|
fn agent_progress_event_serializes_backward_compatible_fields() {
|
||||||
let event = AgentProgressEvent::transfer("driver", 512, 1024).with_batch(Some("h2"), Some(1), Some(2));
|
let event = AgentProgressEvent::transfer("driver", 512, 1024)
|
||||||
|
.with_batch(Some("h2"), Some(1), Some(2))
|
||||||
|
.with_operation_id("upgrade-123");
|
||||||
|
|
||||||
let value = serde_json::to_value(event).unwrap();
|
let value = serde_json::to_value(event).unwrap();
|
||||||
|
|
||||||
@@ -374,16 +376,17 @@ fn agent_progress_event_serializes_backward_compatible_fields() {
|
|||||||
assert_eq!(value["db_type"], "h2");
|
assert_eq!(value["db_type"], "h2");
|
||||||
assert_eq!(value["current"], 1);
|
assert_eq!(value["current"], 1);
|
||||||
assert_eq!(value["total_drivers"], 2);
|
assert_eq!(value["total_drivers"], 2);
|
||||||
|
assert_eq!(value["operation_id"], "upgrade-123");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn local_jar_import_updates_driver_state() {
|
async fn local_jar_import_updates_driver_state() {
|
||||||
let manager = test_manager("local-import");
|
let manager = test_manager("local-import");
|
||||||
let source = test_path("local-import-source").join("dbx-agent-h2.jar");
|
let source = test_path("local-import-source").join("dbx-agent-h2.jar");
|
||||||
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
|
||||||
write_test_agent_jar(&source);
|
write_test_agent_jar(&source);
|
||||||
|
|
||||||
import_agent_jar(&manager, "h2", &source).unwrap();
|
import_agent_jar(&manager, "h2", &source).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(std::fs::read(manager.driver_jar_path("h2")).unwrap(), std::fs::read(&source).unwrap());
|
assert_eq!(std::fs::read(manager.driver_jar_path("h2")).unwrap(), std::fs::read(&source).unwrap());
|
||||||
let state = manager.load_state();
|
let state = manager.load_state();
|
||||||
@@ -392,22 +395,22 @@ fn local_jar_import_updates_driver_state() {
|
|||||||
assert_eq!(installed.jre, DEFAULT_JRE_KEY);
|
assert_eq!(installed.jre, DEFAULT_JRE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn local_jar_import_rejects_corrupt_jar() {
|
async fn local_jar_import_rejects_corrupt_jar() {
|
||||||
let manager = test_manager("local-import-corrupt");
|
let manager = test_manager("local-import-corrupt");
|
||||||
let source = test_path("local-import-corrupt-source").join("dbx-agent-h2.jar");
|
let source = test_path("local-import-corrupt-source").join("dbx-agent-h2.jar");
|
||||||
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
|
||||||
std::fs::write(&source, b"jar").unwrap();
|
std::fs::write(&source, b"jar").unwrap();
|
||||||
|
|
||||||
let err = import_agent_jar(&manager, "h2", &source).unwrap_err();
|
let err = import_agent_jar(&manager, "h2", &source).await.unwrap_err();
|
||||||
|
|
||||||
assert!(err.contains("invalid or corrupt"));
|
assert!(err.contains("invalid or corrupt"));
|
||||||
assert!(!manager.driver_jar_path("h2").exists());
|
assert!(!manager.driver_jar_path("h2").exists());
|
||||||
assert!(!manager.load_state().installed_drivers.contains_key("h2"));
|
assert!(!manager.load_state().installed_drivers.contains_key("h2"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn local_native_import_installs_current_platform_executable() {
|
async fn local_native_import_installs_current_platform_executable() {
|
||||||
let manager = test_manager("local-native-import");
|
let manager = test_manager("local-native-import");
|
||||||
let source = test_path("local-native-import-source").join(if cfg!(windows) {
|
let source = test_path("local-native-import-source").join(if cfg!(windows) {
|
||||||
"dbx-agent-kingbase-windows.exe"
|
"dbx-agent-kingbase-windows.exe"
|
||||||
@@ -417,35 +420,35 @@ fn local_native_import_installs_current_platform_executable() {
|
|||||||
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
|
||||||
std::fs::write(&source, current_platform_native_binary()).unwrap();
|
std::fs::write(&source, current_platform_native_binary()).unwrap();
|
||||||
|
|
||||||
import_agent_driver(&manager, "kingbase", &source).unwrap();
|
import_agent_driver(&manager, "kingbase", &source).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(std::fs::read(manager.driver_native_path("kingbase")).unwrap(), std::fs::read(&source).unwrap());
|
assert_eq!(std::fs::read(manager.driver_native_path("kingbase")).unwrap(), std::fs::read(&source).unwrap());
|
||||||
assert!(!manager.driver_jar_path("kingbase").exists());
|
assert!(!manager.driver_jar_path("kingbase").exists());
|
||||||
assert_eq!(manager.load_state().installed_drivers["kingbase"].version, "0.1.0-local");
|
assert_eq!(manager.load_state().installed_drivers["kingbase"].version, "0.1.0-local");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn local_native_import_rejects_wrong_platform_binary() {
|
async fn local_native_import_rejects_wrong_platform_binary() {
|
||||||
let manager = test_manager("local-native-import-invalid");
|
let manager = test_manager("local-native-import-invalid");
|
||||||
let source = test_path("local-native-import-invalid-source").join("dbx-agent-kingbase");
|
let source = test_path("local-native-import-invalid-source").join("dbx-agent-kingbase");
|
||||||
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
|
||||||
std::fs::write(&source, b"not-an-executable").unwrap();
|
std::fs::write(&source, b"not-an-executable").unwrap();
|
||||||
|
|
||||||
let err = import_agent_driver(&manager, "kingbase", &source).unwrap_err();
|
let err = import_agent_driver(&manager, "kingbase", &source).await.unwrap_err();
|
||||||
|
|
||||||
assert!(err.contains(AgentManager::current_platform()));
|
assert!(err.contains(AgentManager::current_platform()));
|
||||||
assert!(!manager.driver_native_path("kingbase").exists());
|
assert!(!manager.driver_native_path("kingbase").exists());
|
||||||
assert!(!manager.load_state().installed_drivers.contains_key("kingbase"));
|
assert!(!manager.load_state().installed_drivers.contains_key("kingbase"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn local_native_import_rejects_wrong_arch_binary() {
|
async fn local_native_import_rejects_wrong_arch_binary() {
|
||||||
let manager = test_manager("local-native-wrong-arch");
|
let manager = test_manager("local-native-wrong-arch");
|
||||||
let native_path = test_path("local-native-wrong-arch-file").join("agent");
|
let native_path = test_path("local-native-wrong-arch-file").join("agent");
|
||||||
std::fs::create_dir_all(native_path.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(native_path.parent().unwrap()).unwrap();
|
||||||
std::fs::write(&native_path, native_binary_for_arch(!cfg!(target_arch = "aarch64"))).unwrap();
|
std::fs::write(&native_path, native_binary_for_arch(!cfg!(target_arch = "aarch64"))).unwrap();
|
||||||
|
|
||||||
let err = import_agent_driver(&manager, "kingbase", &native_path).unwrap_err();
|
let err = import_agent_driver(&manager, "kingbase", &native_path).await.unwrap_err();
|
||||||
|
|
||||||
assert!(err.contains("not a"));
|
assert!(err.contains("not a"));
|
||||||
assert!(!manager.driver_native_path("kingbase").exists());
|
assert!(!manager.driver_native_path("kingbase").exists());
|
||||||
@@ -515,8 +518,8 @@ fn clear_download_cache_removes_only_cache_entries() {
|
|||||||
assert!(installed_driver.exists());
|
assert!(installed_driver.exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn offline_zip_import_emits_progress_and_updates_state() {
|
async fn offline_zip_import_emits_progress_and_updates_state() {
|
||||||
let manager = test_manager("offline-progress");
|
let manager = test_manager("offline-progress");
|
||||||
let zip_path = test_path("offline-progress-zip").join("agents.zip");
|
let zip_path = test_path("offline-progress-zip").join("agents.zip");
|
||||||
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
||||||
@@ -526,6 +529,7 @@ fn offline_zip_import_emits_progress_and_updates_state() {
|
|||||||
let result = import_agents_from_zip(&manager, &zip_path, |event| {
|
let result = import_agents_from_zip(&manager, &zip_path, |event| {
|
||||||
events.lock().unwrap().push(event);
|
events.lock().unwrap().push(event);
|
||||||
})
|
})
|
||||||
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.drivers_installed, vec!["h2"]);
|
assert_eq!(result.drivers_installed, vec!["h2"]);
|
||||||
@@ -533,11 +537,12 @@ fn offline_zip_import_emits_progress_and_updates_state() {
|
|||||||
assert!(installed_jar.windows(b"Main-Class:".len()).any(|window| window == b"Main-Class:"));
|
assert!(installed_jar.windows(b"Main-Class:".len()).any(|window| window == b"Main-Class:"));
|
||||||
assert_eq!(manager.load_state().installed_drivers.get("h2").unwrap().version, "0.2.0");
|
assert_eq!(manager.load_state().installed_drivers.get("h2").unwrap().version, "0.2.0");
|
||||||
let events = events.lock().unwrap();
|
let events = events.lock().unwrap();
|
||||||
assert!(events.iter().any(|event| event.step == "driver" && event.db_type.as_deref() == Some("H2")));
|
// db_type carries the real database key, not the display label.
|
||||||
|
assert!(events.iter().any(|event| event.step == "driver" && event.db_type.as_deref() == Some("h2")));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn offline_zip_import_installs_release_named_jre() {
|
async fn offline_zip_import_installs_release_named_jre() {
|
||||||
let manager = test_manager("offline-release-jre");
|
let manager = test_manager("offline-release-jre");
|
||||||
let zip_path = test_path("offline-release-jre-zip").join("agents.zip");
|
let zip_path = test_path("offline-release-jre-zip").join("agents.zip");
|
||||||
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
||||||
@@ -547,6 +552,7 @@ fn offline_zip_import_installs_release_named_jre() {
|
|||||||
let result = import_agents_from_zip(&manager, &zip_path, |event| {
|
let result = import_agents_from_zip(&manager, &zip_path, |event| {
|
||||||
events.lock().unwrap().push(event);
|
events.lock().unwrap().push(event);
|
||||||
})
|
})
|
||||||
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.jre_installed, vec![DEFAULT_JRE_KEY]);
|
assert_eq!(result.jre_installed, vec![DEFAULT_JRE_KEY]);
|
||||||
@@ -556,35 +562,35 @@ fn offline_zip_import_installs_release_named_jre() {
|
|||||||
assert!(events.lock().unwrap().iter().any(|event| event.step == "jre-extract"));
|
assert!(events.lock().unwrap().iter().any(|event| event.step == "jre-extract"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn offline_zip_import_preserves_existing_jre_when_archive_is_corrupt() {
|
async fn offline_zip_import_preserves_existing_jre_when_archive_is_corrupt() {
|
||||||
let manager = test_manager("offline-corrupt-jre-preserves-existing");
|
let manager = test_manager("offline-corrupt-jre-preserves-existing");
|
||||||
let root = test_path("offline-corrupt-jre-preserves-existing-zip");
|
let root = test_path("offline-corrupt-jre-preserves-existing-zip");
|
||||||
let valid_zip = root.join("valid.zip");
|
let valid_zip = root.join("valid.zip");
|
||||||
let corrupt_zip = root.join("corrupt.zip");
|
let corrupt_zip = root.join("corrupt.zip");
|
||||||
std::fs::create_dir_all(&root).unwrap();
|
std::fs::create_dir_all(&root).unwrap();
|
||||||
write_offline_driver_zip_with_jre(&valid_zip, "h2", "0.2.0", "21.0.12");
|
write_offline_driver_zip_with_jre(&valid_zip, "h2", "0.2.0", "21.0.12");
|
||||||
import_agents_from_zip(&manager, &valid_zip, |_| {}).unwrap();
|
import_agents_from_zip(&manager, &valid_zip, |_| {}).await.unwrap();
|
||||||
let java_path = manager.jre_java_path(DEFAULT_JRE_KEY);
|
let java_path = manager.jre_java_path(DEFAULT_JRE_KEY);
|
||||||
let original_java = std::fs::read(&java_path).unwrap();
|
let original_java = std::fs::read(&java_path).unwrap();
|
||||||
|
|
||||||
write_offline_driver_zip_with_jre_bytes(&corrupt_zip, "h2", "0.3.0", "21.0.13", b"not-a-tar-gz".to_vec());
|
write_offline_driver_zip_with_jre_bytes(&corrupt_zip, "h2", "0.3.0", "21.0.13", b"not-a-tar-gz".to_vec());
|
||||||
let err = import_agents_from_zip(&manager, &corrupt_zip, |_| {}).unwrap_err();
|
let err = import_agents_from_zip(&manager, &corrupt_zip, |_| {}).await.unwrap_err();
|
||||||
|
|
||||||
assert!(err.contains("Failed to extract JRE archive"));
|
assert!(err.contains("Failed to extract JRE archive"));
|
||||||
assert_eq!(std::fs::read(java_path).unwrap(), original_java);
|
assert_eq!(std::fs::read(java_path).unwrap(), original_java);
|
||||||
assert_eq!(manager.load_state().jre_versions.get(DEFAULT_JRE_KEY).map(String::as_str), Some("21.0.12"));
|
assert_eq!(manager.load_state().jre_versions.get(DEFAULT_JRE_KEY).map(String::as_str), Some("21.0.12"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn offline_zip_import_preserves_existing_jre_when_driver_is_corrupt() {
|
async fn offline_zip_import_preserves_existing_jre_when_driver_is_corrupt() {
|
||||||
let manager = test_manager("offline-corrupt-driver-preserves-jre");
|
let manager = test_manager("offline-corrupt-driver-preserves-jre");
|
||||||
let root = test_path("offline-corrupt-driver-preserves-jre-zip");
|
let root = test_path("offline-corrupt-driver-preserves-jre-zip");
|
||||||
let valid_zip = root.join("valid.zip");
|
let valid_zip = root.join("valid.zip");
|
||||||
let corrupt_zip = root.join("corrupt.zip");
|
let corrupt_zip = root.join("corrupt.zip");
|
||||||
std::fs::create_dir_all(&root).unwrap();
|
std::fs::create_dir_all(&root).unwrap();
|
||||||
write_offline_driver_zip_with_jre(&valid_zip, "h2", "0.2.0", "21.0.12");
|
write_offline_driver_zip_with_jre(&valid_zip, "h2", "0.2.0", "21.0.12");
|
||||||
import_agents_from_zip(&manager, &valid_zip, |_| {}).unwrap();
|
import_agents_from_zip(&manager, &valid_zip, |_| {}).await.unwrap();
|
||||||
let java_path = manager.jre_java_path(DEFAULT_JRE_KEY);
|
let java_path = manager.jre_java_path(DEFAULT_JRE_KEY);
|
||||||
let original_java = std::fs::read(&java_path).unwrap();
|
let original_java = std::fs::read(&java_path).unwrap();
|
||||||
|
|
||||||
@@ -596,48 +602,48 @@ fn offline_zip_import_preserves_existing_jre_when_driver_is_corrupt() {
|
|||||||
test_jre_archive_bytes(),
|
test_jre_archive_bytes(),
|
||||||
b"jar".to_vec(),
|
b"jar".to_vec(),
|
||||||
);
|
);
|
||||||
let err = import_agents_from_zip(&manager, &corrupt_zip, |_| {}).unwrap_err();
|
let err = import_agents_from_zip(&manager, &corrupt_zip, |_| {}).await.unwrap_err();
|
||||||
|
|
||||||
assert!(err.contains("invalid or corrupt"));
|
assert!(err.contains("invalid or corrupt"));
|
||||||
assert_eq!(std::fs::read(java_path).unwrap(), original_java);
|
assert_eq!(std::fs::read(java_path).unwrap(), original_java);
|
||||||
assert_eq!(manager.load_state().jre_versions.get(DEFAULT_JRE_KEY).map(String::as_str), Some("21.0.12"));
|
assert_eq!(manager.load_state().jre_versions.get(DEFAULT_JRE_KEY).map(String::as_str), Some("21.0.12"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn offline_zip_import_rejects_corrupt_jar() {
|
async fn offline_zip_import_rejects_corrupt_jar() {
|
||||||
let manager = test_manager("offline-corrupt-driver");
|
let manager = test_manager("offline-corrupt-driver");
|
||||||
let zip_path = test_path("offline-corrupt-driver-zip").join("agents.zip");
|
let zip_path = test_path("offline-corrupt-driver-zip").join("agents.zip");
|
||||||
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
||||||
write_offline_driver_zip_with_jar(&zip_path, "h2", "0.2.0", b"jar".to_vec());
|
write_offline_driver_zip_with_jar(&zip_path, "h2", "0.2.0", b"jar".to_vec());
|
||||||
|
|
||||||
let err = import_agents_from_zip(&manager, &zip_path, |_| {}).unwrap_err();
|
let err = import_agents_from_zip(&manager, &zip_path, |_| {}).await.unwrap_err();
|
||||||
|
|
||||||
assert!(err.contains("invalid or corrupt"));
|
assert!(err.contains("invalid or corrupt"));
|
||||||
assert!(!manager.driver_jar_path("h2").exists());
|
assert!(!manager.driver_jar_path("h2").exists());
|
||||||
assert!(!manager.load_state().installed_drivers.contains_key("h2"));
|
assert!(!manager.load_state().installed_drivers.contains_key("h2"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn offline_zip_import_preserves_existing_driver_when_jar_is_corrupt() {
|
async fn offline_zip_import_preserves_existing_driver_when_jar_is_corrupt() {
|
||||||
let manager = test_manager("offline-corrupt-driver-preserves-existing");
|
let manager = test_manager("offline-corrupt-driver-preserves-existing");
|
||||||
let root = test_path("offline-corrupt-driver-preserves-existing-zip");
|
let root = test_path("offline-corrupt-driver-preserves-existing-zip");
|
||||||
let valid_zip = root.join("valid.zip");
|
let valid_zip = root.join("valid.zip");
|
||||||
let corrupt_zip = root.join("corrupt.zip");
|
let corrupt_zip = root.join("corrupt.zip");
|
||||||
std::fs::create_dir_all(&root).unwrap();
|
std::fs::create_dir_all(&root).unwrap();
|
||||||
write_offline_driver_zip(&valid_zip, "h2", "0.2.0");
|
write_offline_driver_zip(&valid_zip, "h2", "0.2.0");
|
||||||
import_agents_from_zip(&manager, &valid_zip, |_| {}).unwrap();
|
import_agents_from_zip(&manager, &valid_zip, |_| {}).await.unwrap();
|
||||||
let original = std::fs::read(manager.driver_jar_path("h2")).unwrap();
|
let original = std::fs::read(manager.driver_jar_path("h2")).unwrap();
|
||||||
|
|
||||||
write_offline_driver_zip_with_jar(&corrupt_zip, "h2", "0.3.0", b"jar".to_vec());
|
write_offline_driver_zip_with_jar(&corrupt_zip, "h2", "0.3.0", b"jar".to_vec());
|
||||||
let err = import_agents_from_zip(&manager, &corrupt_zip, |_| {}).unwrap_err();
|
let err = import_agents_from_zip(&manager, &corrupt_zip, |_| {}).await.unwrap_err();
|
||||||
|
|
||||||
assert!(err.contains("invalid or corrupt"));
|
assert!(err.contains("invalid or corrupt"));
|
||||||
assert_eq!(std::fs::read(manager.driver_jar_path("h2")).unwrap(), original);
|
assert_eq!(std::fs::read(manager.driver_jar_path("h2")).unwrap(), original);
|
||||||
assert_eq!(manager.load_state().installed_drivers["h2"].version, "0.2.0");
|
assert_eq!(manager.load_state().installed_drivers["h2"].version, "0.2.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn offline_zip_import_keeps_legacy_unversioned_jar_compatibility() {
|
async fn offline_zip_import_keeps_legacy_unversioned_jar_compatibility() {
|
||||||
let manager = test_manager("offline-legacy-jar-zip");
|
let manager = test_manager("offline-legacy-jar-zip");
|
||||||
let zip_path = test_path("offline-legacy-jar-zip").join("h2.zip");
|
let zip_path = test_path("offline-legacy-jar-zip").join("h2.zip");
|
||||||
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
||||||
@@ -662,19 +668,19 @@ fn offline_zip_import_keeps_legacy_unversioned_jar_compatibility() {
|
|||||||
std::io::Write::write_all(&mut zip, &jar).unwrap();
|
std::io::Write::write_all(&mut zip, &jar).unwrap();
|
||||||
zip.finish().unwrap();
|
zip.finish().unwrap();
|
||||||
|
|
||||||
import_agents_from_zip(&manager, &zip_path, |_| {}).unwrap();
|
import_agents_from_zip(&manager, &zip_path, |_| {}).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(manager.load_state().installed_drivers["h2"].version, "0.1.9");
|
assert_eq!(manager.load_state().installed_drivers["h2"].version, "0.1.9");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn offline_zip_import_installs_versioned_native_driver_package() {
|
async fn offline_zip_import_installs_versioned_native_driver_package() {
|
||||||
let manager = test_manager("offline-native-driver-zip");
|
let manager = test_manager("offline-native-driver-zip");
|
||||||
let zip_path = test_path("offline-native-driver-zip").join("kingbase.zip");
|
let zip_path = test_path("offline-native-driver-zip").join("kingbase.zip");
|
||||||
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
||||||
write_offline_native_driver_zip(&zip_path, "kingbase", "0.1.34");
|
write_offline_native_driver_zip(&zip_path, "kingbase", "0.1.34");
|
||||||
|
|
||||||
let result = import_agents_from_zip(&manager, &zip_path, |_| {}).unwrap();
|
let result = import_agents_from_zip(&manager, &zip_path, |_| {}).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.drivers_installed, vec!["kingbase"]);
|
assert_eq!(result.drivers_installed, vec!["kingbase"]);
|
||||||
assert_eq!(manager.load_state().installed_drivers["kingbase"].version, "0.1.34");
|
assert_eq!(manager.load_state().installed_drivers["kingbase"].version, "0.1.34");
|
||||||
@@ -682,30 +688,30 @@ fn offline_zip_import_installs_versioned_native_driver_package() {
|
|||||||
assert!(!manager.driver_jar_path("kingbase").exists());
|
assert!(!manager.driver_jar_path("kingbase").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn offline_zip_import_rejects_native_driver_for_another_platform() {
|
async fn offline_zip_import_rejects_native_driver_for_another_platform() {
|
||||||
let manager = test_manager("offline-wrong-platform-native-driver-zip");
|
let manager = test_manager("offline-wrong-platform-native-driver-zip");
|
||||||
let zip_path = test_path("offline-wrong-platform-native-driver-zip").join("kingbase.zip");
|
let zip_path = test_path("offline-wrong-platform-native-driver-zip").join("kingbase.zip");
|
||||||
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
||||||
let other_platform = if AgentManager::current_platform() == "windows-x64" { "linux-x64" } else { "windows-x64" };
|
let other_platform = if AgentManager::current_platform() == "windows-x64" { "linux-x64" } else { "windows-x64" };
|
||||||
write_offline_native_driver_zip_for_platform(&zip_path, "kingbase", "0.1.34", other_platform);
|
write_offline_native_driver_zip_for_platform(&zip_path, "kingbase", "0.1.34", other_platform);
|
||||||
|
|
||||||
let err = import_agents_from_zip(&manager, &zip_path, |_| {}).unwrap_err();
|
let err = import_agents_from_zip(&manager, &zip_path, |_| {}).await.unwrap_err();
|
||||||
|
|
||||||
assert!(err.contains("no drivers compatible"));
|
assert!(err.contains("no drivers compatible"));
|
||||||
assert!(err.contains(AgentManager::current_platform()));
|
assert!(err.contains(AgentManager::current_platform()));
|
||||||
assert!(!manager.is_driver_installed("kingbase"));
|
assert!(!manager.is_driver_installed("kingbase"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn offline_zip_import_preserves_existing_native_driver_when_binary_is_invalid() {
|
async fn offline_zip_import_preserves_existing_native_driver_when_binary_is_invalid() {
|
||||||
let manager = test_manager("offline-invalid-native-preserves-existing");
|
let manager = test_manager("offline-invalid-native-preserves-existing");
|
||||||
let root = test_path("offline-invalid-native-preserves-existing-zip");
|
let root = test_path("offline-invalid-native-preserves-existing-zip");
|
||||||
let valid_zip = root.join("valid.zip");
|
let valid_zip = root.join("valid.zip");
|
||||||
let invalid_zip = root.join("invalid.zip");
|
let invalid_zip = root.join("invalid.zip");
|
||||||
std::fs::create_dir_all(&root).unwrap();
|
std::fs::create_dir_all(&root).unwrap();
|
||||||
write_offline_native_driver_zip(&valid_zip, "kingbase", "0.1.34");
|
write_offline_native_driver_zip(&valid_zip, "kingbase", "0.1.34");
|
||||||
import_agents_from_zip(&manager, &valid_zip, |_| {}).unwrap();
|
import_agents_from_zip(&manager, &valid_zip, |_| {}).await.unwrap();
|
||||||
let original = std::fs::read(manager.driver_native_path("kingbase")).unwrap();
|
let original = std::fs::read(manager.driver_native_path("kingbase")).unwrap();
|
||||||
|
|
||||||
write_offline_native_driver_zip_with_bytes(
|
write_offline_native_driver_zip_with_bytes(
|
||||||
@@ -715,7 +721,7 @@ fn offline_zip_import_preserves_existing_native_driver_when_binary_is_invalid()
|
|||||||
AgentManager::current_platform(),
|
AgentManager::current_platform(),
|
||||||
b"not-a-native-agent".to_vec(),
|
b"not-a-native-agent".to_vec(),
|
||||||
);
|
);
|
||||||
let err = import_agents_from_zip(&manager, &invalid_zip, |_| {}).unwrap_err();
|
let err = import_agents_from_zip(&manager, &invalid_zip, |_| {}).await.unwrap_err();
|
||||||
|
|
||||||
assert!(err.contains("not a"));
|
assert!(err.contains("not a"));
|
||||||
assert_eq!(std::fs::read(manager.driver_native_path("kingbase")).unwrap(), original);
|
assert_eq!(std::fs::read(manager.driver_native_path("kingbase")).unwrap(), original);
|
||||||
@@ -763,15 +769,15 @@ fn offline_zip_import_rejects_unsafe_entry_path() {
|
|||||||
assert!(err.contains("unsafe path"));
|
assert!(err.contains("unsafe path"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn offline_zip_import_prefers_native_artifact_over_java_fallback() {
|
async fn offline_zip_import_prefers_native_artifact_over_java_fallback() {
|
||||||
let manager = test_manager("offline-native-preferred");
|
let manager = test_manager("offline-native-preferred");
|
||||||
let zip_path = test_path("offline-native-preferred-zip").join("kingbase.zip");
|
let zip_path = test_path("offline-native-preferred-zip").join("kingbase.zip");
|
||||||
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
||||||
write_offline_hybrid_driver_zip(&zip_path, "kingbase", "0.1.34");
|
write_offline_hybrid_driver_zip(&zip_path, "kingbase", "0.1.34");
|
||||||
|
|
||||||
let plan = inspect_offline_zip(&zip_path).unwrap();
|
let plan = inspect_offline_zip(&zip_path).unwrap();
|
||||||
let result = import_agents_from_zip(&manager, &zip_path, |_| {}).unwrap();
|
let result = import_agents_from_zip(&manager, &zip_path, |_| {}).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(plan.driver_keys, vec!["kingbase"]);
|
assert_eq!(plan.driver_keys, vec!["kingbase"]);
|
||||||
assert_eq!(result.drivers_installed, vec!["kingbase"]);
|
assert_eq!(result.drivers_installed, vec!["kingbase"]);
|
||||||
@@ -779,6 +785,36 @@ fn offline_zip_import_prefers_native_artifact_over_java_fallback() {
|
|||||||
assert!(!manager.driver_jar_path("kingbase").exists());
|
assert!(!manager.driver_jar_path("kingbase").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn offline_zip_import_progress_carries_real_db_type_not_label() {
|
||||||
|
// Verify that the AgentProgressEvent.db_type field carries the real
|
||||||
|
// database key (e.g. "kingbase") rather than the display label
|
||||||
|
// (e.g. "KingbaseES"), so the frontend per-driver progress routing
|
||||||
|
// matches.
|
||||||
|
let manager = test_manager("offline-progress-db-type");
|
||||||
|
let zip_path = test_path("offline-progress-db-type-zip").join("kingbase.zip");
|
||||||
|
std::fs::create_dir_all(zip_path.parent().unwrap()).unwrap();
|
||||||
|
write_offline_native_driver_zip(&zip_path, "kingbase", "0.1.34");
|
||||||
|
let events = std::sync::Mutex::new(Vec::new());
|
||||||
|
|
||||||
|
import_agents_from_zip(&manager, &zip_path, |event| {
|
||||||
|
events.lock().unwrap().push(event);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let events = events.lock().unwrap();
|
||||||
|
let driver_events: Vec<_> = events.iter().filter(|e| e.step == "driver").collect();
|
||||||
|
assert!(!driver_events.is_empty(), "expected at least one driver progress event");
|
||||||
|
for event in &driver_events {
|
||||||
|
assert_eq!(
|
||||||
|
event.db_type.as_deref(),
|
||||||
|
Some("kingbase"),
|
||||||
|
"db_type must be the real key 'kingbase', not the display label"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn write_offline_driver_zip(path: &std::path::Path, db_type: &str, version: &str) {
|
fn write_offline_driver_zip(path: &std::path::Path, db_type: &str, version: &str) {
|
||||||
write_offline_driver_zip_with_jar(path, db_type, version, test_agent_jar_bytes());
|
write_offline_driver_zip_with_jar(path, db_type, version, test_agent_jar_bytes());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,12 +25,20 @@ use crate::state::WebState;
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AgentTypeRequest {
|
pub struct AgentTypeRequest {
|
||||||
pub db_type: String,
|
pub db_type: String,
|
||||||
|
pub operation_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct JreRequest {
|
pub struct JreRequest {
|
||||||
pub jre_key: Option<String>,
|
pub jre_key: Option<String>,
|
||||||
|
pub operation_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AgentOperationRequest {
|
||||||
|
pub operation_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -101,23 +109,32 @@ pub async fn install_agent(
|
|||||||
Json(req): Json<AgentTypeRequest>,
|
Json(req): Json<AgentTypeRequest>,
|
||||||
) -> Result<Json<serde_json::Value>, AppError> {
|
) -> Result<Json<serde_json::Value>, AppError> {
|
||||||
ensure_no_agent_update_blockers(&state.app, std::slice::from_ref(&req.db_type)).await.map_err(AppError::from)?;
|
ensure_no_agent_update_blockers(&state.app, std::slice::from_ref(&req.db_type)).await.map_err(AppError::from)?;
|
||||||
|
let operation_id = req.operation_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||||
let tx = progress_sender(&state, "global").await;
|
let tx = progress_sender(&state, "global").await;
|
||||||
install_agent_driver(&state.app.agent_manager, &req.db_type, |event| send_progress_event(&tx, event))
|
install_agent_driver(&state.app.agent_manager, &req.db_type, |event| {
|
||||||
.await
|
send_progress_event(&tx, event.with_operation_id(&operation_id))
|
||||||
.map_err(AppError::from)?;
|
})
|
||||||
|
.await
|
||||||
|
.map_err(AppError::from)?;
|
||||||
Ok(Json(serde_json::json!({ "ok": true })))
|
Ok(Json(serde_json::json!({ "ok": true })))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn upgrade_all_agents(State(state): State<Arc<WebState>>) -> Result<Json<serde_json::Value>, AppError> {
|
pub async fn upgrade_all_agents(
|
||||||
|
State(state): State<Arc<WebState>>,
|
||||||
|
Json(req): Json<AgentOperationRequest>,
|
||||||
|
) -> Result<Json<serde_json::Value>, AppError> {
|
||||||
let registry = fetch_registry().await.map_err(AppError::from)?;
|
let registry = fetch_registry().await.map_err(AppError::from)?;
|
||||||
let agents = build_agent_list(&state.app.agent_manager, Some(®istry));
|
let agents = build_agent_list(&state.app.agent_manager, Some(®istry));
|
||||||
let updatable: Vec<String> =
|
let updatable: Vec<String> =
|
||||||
agents.iter().filter(|agent| agent.update_available).map(|agent| agent.db_type.clone()).collect();
|
agents.iter().filter(|agent| agent.update_available).map(|agent| agent.db_type.clone()).collect();
|
||||||
ensure_no_agent_update_blockers(&state.app, &updatable).await.map_err(AppError::from)?;
|
ensure_no_agent_update_blockers(&state.app, &updatable).await.map_err(AppError::from)?;
|
||||||
|
let operation_id = req.operation_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||||
let tx = progress_sender(&state, "global").await;
|
let tx = progress_sender(&state, "global").await;
|
||||||
let result = upgrade_all_agent_drivers(&state.app.agent_manager, |event| send_progress_event(&tx, event))
|
let result = upgrade_all_agent_drivers(&state.app.agent_manager, |event| {
|
||||||
.await
|
send_progress_event(&tx, event.with_operation_id(&operation_id))
|
||||||
.map_err(AppError::from)?;
|
})
|
||||||
|
.await
|
||||||
|
.map_err(AppError::from)?;
|
||||||
Ok(Json(serde_json::to_value(result).map_err(|err| AppError::from(err.to_string()))?))
|
Ok(Json(serde_json::to_value(result).map_err(|err| AppError::from(err.to_string()))?))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,9 +169,7 @@ pub async fn set_agent_java_runtime_config(
|
|||||||
config.custom_java_path = None;
|
config.custom_java_path = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut local_state = am.load_state();
|
am.mutate_state(|local_state| local_state.java_runtime = config.clone()).map_err(AppError::from)?;
|
||||||
local_state.java_runtime = config.clone();
|
|
||||||
am.save_state(&local_state).map_err(AppError::from)?;
|
|
||||||
am.stop_daemons().await;
|
am.stop_daemons().await;
|
||||||
Ok(Json(config))
|
Ok(Json(config))
|
||||||
}
|
}
|
||||||
@@ -171,7 +186,19 @@ pub async fn import_agents_from_zip(
|
|||||||
let tmp_dir = state.data_dir.join("tmp");
|
let tmp_dir = state.data_dir.join("tmp");
|
||||||
std::fs::create_dir_all(&tmp_dir).map_err(|err| AppError::from(err.to_string()))?;
|
std::fs::create_dir_all(&tmp_dir).map_err(|err| AppError::from(err.to_string()))?;
|
||||||
|
|
||||||
if let Some(field) = multipart.next_field().await.map_err(|err| AppError::from(err.to_string()))? {
|
let mut operation_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
while let Some(field) = multipart.next_field().await.map_err(|err| AppError::from(err.to_string()))? {
|
||||||
|
if field.name() == Some("operationId") {
|
||||||
|
let candidate = field.text().await.map_err(|err| AppError::from(err.to_string()))?;
|
||||||
|
if !candidate.is_empty() {
|
||||||
|
operation_id = candidate;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if field.name() != Some("file") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let file_name = field.file_name().unwrap_or("offline-drivers.zip").to_string();
|
let file_name = field.file_name().unwrap_or("offline-drivers.zip").to_string();
|
||||||
if !file_name.to_ascii_lowercase().ends_with(".zip") {
|
if !file_name.to_ascii_lowercase().ends_with(".zip") {
|
||||||
return Err(AppError::from("Offline driver package must be a .zip file".to_string()));
|
return Err(AppError::from("Offline driver package must be a .zip file".to_string()));
|
||||||
@@ -190,14 +217,17 @@ pub async fn import_agents_from_zip(
|
|||||||
|
|
||||||
let plan = inspect_offline_zip(&zip_path).map_err(AppError::from)?;
|
let plan = inspect_offline_zip(&zip_path).map_err(AppError::from)?;
|
||||||
ensure_no_offline_import_blockers(&state.app, &plan).await.map_err(AppError::from)?;
|
ensure_no_offline_import_blockers(&state.app, &plan).await.map_err(AppError::from)?;
|
||||||
import_agents_from_zip_core(&state.app.agent_manager, &zip_path, |event| send_progress_event(&tx, event))
|
import_agents_from_zip_core(&state.app.agent_manager, &zip_path, |event| {
|
||||||
.map_err(AppError::from)
|
send_progress_event(&tx, event.with_operation_id(&operation_id))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(AppError::from)
|
||||||
}
|
}
|
||||||
.await;
|
.await;
|
||||||
let _ = std::fs::remove_file(&zip_path);
|
let _ = std::fs::remove_file(&zip_path);
|
||||||
|
|
||||||
let result = result?;
|
let result = result?;
|
||||||
send_progress_event(&tx, AgentProgressEvent::step("done"));
|
send_progress_event(&tx, AgentProgressEvent::step("done").with_operation_id(&operation_id));
|
||||||
return Ok(Json(serde_json::json!({ "count": result.drivers_installed.len() as u32 })));
|
return Ok(Json(serde_json::json!({ "count": result.drivers_installed.len() as u32 })));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +267,7 @@ pub async fn import_agent_driver_file(
|
|||||||
|
|
||||||
let result = async {
|
let result = async {
|
||||||
ensure_no_agent_update_blockers(&state.app, std::slice::from_ref(&db_type)).await.map_err(AppError::from)?;
|
ensure_no_agent_update_blockers(&state.app, std::slice::from_ref(&db_type)).await.map_err(AppError::from)?;
|
||||||
import_agent_driver(&state.app.agent_manager, &db_type, &tmp_path).map_err(AppError::from)
|
import_agent_driver(&state.app.agent_manager, &db_type, &tmp_path).await.map_err(AppError::from)
|
||||||
}
|
}
|
||||||
.await;
|
.await;
|
||||||
let _ = std::fs::remove_file(&tmp_path);
|
let _ = std::fs::remove_file(&tmp_path);
|
||||||
@@ -249,9 +279,10 @@ pub async fn reinstall_jre(
|
|||||||
State(state): State<Arc<WebState>>,
|
State(state): State<Arc<WebState>>,
|
||||||
Json(req): Json<JreRequest>,
|
Json(req): Json<JreRequest>,
|
||||||
) -> Result<Json<serde_json::Value>, AppError> {
|
) -> Result<Json<serde_json::Value>, AppError> {
|
||||||
|
let operation_id = req.operation_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||||
let tx = progress_sender(&state, "global").await;
|
let tx = progress_sender(&state, "global").await;
|
||||||
reinstall_agent_jre(&state.app.agent_manager, req.jre_key.as_deref().unwrap_or(DEFAULT_JRE_KEY), |event| {
|
reinstall_agent_jre(&state.app.agent_manager, req.jre_key.as_deref().unwrap_or(DEFAULT_JRE_KEY), |event| {
|
||||||
send_progress_event(&tx, event);
|
send_progress_event(&tx, event.with_operation_id(&operation_id));
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|||||||
+3
-3
@@ -17,7 +17,7 @@ test("shares the configured download source with agent driver management", () =>
|
|||||||
assert.match(driverStore, /void forceRefresh\(\)\.catch\(\(\) => undefined\)/);
|
assert.match(driverStore, /void forceRefresh\(\)\.catch\(\(\) => undefined\)/);
|
||||||
|
|
||||||
assert.match(backendApi, /backend\.listInstalledAgents\(useSettingsStore\(\)\.editorSettings\.updateDownloadSource\)/);
|
assert.match(backendApi, /backend\.listInstalledAgents\(useSettingsStore\(\)\.editorSettings\.updateDownloadSource\)/);
|
||||||
assert.match(backendApi, /backend\.installAgent\(dbType, useSettingsStore\(\)\.editorSettings\.updateDownloadSource\)/);
|
assert.match(backendApi, /backend\.installAgent\(dbType, useSettingsStore\(\)\.editorSettings\.updateDownloadSource(?:, operationId)?\)/);
|
||||||
assert.match(backendApi, /backend\.upgradeAllAgents\(useSettingsStore\(\)\.editorSettings\.updateDownloadSource\)/);
|
assert.match(backendApi, /backend\.upgradeAllAgents\(useSettingsStore\(\)\.editorSettings\.updateDownloadSource(?:, operationId)?\)/);
|
||||||
assert.match(backendApi, /backend\.reinstallJre\(jreKey, useSettingsStore\(\)\.editorSettings\.updateDownloadSource\)/);
|
assert.match(backendApi, /backend\.reinstallJre\(jreKey, useSettingsStore\(\)\.editorSettings\.updateDownloadSource(?:, operationId)?\)/);
|
||||||
});
|
});
|
||||||
+3
-3
@@ -16,13 +16,13 @@ test("returns null when install progress has no measurable total", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("targets only the row currently installing or upgrading", () => {
|
test("targets only the row currently installing or upgrading", () => {
|
||||||
assert.equal(isDriverInstallProgressTarget("mysql", { installing: "mysql", upgradingAll: false, progress: null }), true);
|
assert.equal(isDriverInstallProgressTarget("mysql", { installing: "mysql", upgradingAll: false, progressMap: {} }), true);
|
||||||
assert.equal(isDriverInstallProgressTarget("postgres", { installing: "mysql", upgradingAll: false, progress: null }), false);
|
assert.equal(isDriverInstallProgressTarget("postgres", { installing: "mysql", upgradingAll: false, progressMap: {} }), false);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
isDriverInstallProgressTarget("postgres", {
|
isDriverInstallProgressTarget("postgres", {
|
||||||
installing: null,
|
installing: null,
|
||||||
upgradingAll: true,
|
upgradingAll: true,
|
||||||
progress: { step: "driver", db_type: "postgres", downloaded: 1, total: 2 },
|
progressMap: { postgres: { step: "driver", db_type: "postgres", downloaded: 1, total: 2 } },
|
||||||
}),
|
}),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -69,11 +69,13 @@ pub async fn install_agent(
|
|||||||
state: State<'_, Arc<AppState>>,
|
state: State<'_, Arc<AppState>>,
|
||||||
db_type: String,
|
db_type: String,
|
||||||
source: Option<DownloadSource>,
|
source: Option<DownloadSource>,
|
||||||
|
operation_id: Option<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
ensure_no_agent_update_blockers(state.inner().as_ref(), std::slice::from_ref(&db_type)).await?;
|
ensure_no_agent_update_blockers(state.inner().as_ref(), std::slice::from_ref(&db_type)).await?;
|
||||||
let app_handle = app.clone();
|
let app_handle = app.clone();
|
||||||
|
let operation_id = operation_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||||
install_agent_driver_from(&state.agent_manager, &db_type, source.unwrap_or_default(), move |event| {
|
install_agent_driver_from(&state.agent_manager, &db_type, source.unwrap_or_default(), move |event| {
|
||||||
emit_agent_progress(&app_handle, event)
|
emit_agent_progress(&app_handle, &operation_id, event)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -83,6 +85,7 @@ pub async fn upgrade_all_agents(
|
|||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
state: State<'_, Arc<AppState>>,
|
state: State<'_, Arc<AppState>>,
|
||||||
source: Option<DownloadSource>,
|
source: Option<DownloadSource>,
|
||||||
|
operation_id: Option<String>,
|
||||||
) -> Result<UpgradeAllAgentDriversResult, String> {
|
) -> Result<UpgradeAllAgentDriversResult, String> {
|
||||||
let source = source.unwrap_or_default();
|
let source = source.unwrap_or_default();
|
||||||
let registry = fetch_registry_from(source).await?;
|
let registry = fetch_registry_from(source).await?;
|
||||||
@@ -91,8 +94,11 @@ pub async fn upgrade_all_agents(
|
|||||||
agents.iter().filter(|agent| agent.update_available).map(|agent| agent.db_type.clone()).collect();
|
agents.iter().filter(|agent| agent.update_available).map(|agent| agent.db_type.clone()).collect();
|
||||||
ensure_no_agent_update_blockers(state.inner().as_ref(), &updatable).await?;
|
ensure_no_agent_update_blockers(state.inner().as_ref(), &updatable).await?;
|
||||||
let app_handle = app.clone();
|
let app_handle = app.clone();
|
||||||
upgrade_all_agent_drivers_from(&state.agent_manager, source, move |event| emit_agent_progress(&app_handle, event))
|
let operation_id = operation_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||||
.await
|
upgrade_all_agent_drivers_from(&state.agent_manager, source, move |event| {
|
||||||
|
emit_agent_progress(&app_handle, &operation_id, event)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -136,9 +142,7 @@ pub async fn set_agent_java_runtime_config(
|
|||||||
config.custom_java_path = None;
|
config.custom_java_path = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut local_state = am.load_state();
|
am.mutate_state(|local_state| local_state.java_runtime = config.clone())?;
|
||||||
local_state.java_runtime = config.clone();
|
|
||||||
am.save_state(&local_state)?;
|
|
||||||
am.stop_daemons().await;
|
am.stop_daemons().await;
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
@@ -159,15 +163,19 @@ pub async fn import_agents_from_zip(
|
|||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
state: State<'_, Arc<AppState>>,
|
state: State<'_, Arc<AppState>>,
|
||||||
path: String,
|
path: String,
|
||||||
|
operation_id: Option<String>,
|
||||||
) -> Result<u32, String> {
|
) -> Result<u32, String> {
|
||||||
let am = &state.agent_manager;
|
let am = &state.agent_manager;
|
||||||
let zip_path = std::path::PathBuf::from(&path);
|
let zip_path = std::path::PathBuf::from(&path);
|
||||||
let plan = inspect_offline_zip(&zip_path)?;
|
let plan = inspect_offline_zip(&zip_path)?;
|
||||||
ensure_no_offline_import_blockers(state.inner().as_ref(), &plan).await?;
|
ensure_no_offline_import_blockers(state.inner().as_ref(), &plan).await?;
|
||||||
let app_handle = app.clone();
|
let app_handle = app.clone();
|
||||||
let result = import_agents_from_zip_core(am, &zip_path, |event| emit_agent_progress(&app_handle, event))?;
|
let operation_id = operation_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||||
|
let result =
|
||||||
|
import_agents_from_zip_core(am, &zip_path, |event| emit_agent_progress(&app_handle, &operation_id, event))
|
||||||
|
.await?;
|
||||||
let count = result.drivers_installed.len() as u32;
|
let count = result.drivers_installed.len() as u32;
|
||||||
emit_agent_progress(&app, AgentProgressEvent::step("done"));
|
emit_agent_progress(&app, &operation_id, AgentProgressEvent::step("done"));
|
||||||
Ok(count)
|
Ok(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +186,7 @@ pub async fn import_agent_driver_cmd(
|
|||||||
path: String,
|
path: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
ensure_no_agent_update_blockers(state.inner().as_ref(), std::slice::from_ref(&db_type)).await?;
|
ensure_no_agent_update_blockers(state.inner().as_ref(), std::slice::from_ref(&db_type)).await?;
|
||||||
import_agent_driver(&state.agent_manager, &db_type, std::path::Path::new(&path))
|
import_agent_driver(&state.agent_manager, &db_type, std::path::Path::new(&path)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -188,7 +196,7 @@ pub async fn import_agent_jar_cmd(
|
|||||||
path: String,
|
path: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
ensure_no_agent_update_blockers(state.inner().as_ref(), std::slice::from_ref(&db_type)).await?;
|
ensure_no_agent_update_blockers(state.inner().as_ref(), std::slice::from_ref(&db_type)).await?;
|
||||||
import_agent_driver(&state.agent_manager, &db_type, std::path::Path::new(&path))
|
import_agent_driver(&state.agent_manager, &db_type, std::path::Path::new(&path)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -197,17 +205,19 @@ pub async fn reinstall_jre(
|
|||||||
state: State<'_, Arc<AppState>>,
|
state: State<'_, Arc<AppState>>,
|
||||||
jre_key: Option<String>,
|
jre_key: Option<String>,
|
||||||
source: Option<DownloadSource>,
|
source: Option<DownloadSource>,
|
||||||
|
operation_id: Option<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let key = jre_key.as_deref().unwrap_or(DEFAULT_JRE_KEY);
|
let key = jre_key.as_deref().unwrap_or(DEFAULT_JRE_KEY);
|
||||||
let app_handle = app.clone();
|
let app_handle = app.clone();
|
||||||
|
let operation_id = operation_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||||
reinstall_agent_jre_from(&state.agent_manager, key, source.unwrap_or_default(), move |event| {
|
reinstall_agent_jre_from(&state.agent_manager, key, source.unwrap_or_default(), move |event| {
|
||||||
emit_agent_progress(&app_handle, event)
|
emit_agent_progress(&app_handle, &operation_id, event)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn emit_agent_progress(app: &tauri::AppHandle, event: AgentProgressEvent) {
|
fn emit_agent_progress(app: &tauri::AppHandle, operation_id: &str, event: AgentProgressEvent) {
|
||||||
let _ = app.emit("agent-install-progress", event);
|
let _ = app.emit("agent-install-progress", event.with_operation_id(operation_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_no_agent_update_blockers(state: &AppState, db_types: &[String]) -> Result<(), String> {
|
async fn ensure_no_agent_update_blockers(state: &AppState, db_types: &[String]) -> Result<(), String> {
|
||||||
|
|||||||
Reference in new issue
Block a user