mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-08-08 02:43:57 +08:00
refactor(install): 重构下载安装队列逻辑,拆分并发控制
1. 将下载与安装流程分离,实现并发下载限制与单安装实例控制 2. 新增任务阶段枚举,替换原有activeDownloads计数逻辑 3. 优化取消任务的处理流程,区分运行中和排队中的任务 4. 修复下载队列显示的统计错误,改为展示已完成任务数 5. 调整日志格式与代码注释,提升可读性
This commit is contained in:
@@ -43,6 +43,7 @@ type InstallTask = {
|
|||||||
filename?: string;
|
filename?: string;
|
||||||
origin: "spark" | "apm";
|
origin: "spark" | "apm";
|
||||||
cancelled?: boolean;
|
cancelled?: boolean;
|
||||||
|
phase: "queued-download" | "downloading" | "queued-install" | "installing";
|
||||||
};
|
};
|
||||||
|
|
||||||
const SHELL_CALLER_PATH = "/opt/spark-store/extras/shell-caller.sh";
|
const SHELL_CALLER_PATH = "/opt/spark-store/extras/shell-caller.sh";
|
||||||
@@ -62,8 +63,8 @@ const CREATE_DESKTOP_CONFIG_PATH = path.join(
|
|||||||
|
|
||||||
// APM应用的.desktop文件可能在以下几个位置
|
// APM应用的.desktop文件可能在以下几个位置
|
||||||
const APM_DESKTOP_ENTRY_DIRS = [
|
const APM_DESKTOP_ENTRY_DIRS = [
|
||||||
"/var/lib/apm", // 实体机/宿主系统
|
"/var/lib/apm", // 实体机/宿主系统
|
||||||
"/var/lib/apm/apm/files/ace-env/var/lib/apm", // ACE容器
|
"/var/lib/apm/apm/files/ace-env/var/lib/apm", // ACE容器
|
||||||
];
|
];
|
||||||
|
|
||||||
// Helper: 用XDG_DESKTOP_DIR拿桌面路径,读不到我就回退到~/Desktop
|
// Helper: 用XDG_DESKTOP_DIR拿桌面路径,读不到我就回退到~/Desktop
|
||||||
@@ -78,7 +79,9 @@ const resolveDesktopDir = async (): Promise<string> => {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// user-dirs.dirs无法读取
|
// user-dirs.dirs无法读取
|
||||||
logger.warn(`Failed to get XDG_DESKTOP_DIR, I'm falling back to ~/Desktop!!`);
|
logger.warn(
|
||||||
|
`Failed to get XDG_DESKTOP_DIR, I'm falling back to ~/Desktop!!`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return path.join(os.homedir(), "Desktop");
|
return path.join(os.homedir(), "Desktop");
|
||||||
};
|
};
|
||||||
@@ -147,9 +150,7 @@ const createApmDesktopShortcut = async (
|
|||||||
logger.info(`Wrote shortcut ${destPath} for ${pkgname}.`);
|
logger.info(`Wrote shortcut ${destPath} for ${pkgname}.`);
|
||||||
return;
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(
|
logger.warn(`Failed to create desktop shortcut for ${pkgname}: ${err}`);
|
||||||
`Failed to create desktop shortcut for ${pkgname}: ${err}`,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,7 +161,10 @@ const createApmDesktopShortcut = async (
|
|||||||
|
|
||||||
export const tasks = new Map<number, InstallTask>();
|
export const tasks = new Map<number, InstallTask>();
|
||||||
|
|
||||||
let idle = true; // Indicates if the installation manager is idle
|
// 下载与安装分离:最多 5 个并发下载,安装一次只允许一个
|
||||||
|
const MAX_CONCURRENT_DOWNLOADS = 5;
|
||||||
|
let activeDownloadCount = 0;
|
||||||
|
let installIdle = true;
|
||||||
|
|
||||||
export const checkSuperUserCommand = async (): Promise<string> => {
|
export const checkSuperUserCommand = async (): Promise<string> => {
|
||||||
if (process.getuid?.() === 0) return "";
|
if (process.getuid?.() === 0) return "";
|
||||||
@@ -372,72 +376,95 @@ ipcMain.on("queue-install", async (event, download_json) => {
|
|||||||
metalinkUrl,
|
metalinkUrl,
|
||||||
filename,
|
filename,
|
||||||
origin: origin || "apm",
|
origin: origin || "apm",
|
||||||
|
phase: metalinkUrl ? "queued-download" : "queued-install",
|
||||||
};
|
};
|
||||||
tasks.set(id, task);
|
tasks.set(id, task);
|
||||||
if (idle) processNextInQueue();
|
processNextDownload();
|
||||||
|
processNextInstall();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cancel Handler
|
// Cancel Handler
|
||||||
ipcMain.on("cancel-install", (event, id) => {
|
ipcMain.on("cancel-install", (event, id) => {
|
||||||
if (tasks.has(id)) {
|
const task = tasks.get(id);
|
||||||
const task = tasks.get(id);
|
if (!task) return;
|
||||||
if (task) {
|
|
||||||
task.cancelled = true;
|
|
||||||
task.download_process?.kill();
|
|
||||||
task.install_process?.kill();
|
|
||||||
logger.info(`已取消任务: ${id}`);
|
|
||||||
|
|
||||||
// 删除下载目录
|
task.cancelled = true;
|
||||||
if (task.downloadDir && fs.existsSync(task.downloadDir)) {
|
logger.info(`已取消任务: ${id}`);
|
||||||
try {
|
|
||||||
fs.rmSync(task.downloadDir, { recursive: true, force: true });
|
|
||||||
logger.info(`已删除下载目录: ${task.downloadDir}`);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`删除下载目录失败 ${task.downloadDir}: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主动发送完成(失败)事件,close 回调会因 cancelled 标志跳过
|
// 删除下载目录
|
||||||
task.webContents?.send("install-complete", {
|
if (task.downloadDir && fs.existsSync(task.downloadDir)) {
|
||||||
id,
|
try {
|
||||||
success: false,
|
fs.rmSync(task.downloadDir, { recursive: true, force: true });
|
||||||
time: Date.now(),
|
logger.info(`已删除下载目录: ${task.downloadDir}`);
|
||||||
exitCode: -1,
|
} catch (err) {
|
||||||
message: JSON.stringify({
|
logger.error(`删除下载目录失败 ${task.downloadDir}: ${err}`);
|
||||||
message: "用户取消",
|
|
||||||
stdout: "",
|
|
||||||
stderr: "",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
tasks.delete(id);
|
|
||||||
idle = true;
|
|
||||||
if (tasks.size > 0) processNextInQueue();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 主动发送完成(失败)事件
|
||||||
|
task.webContents?.send("install-complete", {
|
||||||
|
id,
|
||||||
|
success: false,
|
||||||
|
time: Date.now(),
|
||||||
|
exitCode: -1,
|
||||||
|
message: JSON.stringify({
|
||||||
|
message: "用户取消",
|
||||||
|
stdout: "",
|
||||||
|
stderr: "",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const isRunning = task.phase === "downloading" || task.phase === "installing";
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
// 运行中的任务:终止进程,由对应的阶段处理器在 finally 中清理计数器与队列
|
||||||
|
task.download_process?.kill();
|
||||||
|
task.install_process?.kill();
|
||||||
|
} else {
|
||||||
|
// 排队中的任务(未开始执行):直接清理并调度
|
||||||
|
tasks.delete(id);
|
||||||
|
processNextDownload();
|
||||||
|
processNextInstall();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function processNextInQueue() {
|
/**
|
||||||
if (!idle) return;
|
* 尝试启动排队中的下载任务,最多同时运行 MAX_CONCURRENT_DOWNLOADS 个。
|
||||||
|
*/
|
||||||
|
function processNextDownload() {
|
||||||
|
while (activeDownloadCount < MAX_CONCURRENT_DOWNLOADS) {
|
||||||
|
const task = Array.from(tasks.values()).find(
|
||||||
|
(t) => t.phase === "queued-download" && !t.cancelled,
|
||||||
|
);
|
||||||
|
if (!task) break;
|
||||||
|
task.phase = "downloading";
|
||||||
|
activeDownloadCount++;
|
||||||
|
void runDownloadPhase(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Always take the first task to ensure sequence
|
/**
|
||||||
const task = Array.from(tasks.values())[0];
|
* 尝试启动排队中的安装任务,安装一次只允许一个。
|
||||||
|
*/
|
||||||
|
function processNextInstall() {
|
||||||
|
if (!installIdle) return;
|
||||||
|
const task = Array.from(tasks.values()).find(
|
||||||
|
(t) => t.phase === "queued-install" && !t.cancelled,
|
||||||
|
);
|
||||||
if (!task) {
|
if (!task) {
|
||||||
idle = true;
|
installIdle = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
installIdle = false;
|
||||||
|
task.phase = "installing";
|
||||||
|
void runInstallPhase(task);
|
||||||
|
}
|
||||||
|
|
||||||
// 如果任务已被取消,跳过并处理下一个
|
/**
|
||||||
if (task.cancelled) {
|
* 下载阶段:获取 Metalink → aria2c 下载(含重试)。
|
||||||
tasks.delete(task.id);
|
* 下载完成后任务进入 queued-install 等待安装。
|
||||||
idle = true;
|
*/
|
||||||
if (tasks.size > 0) {
|
async function runDownloadPhase(task: InstallTask) {
|
||||||
processNextInQueue();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
idle = false;
|
|
||||||
const { webContents, id, downloadDir } = task;
|
const { webContents, id, downloadDir } = task;
|
||||||
|
|
||||||
const sendLog = (msg: string) => {
|
const sendLog = (msg: string) => {
|
||||||
@@ -452,6 +479,8 @@ async function processNextInQueue() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (task.cancelled) throw new Error("下载已取消");
|
||||||
|
|
||||||
// 1. Metalink & Aria2c Phase
|
// 1. Metalink & Aria2c Phase
|
||||||
if (task.metalinkUrl) {
|
if (task.metalinkUrl) {
|
||||||
try {
|
try {
|
||||||
@@ -617,10 +646,50 @@ async function processNextInQueue() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 进入安装阶段前检查是否已取消
|
// 下载完成,进入安装队列等待
|
||||||
if (task.cancelled) {
|
task.phase = "queued-install";
|
||||||
throw new Error("安装已取消");
|
} catch (error) {
|
||||||
|
logger.error(`Task ${id} download failed: ${error}`);
|
||||||
|
if (!task.cancelled) {
|
||||||
|
webContents?.send("install-complete", {
|
||||||
|
id,
|
||||||
|
success: false,
|
||||||
|
time: Date.now(),
|
||||||
|
exitCode: -1,
|
||||||
|
message: JSON.stringify({
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
stdout: "",
|
||||||
|
stderr: "",
|
||||||
|
}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
tasks.delete(id);
|
||||||
|
} finally {
|
||||||
|
activeDownloadCount--;
|
||||||
|
processNextDownload();
|
||||||
|
processNextInstall();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装阶段:执行安装命令,安装一次只允许一个。
|
||||||
|
*/
|
||||||
|
async function runInstallPhase(task: InstallTask) {
|
||||||
|
const { webContents, id } = task;
|
||||||
|
|
||||||
|
const sendLog = (msg: string) => {
|
||||||
|
webContents?.send("install-log", { id, time: Date.now(), message: msg });
|
||||||
|
};
|
||||||
|
const sendStatus = (status: string) => {
|
||||||
|
webContents?.send("install-status", {
|
||||||
|
id,
|
||||||
|
time: Date.now(),
|
||||||
|
message: status,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (task.cancelled) throw new Error("安装已取消");
|
||||||
|
|
||||||
// 2. Install Phase
|
// 2. Install Phase
|
||||||
sendStatus("installing");
|
sendStatus("installing");
|
||||||
@@ -706,9 +775,7 @@ async function processNextInQueue() {
|
|||||||
try {
|
try {
|
||||||
await createApmDesktopShortcut(task.pkgname, sendLog);
|
await createApmDesktopShortcut(task.pkgname, sendLog);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(
|
logger.warn(`Failed to create shortcut for ${task.pkgname}: ${err}`);
|
||||||
`Failed to create shortcut for ${task.pkgname}: ${err}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -723,27 +790,25 @@ async function processNextInQueue() {
|
|||||||
message: JSON.stringify(msgObj),
|
message: JSON.stringify(msgObj),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Task ${id} failed: ${error}`);
|
logger.error(`Task ${id} install failed: ${error}`);
|
||||||
webContents?.send("install-complete", {
|
|
||||||
id,
|
|
||||||
success: false,
|
|
||||||
time: Date.now(),
|
|
||||||
exitCode: -1,
|
|
||||||
message: JSON.stringify({
|
|
||||||
message: error instanceof Error ? error.message : String(error),
|
|
||||||
stdout: "",
|
|
||||||
stderr: "",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
// 如果已被 cancel handler 清理,跳过重复清理
|
|
||||||
if (!task.cancelled) {
|
if (!task.cancelled) {
|
||||||
tasks.delete(id);
|
webContents?.send("install-complete", {
|
||||||
idle = true;
|
id,
|
||||||
if (tasks.size > 0) {
|
success: false,
|
||||||
processNextInQueue();
|
time: Date.now(),
|
||||||
}
|
exitCode: -1,
|
||||||
|
message: JSON.stringify({
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
stdout: "",
|
||||||
|
stderr: "",
|
||||||
|
}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
tasks.delete(id);
|
||||||
|
installIdle = true;
|
||||||
|
processNextInstall();
|
||||||
|
processNextDownload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
v-if="downloads.length"
|
v-if="downloads.length"
|
||||||
class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500 dark:bg-slate-800/70 dark:text-slate-300"
|
class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500 dark:bg-slate-800/70 dark:text-slate-300"
|
||||||
>
|
>
|
||||||
({{ activeDownloads }}/{{ downloads.length }})
|
({{ completedDownloads }}/{{ downloads.length }})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -148,10 +148,8 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const isExpanded = ref(false);
|
const isExpanded = ref(false);
|
||||||
|
|
||||||
const activeDownloads = computed(() => {
|
const completedDownloads = computed(() => {
|
||||||
return props.downloads.filter(
|
return props.downloads.filter((d) => d.status === "completed").length;
|
||||||
(d) => d.status === "downloading" || d.status === "installing",
|
|
||||||
).length;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleExpand = () => {
|
const toggleExpand = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user