From 5bd0a367c4d22196d36ac9e59fce3cb09c048d97 Mon Sep 17 00:00:00 2001 From: xiyidaiwa Date: Mon, 10 Aug 2026 23:14:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor(update-center):=20=E6=B6=88=E9=99=A4?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E9=87=8D=E8=AF=95=E4=BB=A3=E7=A0=81=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=B9=B6=E5=8A=A0=E5=AF=B9=E7=A7=B0=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - updateCenter.ts: 提取 getBackoffDelay,补充与主进程预刷新为对称设计的说明 - index.ts: 提取 getPreRefreshBackoffDelay,对齐命名与注释 - 仅消除重复拼写、补注释,刷新重试功能行为不变 - 超时保护仍只作用于刷新源路径,不波及列表加载与其他 IPC 验证: eslint + vue-tsc 通过,test-build 5.2.1.14-test 通过 --- electron/main/backend/update-center/index.ts | 232 ++++++++++--------- electron/main/index.ts | 49 +++- src/App.vue | 3 + src/modules/updateCenter.ts | 81 +++++-- 4 files changed, 239 insertions(+), 126 deletions(-) diff --git a/electron/main/backend/update-center/index.ts b/electron/main/backend/update-center/index.ts index 6aa1d3e0..16914ddc 100644 --- a/electron/main/backend/update-center/index.ts +++ b/electron/main/backend/update-center/index.ts @@ -512,6 +512,131 @@ export const loadUpdateCenterItems = async ( }; }; +// 子进程超时(毫秒):网络慢/镜像源卡死时,避免命令永久挂起 +const SYSTEM_UPDATE_COMMAND_TIMEOUT_MS = 90_000; + +// 带超时保护的命令执行:超时杀掉子进程并 resolve,防止调用方永久冻结 +const runCommandWithTimeout = ( + command: string, + args: string[], +): Promise<{ code: number; stdout: string; stderr: string }> => + new Promise((resolve) => { + const child = spawn(command, args, { shell: false, env: process.env }); + let stdout = ""; + let stderr = ""; + let settled = false; + const timer = setTimeout(() => { + if (settled) return; + settled = true; + try { + child.kill("SIGKILL"); + } catch { + // 忽略杀进程异常 + } + resolve({ + code: -1, + stdout, + stderr: `${stderr}\n[timeout] command exceeded ${SYSTEM_UPDATE_COMMAND_TIMEOUT_MS}ms`, + }); + }, SYSTEM_UPDATE_COMMAND_TIMEOUT_MS); + const finish = (result: { + code: number; + stdout: string; + stderr: string; + }): void => { + if (settled) return; + settled = true; + clearTimeout(timer); + resolve(result); + }; + child.stdout?.on("data", (data) => { + stdout += data.toString(); + }); + child.stderr?.on("data", (data) => { + stderr += data.toString(); + }); + child.on("error", (err) => + finish({ code: -1, stdout, stderr: err.message }), + ); + child.on("close", (code) => finish({ code: code ?? -1, stdout, stderr })); + }); + +// 刷新软件源(aptss ssupdate + apm update),提权执行。 +// 供更新中心 IPC 与“启动后空闲预刷新”复用,单一逻辑来源。 +export const runSystemUpdateSources = async ( + storeFilter: StoreFilter = "both", +): Promise<{ aptss?: string; apm?: string }> => { + console.log( + `[UpdateCenter] runSystemUpdateSources called with storeFilter=${storeFilter}`, + ); + + const results: { aptss?: string; apm?: string } = {}; + const isSourceEnabled = ( + filter: StoreFilter, + source: "spark" | "apm", + ): boolean => filter === "both" || filter === source; + + if (isSourceEnabled(storeFilter, "spark")) { + const whichResult = await runCommandWithTimeout("which", ["aptss"]); + const aptssAvailable = + whichResult.code === 0 && whichResult.stdout.trim().length > 0; + if (aptssAvailable) { + console.log("[UpdateCenter] Running: pkexec shell-caller aptss ssupdate"); + const superUserCmd = await findExecutable( + SUPER_USER_COMMAND_CANDIDATES[0], + ); + if (superUserCmd) { + const result = await runCommandWithTimeout(superUserCmd, [ + SHELL_CALLER_PATH, + "aptss", + "ssupdate", + ]); + results.aptss = + result.code === 0 + ? "ok" + : `failed: ${result.stderr.substring(0, 200)}`; + console.log("[UpdateCenter] aptss ssupdate result:", results.aptss); + } else { + results.aptss = "failed: pkexec not found"; + console.warn("[UpdateCenter] pkexec not found, skipping aptss update"); + } + } else { + results.aptss = "skipped: aptss not installed"; + } + } + + if (isSourceEnabled(storeFilter, "apm")) { + const whichResult = await runCommandWithTimeout("which", ["apm"]); + const apmAvailable = + whichResult.code === 0 && whichResult.stdout.trim().length > 0; + if (apmAvailable) { + console.log("[UpdateCenter] Running: pkexec shell-caller apm update"); + const superUserCmd = await findExecutable( + SUPER_USER_COMMAND_CANDIDATES[0], + ); + if (superUserCmd) { + const result = await runCommandWithTimeout(superUserCmd, [ + SHELL_CALLER_PATH, + "apm", + "update", + ]); + results.apm = + result.code === 0 + ? "ok" + : `failed: ${result.stderr.substring(0, 200)}`; + console.log("[UpdateCenter] apm update result:", results.apm); + } else { + results.apm = "failed: pkexec not found"; + console.warn("[UpdateCenter] pkexec not found, skipping apm update"); + } + } else { + results.apm = "skipped: apm not installed"; + } + } + + return results; +}; + export const registerUpdateCenterIpc = ( ipc: Pick, service: Pick< @@ -528,111 +653,8 @@ export const registerUpdateCenterIpc = ( ): void => { ipc.handle( "update-center-run-system-update", - async (_event, storeFilter: StoreFilter = "both") => { - console.log( - `[UpdateCenter] update-center-run-system-update called with storeFilter=${storeFilter}`, - ); - - const results: { aptss?: string; apm?: string } = {}; - - const runCommand = ( - command: string, - args: string[], - ): Promise<{ code: number; stdout: string; stderr: string }> => - new Promise((resolve) => { - const child = spawn(command, args, { - shell: false, - env: process.env, - }); - let stdout = ""; - let stderr = ""; - child.stdout?.on("data", (data) => { - stdout += data.toString(); - }); - child.stderr?.on("data", (data) => { - stderr += data.toString(); - }); - child.on("error", (err) => - resolve({ code: -1, stdout, stderr: err.message }), - ); - child.on("close", (code) => - resolve({ code: code ?? -1, stdout, stderr }), - ); - }); - - const isSourceEnabled = ( - filter: StoreFilter, - source: "spark" | "apm", - ): boolean => filter === "both" || filter === source; - - // aptss update — 需要提权 - if (isSourceEnabled(storeFilter, "spark")) { - const whichResult = await runCommand("which", ["aptss"]); - const aptssAvailable = - whichResult.code === 0 && whichResult.stdout.trim().length > 0; - if (aptssAvailable) { - console.log( - "[UpdateCenter] Running: pkexec shell-caller aptss ssupdate", - ); - const superUserCmd = await findExecutable( - SUPER_USER_COMMAND_CANDIDATES[0], - ); - if (superUserCmd) { - const result = await runCommand(superUserCmd, [ - SHELL_CALLER_PATH, - "aptss", - "ssupdate", - ]); - results.aptss = - result.code === 0 - ? "ok" - : `failed: ${result.stderr.substring(0, 200)}`; - console.log("[UpdateCenter] aptss ssupdate result:", results.aptss); - } else { - results.aptss = "failed: pkexec not found"; - console.warn( - "[UpdateCenter] pkexec not found, skipping aptss update", - ); - } - } else { - results.aptss = "skipped: aptss not installed"; - } - } - - // apm update — 也需要提权 - if (isSourceEnabled(storeFilter, "apm")) { - const whichResult = await runCommand("which", ["apm"]); - const apmAvailable = - whichResult.code === 0 && whichResult.stdout.trim().length > 0; - if (apmAvailable) { - console.log("[UpdateCenter] Running: pkexec shell-caller apm update"); - const superUserCmd = await findExecutable( - SUPER_USER_COMMAND_CANDIDATES[0], - ); - if (superUserCmd) { - const result = await runCommand(superUserCmd, [ - SHELL_CALLER_PATH, - "apm", - "update", - ]); - results.apm = - result.code === 0 - ? "ok" - : `failed: ${result.stderr.substring(0, 200)}`; - console.log("[UpdateCenter] apm update result:", results.apm); - } else { - results.apm = "failed: pkexec not found"; - console.warn( - "[UpdateCenter] pkexec not found, skipping apm update", - ); - } - } else { - results.apm = "skipped: apm not installed"; - } - } - - return results; - }, + async (_event, storeFilter: StoreFilter = "both") => + runSystemUpdateSources(storeFilter), ); ipc.handle( diff --git a/electron/main/index.ts b/electron/main/index.ts index eb7e6fd4..ce217580 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -19,7 +19,10 @@ import { handleCommandLine } from "./deeplink.js"; import { isLoaded } from "../global.js"; import { tasks } from "./backend/install-manager.js"; import { sendTelemetryOnce } from "./backend/telemetry.js"; -import { initializeUpdateCenter } from "./backend/update-center/index.js"; +import { + initializeUpdateCenter, + runSystemUpdateSources, +} from "./backend/update-center/index.js"; import { getMainWindowCloseAction, type MainWindowCloseGuardState, @@ -656,8 +659,52 @@ app.whenReady().then(() => { initializeUpdateCenter(); // 启动后执行一次遥测(仅 Linux,不阻塞) sendTelemetryOnce(getAppVersion()); + + // 注册“渲染进程首页加载完成”信号:收到后立即开始后台刷新软件源, + // 趁系统负载不高时提前刷新 aptss/apm 源,用户稍后打开“软件更新”即可秒出。 + // 同时保留一个兜底定时器,防止渲染进程未发信号时完全不刷新。 + ipcMain.on("update-center-trigger-prefetch", () => { + startSourcePreRefreshOnce(); + }); + setTimeout(startSourcePreRefreshOnce, PRE_REFRESH_FALLBACK_MS); }); +// 启动后空闲预刷新软件源(带重试),不阻塞启动流程 +// 与 src/modules/updateCenter.ts 的 backgroundRefresh 重试为【对称设计】,非代码遗漏: +// 主进程负责“启动预热”,前端负责“打开兜底”, +// 两者进程/守卫/调用目标不同,故各自保留一份,勿抽共享。 +const PRE_REFRESH_BACKOFF_MS = [2000, 4000, 8000]; +const PRE_REFRESH_MAX_RETRIES = 3; +const PRE_REFRESH_FALLBACK_MS = 15_000; // 渲染信号未到达时的兜底,15s 后也跑 +let preRefreshStarted = false; + +// 按重试次数取退避毫秒(越界时回退到最大间隔) +const getPreRefreshBackoffDelay = (attempt: number): number => + PRE_REFRESH_BACKOFF_MS[attempt - 1] ?? + PRE_REFRESH_BACKOFF_MS[PRE_REFRESH_BACKOFF_MS.length - 1]; + +// 确保预刷新只触发一次(渲染信号或兜底定时器 whichever first) +const startSourcePreRefreshOnce = (attempt = 1): void => { + if (preRefreshStarted) return; + preRefreshStarted = true; + + const run = (): void => { + runSystemUpdateSources("both") + .then((results) => { + console.log("[UpdateCenter] pre-refresh done:", results); + }) + .catch((error) => { + console.warn("[UpdateCenter] pre-refresh failed:", error); + if (attempt < PRE_REFRESH_MAX_RETRIES) { + const delay = getPreRefreshBackoffDelay(attempt); + setTimeout(() => startSourcePreRefreshOnce(attempt + 1), delay); + } + }); + }; + + run(); +}; + app.on("window-all-closed", () => { win = null; allowAppExit = false; diff --git a/src/App.vue b/src/App.vue index 5283413e..20e96fdb 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3466,6 +3466,9 @@ onMounted(async () => { window.ipcRenderer.on("remove-complete", handleRemoveComplete); window.ipcRenderer.send("renderer-ready", { status: true }); + // 首页/数据加载完成(主界面已可交互),通知主进程立即开始后台刷新软件源, + // 趁系统负载不高时提前刷新 aptss/apm 源,用户稍后打开“软件更新”即可秒出。 + window.ipcRenderer.send("update-center-trigger-prefetch"); logger.info("Renderer process is ready!"); }); diff --git a/src/modules/updateCenter.ts b/src/modules/updateCenter.ts index 4780b6ea..c26d7cce 100644 --- a/src/modules/updateCenter.ts +++ b/src/modules/updateCenter.ts @@ -133,11 +133,8 @@ export const createUpdateCenterStore = (): UpdateCenterStore => { isBound = false; }; - // 先刷新软件源,再加载更新列表;刷新失败仅告警,不阻断扫描 - const runSystemUpdateThenLoad = async ( - storeFilter: StoreFilter, - load: (filter: StoreFilter) => Promise, - ): Promise => { + // 刷新软件源(不加载列表):网络慢/卡死由主进程超时保护,不会永久挂起 + const runSystemUpdate = async (storeFilter: StoreFilter): Promise => { try { await window.ipcRenderer.invoke( "update-center-run-system-update", @@ -145,9 +142,61 @@ export const createUpdateCenterStore = (): UpdateCenterStore => { ); } catch (error) { console.error("[UpdateCenter] system update failed", error); + throw error; + } + }; + + // 主动刷新:先刷新源再加载列表(用户点击刷新按钮时使用,期望即时结果) + const refresh = async ( + storeFilter: StoreFilter = lastStoreFilter, + ): Promise => { + lastStoreFilter = storeFilter; + loading.value = true; + try { + await runSystemUpdate(storeFilter); + const nextSnapshot = await window.updateCenter.refresh(storeFilter); + applySnapshot(nextSnapshot); + } finally { + loading.value = false; + } + }; + + // 打开更新中心:先用缓存秒开列表(不卡 UI),再后台刷新源并重新加载。 + // 后台刷新带指数退避重试;窗口关闭即停,避免无效重试。 + // 与 electron/main/index.ts 的预刷新重试为【对称设计】,非代码遗漏: + // 前端负责“打开更新中心兜底”,主进程负责“启动预热”, + // 两者进程/守卫/调用目标不同,故各自保留一份,勿抽共享。 + const MAX_BACKGROUND_RETRIES = 3; + const BACKGROUND_BACKOFF_MS = [2000, 4000, 8000]; + + // 按重试次数取退避毫秒(越界时回退到最大间隔) + const getBackoffDelay = (attempt: number): number => + BACKGROUND_BACKOFF_MS[attempt - 1] ?? + BACKGROUND_BACKOFF_MS[BACKGROUND_BACKOFF_MS.length - 1]; + + const backgroundRefresh = async ( + storeFilter: StoreFilter, + attempt: number, + ): Promise => { + if (!isOpen.value) return; // 窗口已关闭,停止重试 + try { + await runSystemUpdate(storeFilter); + const nextSnapshot = await window.updateCenter.refresh(storeFilter); + if (!isOpen.value) return; + applySnapshot(nextSnapshot); + } catch (error) { + if (attempt >= MAX_BACKGROUND_RETRIES) { + console.warn( + `[UpdateCenter] background refresh failed after ${MAX_BACKGROUND_RETRIES} attempts`, + error, + ); + return; + } + const delay = getBackoffDelay(attempt); + window.setTimeout(() => { + void backgroundRefresh(storeFilter, attempt + 1); + }, delay); } - const nextSnapshot = await load(storeFilter); - applySnapshot(nextSnapshot); }; const open = async (storeFilter: StoreFilter = "both"): Promise => { @@ -156,22 +205,14 @@ export const createUpdateCenterStore = (): UpdateCenterStore => { isOpen.value = true; loading.value = true; try { - await runSystemUpdateThenLoad(storeFilter, window.updateCenter.open); - } finally { - loading.value = false; - } - }; - - const refresh = async ( - storeFilter: StoreFilter = lastStoreFilter, - ): Promise => { - lastStoreFilter = storeFilter; - loading.value = true; - try { - await runSystemUpdateThenLoad(storeFilter, window.updateCenter.refresh); + // 1. 先加载缓存,立即显示列表(秒开,不阻塞于网络刷新) + const cachedSnapshot = await window.updateCenter.open(storeFilter); + applySnapshot(cachedSnapshot); } finally { loading.value = false; } + // 2. 后台异步刷新源并在完成后更新列表(失败自动重试) + void backgroundRefresh(storeFilter, 1); }; const ignoreItem = async (