refactor(update-center): 消除刷新重试代码重复并加对称设计注释

- updateCenter.ts: 提取 getBackoffDelay,补充与主进程预刷新为对称设计的说明
- index.ts: 提取 getPreRefreshBackoffDelay,对齐命名与注释
- 仅消除重复拼写、补注释,刷新重试功能行为不变
- 超时保护仍只作用于刷新源路径,不波及列表加载与其他 IPC

验证: eslint + vue-tsc 通过,test-build 5.2.1.14-test 通过
This commit is contained in:
xiyidaiwa
2026-08-10 23:14:18 +08:00
parent 3f22207505
commit 5bd0a367c4
4 changed files with 239 additions and 126 deletions
+127 -105
View File
@@ -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<typeof ipcMain, "handle">,
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(
+48 -1
View File
@@ -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;