mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 01:10:16 +08:00
fix(下载): 增强aria2c下载的稳定性和重试机制
添加连接超时、下载超时和重试参数配置 实现0%卡死检测和自动重试逻辑 移除冗余的dpkg-query检测代码 优化安装按钮状态判断逻辑 更新版本号至4.9.9alpha4
This commit is contained in:
@@ -383,38 +383,92 @@ async function processNextInQueue() {
|
|||||||
`--dir=${downloadDir}`,
|
`--dir=${downloadDir}`,
|
||||||
"--allow-overwrite=true",
|
"--allow-overwrite=true",
|
||||||
"--summary-interval=1",
|
"--summary-interval=1",
|
||||||
|
"--connect-timeout=15",
|
||||||
|
"--timeout=15",
|
||||||
|
"--max-tries=3",
|
||||||
|
"--retry-wait=5",
|
||||||
|
"--max-concurrent-downloads=4",
|
||||||
|
"--min-split-size=1M",
|
||||||
|
"--lowest-speed-limit=1K",
|
||||||
"-M",
|
"-M",
|
||||||
metalinkPath,
|
metalinkPath,
|
||||||
];
|
];
|
||||||
|
|
||||||
sendStatus("downloading");
|
sendStatus("downloading");
|
||||||
sendLog(`启动下载: aria2c ${aria2Args.join(" ")}`);
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
// 下载重试逻辑:卡在0% 30秒则重启,最多3次
|
||||||
const child = spawn("aria2c", aria2Args);
|
const maxRetries = 3;
|
||||||
task.download_process = child;
|
let retryCount = 0;
|
||||||
|
let downloadSuccess = false;
|
||||||
|
|
||||||
child.stdout.on("data", (data) => {
|
while (retryCount < maxRetries && !downloadSuccess) {
|
||||||
const str = data.toString();
|
if (retryCount > 0) {
|
||||||
// Match ( 12%) or (12%)
|
sendLog(`第 ${retryCount} 次重试下载...`);
|
||||||
const match = str.match(/[0-9]+(\.[0-9]+)?%/g);
|
webContents?.send("install-progress", { id, progress: 0 });
|
||||||
if (match) {
|
}
|
||||||
const p = parseFloat(match.at(-1)) / 100;
|
|
||||||
webContents?.send("install-progress", { id, progress: p });
|
try {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
sendLog(`启动下载: aria2c ${aria2Args.join(" ")}`);
|
||||||
|
const child = spawn("aria2c", aria2Args);
|
||||||
|
task.download_process = child;
|
||||||
|
|
||||||
|
let lastProgressTime = Date.now();
|
||||||
|
let lastProgress = 0;
|
||||||
|
const zeroProgressTimeout = 30000; // 0%卡死30秒超时
|
||||||
|
const progressCheckInterval = 3000; // 每3秒检查一次
|
||||||
|
|
||||||
|
// 设置超时检测定时器
|
||||||
|
const timeoutChecker = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
// 只在进度为0时检查超时
|
||||||
|
if (lastProgress === 0 && now - lastProgressTime > zeroProgressTimeout) {
|
||||||
|
clearInterval(timeoutChecker);
|
||||||
|
child.kill();
|
||||||
|
reject(new Error(`下载卡在0%超过 ${zeroProgressTimeout / 1000} 秒`));
|
||||||
|
}
|
||||||
|
}, progressCheckInterval);
|
||||||
|
|
||||||
|
child.stdout.on("data", (data) => {
|
||||||
|
const str = data.toString();
|
||||||
|
// Match ( 12%) or (12%)
|
||||||
|
const match = str.match(/[0-9]+(\.[0-9]+)?%/g);
|
||||||
|
if (match) {
|
||||||
|
const p = parseFloat(match.at(-1)) / 100;
|
||||||
|
if (p > lastProgress) {
|
||||||
|
lastProgress = p;
|
||||||
|
lastProgressTime = Date.now();
|
||||||
|
}
|
||||||
|
webContents?.send("install-progress", { id, progress: p });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (d) => sendLog(`aria2c: ${d}`));
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
clearInterval(timeoutChecker);
|
||||||
|
if (code === 0) {
|
||||||
|
webContents?.send("install-progress", { id, progress: 1 });
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Aria2c exited with code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.on("error", (err) => {
|
||||||
|
clearInterval(timeoutChecker);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
downloadSuccess = true;
|
||||||
|
} catch (err) {
|
||||||
|
retryCount++;
|
||||||
|
if (retryCount >= maxRetries) {
|
||||||
|
throw new Error(`下载失败,已重试 ${maxRetries} 次: ${err}`);
|
||||||
}
|
}
|
||||||
});
|
sendLog(`下载失败,准备重试 (${retryCount}/${maxRetries})`);
|
||||||
child.stderr.on("data", (d) => sendLog(`aria2c: ${d}`));
|
// 等待2秒后重试
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
child.on("close", (code) => {
|
}
|
||||||
if (code === 0) {
|
}
|
||||||
webContents?.send("install-progress", { id, progress: 1 });
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(new Error(`Aria2c exited with code ${code}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
child.on("error", reject);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Install Phase
|
// 2. Install Phase
|
||||||
@@ -571,20 +625,6 @@ ipcMain.handle("check-installed", async (_event, payload: any) => {
|
|||||||
if (isInstalled) return true;
|
if (isInstalled) return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果脚本不存在或检测不到,使用 dpkg-query 作为后备
|
|
||||||
logger.info(`尝试使用 dpkg-query 检测: ${pkgname}`);
|
|
||||||
const { code, stdout } = await runCommandCapture("dpkg-query", [
|
|
||||||
"-W",
|
|
||||||
"-f=${Status}",
|
|
||||||
pkgname,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (code === 0 && stdout.includes("install ok installed")) {
|
|
||||||
isInstalled = true;
|
|
||||||
logger.info(`应用已安装 (dpkg-query 检测): ${pkgname}`);
|
|
||||||
} else {
|
|
||||||
logger.info(`应用未安装 (dpkg-query 检测): ${pkgname}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isInstalled;
|
return isInstalled;
|
||||||
});
|
});
|
||||||
@@ -610,7 +650,7 @@ ipcMain.on("remove-installed", async (_event, payload) => {
|
|||||||
if (origin === "spark") {
|
if (origin === "spark") {
|
||||||
execParams.push("aptss", "remove", pkgname);
|
execParams.push("aptss", "remove", pkgname);
|
||||||
} else {
|
} else {
|
||||||
execParams.push("apm", "remove", "-y", pkgname);
|
execParams.push("apm", "autoremove", "-y", pkgname);
|
||||||
}
|
}
|
||||||
|
|
||||||
const child = spawn(execCommand, execParams, {
|
const child = spawn(execCommand, execParams, {
|
||||||
@@ -724,7 +764,7 @@ ipcMain.handle("launch-app", async (_event, payload: any) => {
|
|||||||
let execParams = ["start", pkgname];
|
let execParams = ["start", pkgname];
|
||||||
|
|
||||||
if (origin === "apm") {
|
if (origin === "apm") {
|
||||||
execCommand = "/opt/spark-store/extras/apm-launcher";
|
execCommand = "apm";
|
||||||
execParams = ["launch", pkgname];
|
execParams = ["launch", pkgname];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "spark-store",
|
"name": "spark-store",
|
||||||
"version": "4.9.9alpha3",
|
"version": "4.9.9alpha4",
|
||||||
"main": "dist-electron/main/index.js",
|
"main": "dist-electron/main/index.js",
|
||||||
"description": "Client for Spark App Store",
|
"description": "Client for Spark App Store",
|
||||||
"author": "elysia-best <elysia-best@simplelinux.cn.eu.org>",
|
"author": "elysia-best <elysia-best@simplelinux.cn.eu.org>",
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
"
|
"
|
||||||
@click="handleInstall"
|
@click="handleInstall"
|
||||||
:disabled="
|
:disabled="
|
||||||
installFeedback || isCompleted || isOtherVersionInstalled
|
installFeedback || isOtherVersionInstalled
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
@@ -260,7 +260,6 @@
|
|||||||
import { computed, useAttrs, ref, watch } from "vue";
|
import { computed, useAttrs, ref, watch } from "vue";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {
|
import {
|
||||||
useDownloadItemStatus,
|
|
||||||
useInstallFeedback,
|
useInstallFeedback,
|
||||||
downloads,
|
downloads,
|
||||||
} from "../global/downloadStatus";
|
} from "../global/downloadStatus";
|
||||||
@@ -290,7 +289,7 @@ const appPkgname = computed(() => props.app?.pkgname);
|
|||||||
|
|
||||||
const isIconLoaded = ref(false);
|
const isIconLoaded = ref(false);
|
||||||
|
|
||||||
const viewingOrigin = ref<"spark" | "apm">("spark");
|
const viewingOrigin = ref<"spark" | "apm">("apm");
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.app,
|
() => props.app,
|
||||||
@@ -298,9 +297,9 @@ watch(
|
|||||||
isIconLoaded.value = false;
|
isIconLoaded.value = false;
|
||||||
if (newApp) {
|
if (newApp) {
|
||||||
if (newApp.isMerged) {
|
if (newApp.isMerged) {
|
||||||
// 若父组件已根据安装状态设置了优先展示的版本,则使用;否则默认 Spark
|
// 若父组件已根据安装状态设置了优先展示的版本,则使用;否则默认 APM
|
||||||
viewingOrigin.value =
|
viewingOrigin.value =
|
||||||
newApp.viewingOrigin ?? (newApp.sparkApp ? "spark" : "apm");
|
newApp.viewingOrigin ?? (newApp.apmApp ? "apm" : "spark");
|
||||||
} else {
|
} else {
|
||||||
viewingOrigin.value = newApp.origin;
|
viewingOrigin.value = newApp.origin;
|
||||||
}
|
}
|
||||||
@@ -344,16 +343,12 @@ const isOtherVersionInstalled = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { installFeedback } = useInstallFeedback(appPkgname);
|
const { installFeedback } = useInstallFeedback(appPkgname);
|
||||||
const { isCompleted } = useDownloadItemStatus(appPkgname);
|
|
||||||
const installBtnText = computed(() => {
|
const installBtnText = computed(() => {
|
||||||
if (isinstalled.value) {
|
if (isinstalled.value) {
|
||||||
return "已安装";
|
return "已安装";
|
||||||
}
|
}
|
||||||
if (isCompleted.value) {
|
|
||||||
return "已安装";
|
|
||||||
}
|
|
||||||
if (isOtherVersionInstalled.value) {
|
if (isOtherVersionInstalled.value) {
|
||||||
return viewingOrigin.value === "spark" ? "已安装apm版" : "已安装spark版";
|
return viewingOrigin.value === "spark" ? "已安装 APM 版" : "已安装 Spark 版";
|
||||||
}
|
}
|
||||||
if (installFeedback.value) {
|
if (installFeedback.value) {
|
||||||
const status = activeDownload.value?.status;
|
const status = activeDownload.value?.status;
|
||||||
|
|||||||
Reference in New Issue
Block a user