mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-09-20 21:50:11 +08:00
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:
@@ -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 = (
|
export const registerUpdateCenterIpc = (
|
||||||
ipc: Pick<typeof ipcMain, "handle">,
|
ipc: Pick<typeof ipcMain, "handle">,
|
||||||
service: Pick<
|
service: Pick<
|
||||||
@@ -528,111 +653,8 @@ export const registerUpdateCenterIpc = (
|
|||||||
): void => {
|
): void => {
|
||||||
ipc.handle(
|
ipc.handle(
|
||||||
"update-center-run-system-update",
|
"update-center-run-system-update",
|
||||||
async (_event, storeFilter: StoreFilter = "both") => {
|
async (_event, storeFilter: StoreFilter = "both") =>
|
||||||
console.log(
|
runSystemUpdateSources(storeFilter),
|
||||||
`[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;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ipc.handle(
|
ipc.handle(
|
||||||
|
|||||||
+48
-1
@@ -19,7 +19,10 @@ import { handleCommandLine } from "./deeplink.js";
|
|||||||
import { isLoaded } from "../global.js";
|
import { isLoaded } from "../global.js";
|
||||||
import { tasks } from "./backend/install-manager.js";
|
import { tasks } from "./backend/install-manager.js";
|
||||||
import { sendTelemetryOnce } from "./backend/telemetry.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 {
|
import {
|
||||||
getMainWindowCloseAction,
|
getMainWindowCloseAction,
|
||||||
type MainWindowCloseGuardState,
|
type MainWindowCloseGuardState,
|
||||||
@@ -656,7 +659,51 @@ app.whenReady().then(() => {
|
|||||||
initializeUpdateCenter();
|
initializeUpdateCenter();
|
||||||
// 启动后执行一次遥测(仅 Linux,不阻塞)
|
// 启动后执行一次遥测(仅 Linux,不阻塞)
|
||||||
sendTelemetryOnce(getAppVersion());
|
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", () => {
|
app.on("window-all-closed", () => {
|
||||||
win = null;
|
win = null;
|
||||||
|
|||||||
@@ -3466,6 +3466,9 @@ onMounted(async () => {
|
|||||||
window.ipcRenderer.on("remove-complete", handleRemoveComplete);
|
window.ipcRenderer.on("remove-complete", handleRemoveComplete);
|
||||||
|
|
||||||
window.ipcRenderer.send("renderer-ready", { status: true });
|
window.ipcRenderer.send("renderer-ready", { status: true });
|
||||||
|
// 首页/数据加载完成(主界面已可交互),通知主进程立即开始后台刷新软件源,
|
||||||
|
// 趁系统负载不高时提前刷新 aptss/apm 源,用户稍后打开“软件更新”即可秒出。
|
||||||
|
window.ipcRenderer.send("update-center-trigger-prefetch");
|
||||||
logger.info("Renderer process is ready!");
|
logger.info("Renderer process is ready!");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+60
-19
@@ -133,11 +133,8 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
|||||||
isBound = false;
|
isBound = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 先刷新软件源,再加载更新列表;刷新失败仅告警,不阻断扫描
|
// 刷新软件源(不加载列表):网络慢/卡死由主进程超时保护,不会永久挂起
|
||||||
const runSystemUpdateThenLoad = async (
|
const runSystemUpdate = async (storeFilter: StoreFilter): Promise<void> => {
|
||||||
storeFilter: StoreFilter,
|
|
||||||
load: (filter: StoreFilter) => Promise<UpdateCenterSnapshot>,
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
try {
|
||||||
await window.ipcRenderer.invoke(
|
await window.ipcRenderer.invoke(
|
||||||
"update-center-run-system-update",
|
"update-center-run-system-update",
|
||||||
@@ -145,9 +142,61 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[UpdateCenter] system update failed", error);
|
console.error("[UpdateCenter] system update failed", error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
const nextSnapshot = await load(storeFilter);
|
};
|
||||||
|
|
||||||
|
// 主动刷新:先刷新源再加载列表(用户点击刷新按钮时使用,期望即时结果)
|
||||||
|
const refresh = async (
|
||||||
|
storeFilter: StoreFilter = lastStoreFilter,
|
||||||
|
): Promise<void> => {
|
||||||
|
lastStoreFilter = storeFilter;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await runSystemUpdate(storeFilter);
|
||||||
|
const nextSnapshot = await window.updateCenter.refresh(storeFilter);
|
||||||
applySnapshot(nextSnapshot);
|
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<void> => {
|
||||||
|
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 open = async (storeFilter: StoreFilter = "both"): Promise<void> => {
|
const open = async (storeFilter: StoreFilter = "both"): Promise<void> => {
|
||||||
@@ -156,22 +205,14 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
|||||||
isOpen.value = true;
|
isOpen.value = true;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
await runSystemUpdateThenLoad(storeFilter, window.updateCenter.open);
|
// 1. 先加载缓存,立即显示列表(秒开,不阻塞于网络刷新)
|
||||||
} finally {
|
const cachedSnapshot = await window.updateCenter.open(storeFilter);
|
||||||
loading.value = false;
|
applySnapshot(cachedSnapshot);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const refresh = async (
|
|
||||||
storeFilter: StoreFilter = lastStoreFilter,
|
|
||||||
): Promise<void> => {
|
|
||||||
lastStoreFilter = storeFilter;
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
await runSystemUpdateThenLoad(storeFilter, window.updateCenter.refresh);
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
// 2. 后台异步刷新源并在完成后更新列表(失败自动重试)
|
||||||
|
void backgroundRefresh(storeFilter, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ignoreItem = async (
|
const ignoreItem = async (
|
||||||
|
|||||||
Reference in New Issue
Block a user