fix(update-center): 修复更新中心点击更新后卡在"开始更新..."不动

- 根因:service.ts 用 webContents.send("queue-install") 向主下载队列发任务,
  但 webContents.send 的目标对象是渲染进程,主进程 ipcMain 监听不到自己发出的消息,
  导致任务从未进入 install-manager 的下载队列,UI 永远卡在 queued。
- 将 install-manager.ts 的 queue-install 处理逻辑抽为导出的 addInstallTask(payload, sender),
  新增 QueueInstallPayload 类型,避免 any。
- 更新中心 service.ts 的 start() 改为直接 await addInstallTask(...),任务真正入队。
- 文档 docs/app-list-cache-analysis.md 新增第 9 节记录本修复。
This commit is contained in:
xiyidaiwa
2026-08-13 09:37:17 +08:00
parent d7bdcf3680
commit b5c5ab04b5
3 changed files with 92 additions and 35 deletions
+54 -31
View File
@@ -50,6 +50,16 @@ const isOriginEnabled = (
return storeFilter === "both" || storeFilter === origin;
};
export interface QueueInstallPayload {
id: number;
pkgname: string;
metalinkUrl?: string;
filename?: string;
origin?: "spark" | "apm";
upgradeOnly?: boolean;
retry?: boolean;
}
type InstallTask = {
id: number;
pkgname: string;
@@ -283,34 +293,30 @@ const parseUpgradableList = (output: string) => {
return apps;
};
// Listen for download requests from renderer process
ipcMain.on("queue-install", async (event, download_json) => {
let download: unknown;
try {
download =
typeof download_json === "string"
? JSON.parse(download_json)
: download_json;
} catch (err) {
logger.error({ err }, "queue-install: invalid JSON payload, ignoring task");
return;
}
const { id, pkgname, metalinkUrl, filename, origin, upgradeOnly } =
download || {};
/**
* 将任务加入安装/下载队列。被 IPC 监听与更新中心等内部模块共享使用。
* 入队前会执行包名、文件名、metalinkUrl 的校验,并检查重复任务。
* @returns 是否成功入队
*/
export const addInstallTask = async (
download: QueueInstallPayload,
sender: WebContents | null,
): Promise<boolean> => {
const { id, pkgname, metalinkUrl, filename, origin, upgradeOnly } = download;
if (!id || !pkgname) {
logger.warn("passed arguments missing id or pkgname");
return;
logger.warn("addInstallTask: passed arguments missing id or pkgname");
return false;
}
// 包名/文件名白名单校验:防止路径遍历(如 ../../)或非法字符进入下载目录与安装命令构建
if (!PKGNAME_PATTERN.test(pkgname)) {
logger.warn(`queue-install invalid pkgname: ${pkgname}`);
return;
logger.warn(`addInstallTask invalid pkgname: ${pkgname}`);
return false;
}
if (filename && !PKGNAME_PATTERN.test(filename)) {
logger.warn(`queue-install invalid filename: ${filename}`);
return;
logger.warn(`addInstallTask invalid filename: ${filename}`);
return false;
}
// metalinkUrl 来自渲染端(由目录 filename / 主进程解析的 downloadUrl 拼成)。
@@ -322,14 +328,14 @@ ipcMain.on("queue-install", async (event, download_json) => {
"https://erotica.spark-app.store",
);
if (!isRelative && !isOfficial) {
logger.warn(`queue-install invalid metalinkUrl: ${metalinkUrl}`);
return;
logger.warn(`addInstallTask invalid metalinkUrl: ${metalinkUrl}`);
return false;
}
}
logger.info(`收到下载任务: ${id}, 软件包名称: ${pkgname}, 来源: ${origin}`);
const webContents = event.sender;
const webContents = sender;
// 避免重复添加同一任务(检查 pkgname + origin),但允许重试下载
if (!download.retry) {
@@ -337,12 +343,12 @@ ipcMain.on("queue-install", async (event, download_json) => {
(t) => t.pkgname === pkgname && t.origin === origin,
);
if (existingTask) {
webContents.send("install-log", {
webContents?.send("install-log", {
id,
time: Date.now(),
message: `任务 ${pkgname} (${origin}) 已在列表中,忽略重复添加`,
});
webContents.send("install-complete", {
webContents?.send("install-complete", {
id,
success: false,
time: Date.now(),
@@ -353,12 +359,12 @@ ipcMain.on("queue-install", async (event, download_json) => {
stderr: "",
}),
});
return;
return false;
}
}
const superUserCmd = await checkSuperUserCommand();
let execCommand = "";
const execParams = [];
const execParams: string[] = [];
const downloadDir = path.join(
os.tmpdir(),
`spark-store-${process.pid}`,
@@ -370,8 +376,8 @@ ipcMain.on("queue-install", async (event, download_json) => {
if (origin === "apm") {
const hasApm = await checkApmAvailable();
if (!hasApm) {
webContents.send("trigger-apm-install-dialog");
webContents.send("install-complete", {
webContents?.send("trigger-apm-install-dialog");
webContents?.send("install-complete", {
id,
success: false,
time: Date.now(),
@@ -382,7 +388,7 @@ ipcMain.on("queue-install", async (event, download_json) => {
stderr: "",
}),
});
return;
return false;
}
}
@@ -420,7 +426,7 @@ ipcMain.on("queue-install", async (event, download_json) => {
const safeFilename = path.basename(filename);
if (safeFilename !== filename) {
logger.warn(`ssinstall filename contains path traversal: ${filename}`);
return;
return false;
}
execParams.push("ssinstall", path.join(downloadDir, safeFilename));
} else {
@@ -446,6 +452,23 @@ ipcMain.on("queue-install", async (event, download_json) => {
tasks.set(id, task);
processNextDownload();
processNextInstall();
return true;
};
// Listen for download requests from renderer process
ipcMain.on("queue-install", async (event, download_json) => {
let download: unknown;
try {
download =
typeof download_json === "string"
? JSON.parse(download_json)
: download_json;
} catch (err) {
logger.error({ err }, "queue-install: invalid JSON payload, ignoring task");
return;
}
await addInstallTask(download as QueueInstallPayload, event.sender);
});
// Cancel Handler
@@ -1,4 +1,8 @@
import { BrowserWindow } from "electron";
import {
addInstallTask,
type QueueInstallPayload,
} from "../install-manager";
import {
IGNORE_CONFIG_PATH,
applyIgnoredEntries,
@@ -249,8 +253,9 @@ export const createUpdateCenterService = (
? `${item.downloadUrl}.metalink`
: undefined;
// 发送到主下载队列
const installTaskData = {
// 直接加入主下载队列(之前用 webContents.send("queue-install") 只会发给渲染端,
// 主进程 ipcMain 监听不到自己发出的 send,导致任务实际未启动而卡死)。
const installTaskData: QueueInstallPayload = {
id: updateTaskId,
pkgname: item.pkgname,
metalinkUrl,
@@ -260,8 +265,7 @@ export const createUpdateCenterService = (
retry: false,
};
// 通过 IPC 发送到主下载队列
webContents.send("queue-install", JSON.stringify(installTaskData));
await addInstallTask(installTaskData, webContents);
// 从更新中心的 items 中移除该应用(不再显示在更新列表中)
currentItems = currentItems.filter(