mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-09-20 21:50:11 +08:00
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:
@@ -236,3 +236,33 @@
|
||||
|
||||
### 8.4 验证边界
|
||||
本机 headless 无法真实触发 aria2c 下载与 GUI 日志面板;已通过 `vue-tsc` 类型检查与 `read_lints` 0 错误。真实失败场景(如 Metalink 404、网络中断)下的日志连贯性需用户在真机验证。
|
||||
|
||||
---
|
||||
|
||||
## 9. 附加修复:更新中心点击"更新"后卡"开始更新..."
|
||||
|
||||
### 9.1 现象(用户反馈)
|
||||
在软件更新中心选择 Visual Studio Code: 点击更新后,下载详情弹窗日志停在:
|
||||
```
|
||||
[09:23:41] 开始更新...
|
||||
```
|
||||
状态为 `queued`,后续无进展、无错误,界面卡死。
|
||||
|
||||
### 9.2 根因
|
||||
更新中心 `electron/main/backend/update-center/service.ts` 的 `start()` 方法,把任务通过 `webContents.send("queue-install", JSON.stringify(installTaskData))` 发送给主下载队列。
|
||||
|
||||
**但 `webContents.send` 是从主进程向渲染进程发消息,渲染进程的 `ipcRenderer.on("queue-install")` 能收到,而主进程的 `ipcMain.on("queue-install") 监听的是渲染进程 `ipcRenderer.send` 的消息,监听不到自己 `webContents.send` 的消息。**
|
||||
|
||||
结果:任务根本没有进入 `install-manager.ts` 的下载队列,`processNextDownload()` 永远不会执行,UI 自然卡在"开始更新...」。
|
||||
|
||||
### 9.3 修复(已提交)
|
||||
- **I1(抽离可复用的入队函数)**:在 `electron/main/backend/install-manager.ts` 新增导出 `addInstallTask(payload, sender)`,把原来 `ipcMain.on("queue-install")` 里的解析、校验、去重、APM 检查、命令构建、入队逻辑全部抽到该函数。`ipcMain.on` 本身只做 JSON 解析并调用 `addInstallTask`。
|
||||
- **I2(更新中心直接调用入队)**:`electron/main/backend/update-center/service.ts` 的 `start()` 不再 `webContents.send("queue-install")`,而是直接 `await addInstallTask(installTaskData, webContents)`,使任务真正进入主下载队列。
|
||||
- **I3(类型安全)**:新增 `QueueInstallPayload` 接口,避免 `any`。
|
||||
|
||||
改动文件:
|
||||
- `electron/main/backend/install-manager.ts`(I1、I3)
|
||||
- `electron/main/backend/update-center/service.ts`(I2)
|
||||
|
||||
### 9.4 验证边界
|
||||
本机 headless 无法启动 GUI 触发真实更新下载;已通过 `vue-tsc` 与 `read_lints` 0 错误。真机需在软件更新中心勾选一项更新并点击"更新",确认日志从"开始更新..."推进到"正在获取 Metalink 文件"、下载进度增加,最终进入安装或明确失败。
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user