[Feat] Download PLS from API

This commit is contained in:
Minoricew
2025-06-09 18:02:11 +08:00
parent 8ab55bc93c
commit ed29f1b86f
13 changed files with 662 additions and 55 deletions

View File

@@ -74,6 +74,7 @@ const buildIpcMain = (electron) => {
};
const { applyConfigIpcHandler } = require("./ipcModules/configIpcHandler");
const { applyFsIpcHandler } = require("./ipcModules/fsIpcHandler");
const { applyPlsIpcHandler } = require("./ipcModules/plsIpcHandler");
ipcMain.handle("$aura.base.restartApplication", async () => {
@@ -82,6 +83,7 @@ const buildIpcMain = (electron) => {
});
applyConfigIpcHandler(ipcMain);
applyFsIpcHandler(ipcMain);
applyPlsIpcHandler(ipcMain);
};

View File

@@ -0,0 +1,186 @@
// @ts-check
const __SCOPE = "main";
const { exec } = require("child_process");
const nodeHttp = require("http");
const nodeHttps = require("https");
const fs = require("fs");
const path = require("path");
const { genRandomHex } = require("../../../utils/crypto");
const composableFunctions = {
/**
*
* @param {string} url
* @param {string} targetPath
* @param {((arg: DownloadTask) => any)} progressCallback
*/
downloadFile: async (url, targetPath, progressCallback) => {
if (!progressCallback) return false;
const taskId = genRandomHex();
/**
* @type {DownloadTask}
*/
const failedTemplate = {
id: taskId,
progress: 100,
status: "failed",
dlUrl: url,
savePath: targetPath,
message: "",
};
if (!url || !targetPath) {
failedTemplate.message = "Invalid arg";
progressCallback(failedTemplate);
return false;
}
if (!fs.existsSync(path.dirname(targetPath))) {
failedTemplate.message = "Path not exists";
progressCallback(failedTemplate);
}
const httpModuleIns = url.startsWith("https") ? nodeHttps : nodeHttp;
global.__HUGO_AURA__.fsTasks?.downloadTasks.set(taskId, {
status: "waiting",
cancelReq: null,
});
const fsStream = fs.createWriteStream(targetPath);
const dlReq = httpModuleIns.get(url, (response) => {
if (response.statusCode !== 200) {
fsStream.close();
failedTemplate.message = `Request error: HTTP ${response.statusCode}`;
progressCallback(failedTemplate);
return false;
}
const contentLength = response.headers["content-length"];
// @ts-expect-error
const totalBytes = parseInt(contentLength, 10) || 0; // No error handling 😆
let curRecvBytes = 0;
global.__HUGO_AURA__.fsTasks?.downloadTasks.set(taskId, {
status: "progressing",
cancelReq: () => {
dlReq.destroy();
fsStream.close();
fs.unlink(targetPath, () => {});
global.__HUGO_AURA__.fsTasks?.downloadTasks.delete(taskId);
progressCallback({
id: taskId,
progress: 100,
curBytes: curRecvBytes,
totalBytes: totalBytes,
status: "cancelled",
dlUrl: url,
savePath: targetPath,
});
},
});
response.on("data", (chunk) => {
curRecvBytes += chunk.length;
const curProgress =
totalBytes > 0 ? (curRecvBytes / totalBytes) * 100 : 0;
progressCallback({
id: taskId,
progress: curProgress.toFixed(2),
curBytes: curRecvBytes,
totalBytes: totalBytes,
status: "progressing",
dlUrl: url,
savePath: targetPath,
});
});
response.pipe(fsStream);
fsStream.on("finish", () => {
fsStream.close();
progressCallback({
id: taskId,
progress: (100).toFixed(2),
curBytes: curRecvBytes,
totalBytes: totalBytes,
status: "done",
dlUrl: url,
savePath: targetPath,
});
});
global.__HUGO_AURA__.fsTasks?.downloadTasks.delete(taskId);
return true;
});
dlReq.on("error", (e) => {
fsStream.close();
fs.unlink(targetPath, () => {});
failedTemplate.message =
"Request error: Unexpected error while downloading file";
failedTemplate.errorObj = e;
progressCallback(failedTemplate);
global.__HUGO_AURA__.fsTasks?.downloadTasks.delete(taskId);
return false;
});
},
};
/**
*
* @param {import("electron").IpcMain} ipcMain
*/
const applyFsIpcHandler = (ipcMain) => {
const methodBase = "$aura.fs";
global.__HUGO_AURA__.fsTasks = {
downloadTasks: new Map(),
};
ipcMain.handle(
`${methodBase}.dl.cancelDownloadTask`,
/**
*
* @param {import("electron").IpcMainInvokeEvent} _evt
* @param {{ targetTaskId: string }} arg
* @returns {{ success: boolean, error: string | null }}
*/
(_evt, arg) => {
if (!arg.targetTaskId) {
return {
success: false,
error: "ARG_INVALID",
};
}
if (!global.__HUGO_AURA__.fsTasks?.downloadTasks.has(arg.targetTaskId)) {
return {
success: false,
error: "TASK_ID_NOT_FOUND",
};
}
const taskObj = global.__HUGO_AURA__.fsTasks.downloadTasks.get(
arg.targetTaskId
);
if (!taskObj?.cancelReq) {
return {
success: false,
error: "TASK_NOT_STARTED",
};
}
taskObj.cancelReq();
return {
success: true,
error: null,
};
}
);
};
module.exports = { fsComposables: composableFunctions, applyFsIpcHandler };

