feat(agents): concurrent all-agent-driver upgrade

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-07-24 23:50:50 +08:00
1 parent 2a99f91099
commit 95db01e359
15 files changed
+1083 -282

No files matched your search

+10 -7
View File
@@ -95,25 +95,28 @@ export const clearDriverDownloadCache = forward("clearDriverDownloadCache");
export const getDriverRuntimeSummary = forward("getDriverRuntimeSummary");
export const stopDriverRuntime = forward("stopDriverRuntime");
export const restartDriverRuntime = forward("restartDriverRuntime");
export async function installAgent(dbType: string) {
export async function installAgent(dbType: string, operationId?: string) {
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();
return backend.upgradeAllAgents(useSettingsStore().editorSettings.updateDownloadSource);
return backend.upgradeAllAgents(useSettingsStore().editorSettings.updateDownloadSource, operationId);
}
export const checkAgentUpdateBlockers = forward("checkAgentUpdateBlockers");
export const uninstallAgent = forward("uninstallAgent");
export const getAgentJavaRuntimeConfig = forward("getAgentJavaRuntimeConfig");
export const setAgentJavaRuntimeConfig = forward("setAgentJavaRuntimeConfig");
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 importAgentJar = importAgentDriver;
export async function reinstallJre(jreKey?: string) {
export async function reinstallJre(jreKey?: string, operationId?: string) {
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 listenAgentInstallProgress = forward("listenAgentInstallProgress");
+8 -7
View File
@@ -437,12 +437,12 @@ export async function restartDriverRuntime(runtimeId: string): Promise<void> {
await post("/api/agents/runtime/restart", { runtimeId });
}
export async function installAgent(dbType: string, _source?: UpdateDownloadSource): Promise<void> {
await post("/api/agents/install", { dbType });
export async function installAgent(dbType: string, _source?: UpdateDownloadSource, operationId?: string): Promise<void> {
await post("/api/agents/install", { dbType, operationId });
}
export async function upgradeAllAgents(_source?: UpdateDownloadSource): Promise<UpgradeAllAgentDriversResult> {
return post("/api/agents/upgrade-all", {});
export async function upgradeAllAgents(_source?: UpdateDownloadSource, operationId?: string): Promise<UpgradeAllAgentDriversResult> {
return post("/api/agents/upgrade-all", { operationId });
}
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", {});
}
export async function importAgentsFromZip(fileOrPath: string | File): Promise<number> {
export async function importAgentsFromZip(fileOrPath: string | File, operationId?: string): Promise<number> {
if (typeof fileOrPath === "string") {
throw new Error("Offline ZIP import in web mode requires a File object, not a file path");
}
const formData = new FormData();
if (operationId) formData.append("operationId", operationId);
formData.append("file", fileOrPath);
const res = await fetch(apiUrl("/api/agents/import-offline"), { method: "POST", body: formData });
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 async function reinstallJre(jreKey?: string, _source?: UpdateDownloadSource): Promise<void> {
await post("/api/agents/reinstall-jre", { jreKey });
export async function reinstallJre(jreKey?: string, _source?: UpdateDownloadSource, operationId?: string): Promise<void> {
await post("/api/agents/reinstall-jre", { jreKey, operationId });
}
export async function uninstallJre(jreKey: string): Promise<void> {
+9 -8
View File
@@ -318,6 +318,7 @@ export interface DroppedFilePreviewSqlOptions {
export type XlsxCellValue = string | number | boolean | null;
export interface DriverInstallProgress {
operation_id?: string;
step: string;
downloaded?: number;
total?: number;
@@ -1364,12 +1365,12 @@ export async function restartDriverRuntime(runtimeId: string): Promise<void> {
return invoke("restart_driver_runtime", { runtimeId });
}
export async function installAgent(dbType: string, source?: UpdateDownloadSource): Promise<void> {
return invoke("install_agent", { dbType, source });
export async function installAgent(dbType: string, source?: UpdateDownloadSource, operationId?: string): Promise<void> {
return invoke("install_agent", { dbType, source, operationId });
}
export async function upgradeAllAgents(source?: UpdateDownloadSource): Promise<UpgradeAllAgentDriversResult> {
return invoke("upgrade_all_agents", { source });
export async function upgradeAllAgents(source?: UpdateDownloadSource, operationId?: string): Promise<UpgradeAllAgentDriversResult> {
return invoke("upgrade_all_agents", { source, operationId });
}
export async function checkAgentUpdateBlockers(dbTypes: string[]): Promise<AgentUpdateBlocker[]> {
@@ -1392,11 +1393,11 @@ export async function invalidateAgentRegistryCache(): Promise<void> {
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") {
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> {
@@ -1408,8 +1409,8 @@ export async function importAgentDriver(dbType: string, path: string | File): Pr
export const importAgentJar = importAgentDriver;
export async function reinstallJre(jreKey?: string, source?: UpdateDownloadSource): Promise<void> {
return invoke("reinstall_jre", { jreKey, source });
export async function reinstallJre(jreKey?: string, source?: UpdateDownloadSource, operationId?: string): Promise<void> {
return invoke("reinstall_jre", { jreKey, source, operationId });
}
export async function uninstallJre(jreKey: string): Promise<void> {
@@ -1,5 +1,5 @@
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", () => {
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);
});
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", () => {
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(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 {
operation_id?: string;
step: string;
downloaded?: number;
total?: number;
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 {
installing: string | null;
upgradingAll: boolean;
progress: DriverInstallProgress | null;
progressMap: Record<string, DriverInstallProgress | null | undefined>;
}
export type DriverInstallProgressChannel = "agent" | "jdbc-plugin";
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 {
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";
@@ -28,7 +39,26 @@ export function updateDriverInstallProgress(current: DriverInstallProgress | nul
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;
const percent = Math.round(((progress.downloaded ?? 0) / progress.total) * 100);
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 {
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[] {