feat(update-center): 支持被系统锁定(hold)包的强制安装与更新中心体验优化

- 标记 apt-mark hold 的 spark(aptss) 源包,默认禁用勾选并提示开启强制安装
- 更新中心发起更新时透传 forceHeld,后端 Spark 本地 deb 走 shell-caller
  force-ssinstall 分支(单次免密 pkexec:unhold→ssinstall→hold),避免二次提权
- 无可选项时禁用全选并提示"无可更新项";更新选中后从列表中移除已启动项
- DownloadDetail 紧凑布局避免整页滚动;Toolbar 全选与更新选中按钮同行
- 继承 PKGNAME_PATTERN 与 metalinkUrl 白名单(https+*.spark-app.store+拒..)纵深防御
- apm 源无 hold 概念,forceHeld 在 apm 分支忽略(正确行为)
This commit is contained in:
xiyidaiwa
2026-08-13 16:42:21 +08:00
parent b5c5ab04b5
commit 0f62a7e36a
17 changed files with 893 additions and 105 deletions
+41 -1
View File
@@ -502,8 +502,12 @@ export const loadUpdateCenterItems = async (
),
);
// 标记被系统锁定(apt-mark hold)的包:这类包被 apt 拒绝升级(除非 --allow-change-held-packages)。
// 更新中心据此默认禁用其勾选,并提示用户单独开启「强制安装」。
const heldItems = await markHeldPackages(mergedItems, runCommand);
return {
items: mergedItems,
items: heldItems,
warnings: [
...warnings,
...enrichedAptssItems.warnings,
@@ -512,6 +516,42 @@ export const loadUpdateCenterItems = async (
};
};
// 通过 `apt-mark showhold` 获取系统锁定的包集合,给可升级项中匹配者打 held 标记。
// 仅 sparkaptss/deb)源受 apt hold 影响;apm 源不经过 apt,无需标记。
const markHeldPackages = async (
items: UpdateCenterItem[],
runCommand: UpdateCenterCommandRunner,
): Promise<UpdateCenterItem[]> => {
const sparkItems = items.filter((item) => item.source === "aptss");
if (sparkItems.length === 0) {
return items;
}
const result = await runCommand("apt-mark", ["showhold"]);
if (result.code !== 0) {
// 查询失败不阻断更新列表,仅跳过 hold 标记
console.warn(`[UpdateCenter] apt-mark showhold failed: ${result.stderr}`);
return items;
}
const heldSet = new Set(
result.stdout
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0),
);
if (heldSet.size === 0) {
return items;
}
return items.map((item) =>
item.source === "aptss" && heldSet.has(item.pkgname)
? { ...item, held: true }
: item,
);
};
// 子进程超时(毫秒):网络慢/镜像源卡死时,避免命令永久挂起
const SYSTEM_UPDATE_COMMAND_TIMEOUT_MS = 90_000;
+52 -15
View File
@@ -71,6 +71,8 @@ export interface UpdateCenterIgnorePayload {
export interface UpdateCenterStartTask {
taskKey: string;
id: number;
// 强制安装被系统锁定(apt-mark hold)的包
forceHeld?: boolean;
}
export interface UpdateCenterService {
@@ -119,6 +121,7 @@ const toState = (
migrationSource: item.migrationSource,
migrationTarget: item.migrationTarget,
aptssVersion: item.aptssVersion,
held: item.held,
})),
tasks: [], // 不再展示任务日志
warnings: [...snapshot.warnings],
@@ -221,15 +224,10 @@ export const createUpdateCenterService = (
},
async start(tasks) {
const snapshot = queue.getSnapshot();
const taskIdByKey = new Map(tasks.map((task) => [task.taskKey, task.id]));
const selectedItems = snapshot.items.filter(
(item) => taskIdByKey.has(getTaskKey(item)) && !item.ignored,
const taskByKey = new Map(
tasks.map((task) => [task.taskKey, task] as const),
);
if (selectedItems.length === 0) {
return;
}
// 获取主窗口的 webContents
const mainWindow = BrowserWindow.getAllWindows()[0];
const webContents = mainWindow?.webContents;
@@ -239,14 +237,54 @@ export const createUpdateCenterService = (
return;
}
// 获取当前 items
let currentItems = snapshot.items;
// 分类:可启动项 vs 被锁定(held)且未开启强制安装的项。
// 被锁定的项需用户单独开启「强制安装」才能升级,否则明确告知失败,避免静默卡在「开始更新」。
const startableItems: typeof snapshot.items = [];
const heldBlocked: Array<{
item: (typeof snapshot.items)[number];
id: number;
}> = [];
for (const item of selectedItems) {
const updateTaskId = taskIdByKey.get(getTaskKey(item));
if (!updateTaskId) {
for (const item of snapshot.items) {
const updateTask = taskByKey.get(getTaskKey(item));
if (!updateTask || item.ignored) continue;
if (item.held === true && !updateTask.forceHeld) {
heldBlocked.push({ item, id: updateTask.id });
continue;
}
startableItems.push(item);
}
// 对「被锁定未强制」的选中项,向前端发送明确失败通知(而非静默跳过)
for (const blocked of heldBlocked) {
webContents.send("install-complete", {
id: blocked.id,
success: false,
time: Date.now(),
exitCode: -1,
message: JSON.stringify({
message: `软件包 ${blocked.item.pkgname} 被系统锁定(hold),已在更新中心默认跳过。如需升级,请在该软件行开启「强制安装」开关后重试。`,
stdout: "",
stderr: "",
}),
});
}
if (startableItems.length === 0) {
return;
}
// 获取当前 items 的副本,启动成功后从更新中心列表移除已交出的项,
// 避免同一包同时出现在更新中心与下载队列(更新中心只展示待更新的项)。
let currentItems = snapshot.items;
for (const item of startableItems) {
const updateTask = taskByKey.get(getTaskKey(item));
if (!updateTask) {
continue;
}
const { id: updateTaskId, forceHeld } = updateTask;
// 构建 metalink URL
const metalinkUrl = item.downloadUrl
@@ -263,19 +301,18 @@ export const createUpdateCenterService = (
upgradeOnly: true,
origin: item.source === "apm" ? "apm" : "spark",
retry: false,
forceHeld: item.held === true ? forceHeld : false,
};
await addInstallTask(installTaskData, webContents);
// 从更新中心的 items 中移除该应用(不再显示在更新列表中)
// 启动成功后从更新中心列表移除该项(被 hold 未强制拦截的项不会进入此处,仍保留)。
currentItems = currentItems.filter(
(i) => getTaskKey(i) !== getTaskKey(item),
);
}
// 更新队列中的 items
queue.setItems(currentItems);
emit();
},
async cancel(taskKey) {
@@ -26,4 +26,6 @@ export interface UpdateCenterItem {
aptssVersion?: string;
// 更新发布时间(毫秒时间戳);由上游解析填充,暂无则缺省
updateTime?: number;
// 是否被系统锁定(apt-mark hold)。被锁定的包默认不可批量选中,需用户单独开启「强制安装」
held?: boolean;
}