View File

@@ -5,6 +5,8 @@ const __SCOPE = "main";
const { exec } = require("child_process");
const fs = require("fs");
const path = require("path");
const nodeHttps = require("https");
const { fsComposables } = require("./fsIpcHandler");
const functions = {
querySvcDetail: (
@@ -47,8 +49,9 @@ const functions = {
* @returns {Promise<{ success: boolean, errorObj?: Error }>}
*/
execCommand: (logHeader, binPath, command) => {
const processedPath = binPath.includes(" ") ? `"${binPath}"` : binPath;
return new Promise((resolve) => {
exec(`"${binPath}" ${command}`, (error, stdout, stderr) => {
exec(`${processedPath} ${command}`, (error, stdout, stderr) => {
if (error) {
console.error(`${logHeader} Failed to execute command:`, error);
resolve({ success: false, errorObj: error });
@@ -58,6 +61,103 @@ const functions = {
});
});
},
/**
*
* @param {"stable" | "alpha"} channel
* @param {(arg: DownloadTask) => any} callbackFn
* @param {string} binPath
*/
handlePLSDownload: async (channel, callbackFn, binPath) => {
// TODO: Channel selection
const apiInfo = global.__HUGO_AURA_API__;
let plsVersionInfo = {};
const getVerPromise = new Promise((resolve) => {
// ↓ 目前 channel param 没有什么用处
nodeHttps
.get(
`${apiInfo.baseUrl}${apiInfo.plsUpdate}?channel=${channel}`,
(rep) => {
let dataChunk = "";
rep.on("data", (chunk) => {
dataChunk += chunk;
});
rep.on("end", () => {
resolve({
success: true,
data: dataChunk,
});
});
}
)
.on("error", (e) => {
resolve({
success: false,
data: null,
errorObj: e,
});
});
});
const rawResInfo = await getVerPromise;
if (!rawResInfo.success) {
callbackFn({
id: "",
progress: 0,
status: "failed",
dlUrl: null,
savePath: null,
message: "未能获取 PLS 版本信息",
errorObj: rawResInfo.errorObj ? rawResInfo.errorObj : null,
});
return false;
}
try {
plsVersionInfo = JSON.parse(rawResInfo.data);
} catch (e) {
callbackFn({
id: "",
progress: 0,
status: "failed",
dlUrl: null,
savePath: null,
message: "PLS 版本信息解析失败",
errorObj: e,
});
console.error(
"[HugoAura / IPC / PLS] Error querying PLS version info:",
e
);
return false;
}
let deviceArch = process.env.PROCESSOR_ARCHITEW6432
? process.env.PROCESSOR_ARCHITEW6432
: process.env.PROCESSOR_ARCHITECTURE;
// @ts-expect-error
deviceArch = deviceArch.toLowerCase();
if (!Object.keys(plsVersionInfo.data.downloadUrl).includes(deviceArch)) {
callbackFn({
id: "",
progress: 0,
status: "failed",
dlUrl: null,
savePath: null,
message: `处理器架构识别失败, 检测到的架构: ${deviceArch}`,
});
return false;
}
fsComposables.downloadFile(
plsVersionInfo.data.downloadUrl[deviceArch],
binPath,
callbackFn
);
},
};
/**
@@ -67,8 +167,8 @@ const functions = {
const applyPlsIpcHandler = (ipcMain) => {
const methodBase = "$aura.pls";
const PLS_INSTALL_DIR = path.join("C:\\Program Files", "HugoAura PLS");
const PLS_BIN_PATH = path.join(PLS_INSTALL_DIR, "bin", "HugoAura-PLS.exe");
const PLS_INSTALL_DIR = path.join("C:\\Program Files", "HugoAura PLS", "bin");
const PLS_BIN_PATH = path.join(PLS_INSTALL_DIR, "HugoAura-PLS.exe");
const PLS_SVC_NAME = "HugoAuraPLS";
const isPlsDetached = process.argv.includes("--pls-detach");
@@ -106,6 +206,48 @@ const applyPlsIpcHandler = (ipcMain) => {
}
);
ipcMain.handle(
`${methodBase}.ensurePlsInstallDir`,
/**
*
* @param {import("electron").IpcMainInvokeEvent} _event
* @param {any} _arg
* @returns {{ success: boolean; data: { alreadyExists: boolean; createdDir: string; }; error?: Error }}
*/
(_event, _arg) => {
const alreadyExists = fs.existsSync(PLS_INSTALL_DIR);
if (alreadyExists) {
return {
success: true,
data: {
alreadyExists: true,
createdDir: PLS_INSTALL_DIR,
},
};
}
try {
fs.mkdirSync(PLS_INSTALL_DIR);
return {
success: true,
data: {
alreadyExists: false,
createdDir: PLS_INSTALL_DIR,
},
};
} catch (error) {
return {
success: false,
data: {
alreadyExists: false,
createdDir: PLS_INSTALL_DIR,
},
error: error,
};
}
}
);
ipcMain.handle(
`${methodBase}.getPlsStats`,
/**
@@ -294,7 +436,7 @@ const applyPlsIpcHandler = (ipcMain) => {
return await functions.execCommand(
logHeader,
PLS_BIN_PATH,
"install"
"--startup auto install"
);
case "rmSvc":
return await functions.execCommand(logHeader, PLS_BIN_PATH, "remove");
@@ -302,12 +444,59 @@ const applyPlsIpcHandler = (ipcMain) => {
return await functions.execCommand(logHeader, PLS_BIN_PATH, "start");
case "stopSvc":
return await functions.execCommand(logHeader, PLS_BIN_PATH, "stop");
case "rmBin":
const unlinkPromise = new Promise((resolve) => {
fs.unlink(PLS_BIN_PATH, (error) => {
if (error) {
resolve({
success: false,
errorObj: error,
});
return false;
}
resolve({
success: true,
errorObj: null,
});
return true;
});
});
const unlinkRet = await unlinkPromise;
return unlinkRet;
default:
return { success: false, errorObj: new Error("Method not found") };
}
}
);
ipcMain.handle(
`${methodBase}.downloadPls`,
/**
*
* @param {import("electron").IpcMainInvokeEvent} _evt
* @param {{channel?: "stable" | "alpha", reportTo?: import("../../../types/main/core").WindowName}} arg
* @returns {void}
*/
(_evt, arg) => {
const channel = arg.channel ? arg.channel : "stable";
const reportWin = arg.reportTo ? arg.reportTo : "assistant";
functions.handlePLSDownload(
channel,
(status) => {
ipcMain.send(
reportWin,
`${methodBase}.post.reportPlsDownloadStatus`,
status
);
},
PLS_BIN_PATH
);
}
);
ipcMain.handle(
`${methodBase}.retryPlsConnect`,
/**