mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 01:10:16 +08:00
feat(install): 实现安装管理器,支持安装、检查已安装状态和初步卸载功能
This commit is contained in:
203
electron/main/backend/install-manager.ts
Normal file
203
electron/main/backend/install-manager.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { ipcMain, WebContents } from 'electron';
|
||||
import { spawn, ChildProcess, exec } from 'node:child_process';
|
||||
import readline from 'node:readline';
|
||||
import { promisify } from 'node:util';
|
||||
import pino from 'pino';
|
||||
|
||||
const logger = pino({ 'name': 'download-manager' });
|
||||
|
||||
type InstallTask = {
|
||||
id: number;
|
||||
execCommand: string;
|
||||
execParams: string[];
|
||||
process: ChildProcess | null;
|
||||
webContents: WebContents | null;
|
||||
};
|
||||
|
||||
const tasks = new Map<number, InstallTask>();
|
||||
|
||||
let idle = true; // Indicates if the installation manager is idle
|
||||
|
||||
// Listen for download requests from renderer process
|
||||
ipcMain.on('queue-install', async (event, download_json) => {
|
||||
const download = JSON.parse(download_json);
|
||||
const { id, pkgname } = download || {};
|
||||
|
||||
if (!id || !pkgname) {
|
||||
logger.warn('passed arguments missing id or pkgname');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`收到下载任务: ${id}, 软件包名称: ${pkgname}`);
|
||||
|
||||
// 避免重复添加同一任务,但允许重试下载
|
||||
if (tasks.has(id) && !download.retry) {
|
||||
tasks.get(id)?.webContents.send('install-log', {
|
||||
id,
|
||||
time: Date.now(),
|
||||
message: `任务id: ${id} 已在列表中,忽略重复添加`
|
||||
});
|
||||
tasks.get(id)?.webContents.send('install-complete', {
|
||||
id: id,
|
||||
success: false,
|
||||
time: Date.now(),
|
||||
exitCode: -1,
|
||||
message: `{"message":"任务id: ${id} 已在列表中,忽略重复添加","stdout":"","stderr":""}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const webContents = event.sender;
|
||||
|
||||
// 开始组装安装命令
|
||||
const execAsync = promisify(exec);
|
||||
let superUserCmd = '';
|
||||
if (process.getuid && process.getuid() !== 0) {
|
||||
const { stdout, stderr } = await execAsync('which pkexec');
|
||||
if (stderr) {
|
||||
logger.error('没有找到 pkexec 命令');
|
||||
return;
|
||||
}
|
||||
logger.info(`找到提升权限命令: ${stdout.trim()}`);
|
||||
superUserCmd = stdout.trim();
|
||||
|
||||
if (superUserCmd.length === 0) {
|
||||
logger.error('没有找到提升权限的命令 pkexec, 无法继续安装');
|
||||
webContents.send('install-error', {
|
||||
id,
|
||||
time: Date.now(),
|
||||
message: '无法找到提升权限的命令 pkexec,请手动安装'
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let execCommand = '';
|
||||
let execParams = [];
|
||||
if (superUserCmd.length > 0) {
|
||||
execCommand = superUserCmd;
|
||||
execParams.push('/usr/bin/apm');
|
||||
} else {
|
||||
execCommand = '/usr/bin/apm';
|
||||
}
|
||||
execParams.push('install', '-y', pkgname);
|
||||
|
||||
const task: InstallTask = {
|
||||
id,
|
||||
execCommand,
|
||||
execParams,
|
||||
process: null,
|
||||
webContents
|
||||
};
|
||||
tasks.set(id, task);
|
||||
if (idle) processNextInQueue(0);
|
||||
});
|
||||
|
||||
function processNextInQueue(index: number) {
|
||||
if (!idle) return;
|
||||
|
||||
idle = false;
|
||||
const task = Array.from(tasks.values())[index];
|
||||
const webContents = task.webContents;
|
||||
let stdoutData = '';
|
||||
let stderrData = '';
|
||||
|
||||
webContents.send('install-status', {
|
||||
id: task.id,
|
||||
time: Date.now(),
|
||||
message: 'installing'
|
||||
})
|
||||
webContents.send('install-log', {
|
||||
id: task.id,
|
||||
time: Date.now(),
|
||||
message: `开始执行: ${task.execCommand} ${task.execParams.join(' ')}`
|
||||
});
|
||||
logger.info(`启动安装命令: ${task.execCommand} ${task.execParams.join(' ')}`);
|
||||
|
||||
const child = spawn(task.execCommand, task.execParams, {
|
||||
shell: true,
|
||||
env: process.env
|
||||
});
|
||||
task.process = child;
|
||||
|
||||
// 监听 stdout
|
||||
child.stdout.on('data', (data) => {
|
||||
stdoutData += data.toString();
|
||||
webContents.send('install-log', {
|
||||
id: task.id,
|
||||
time: Date.now(),
|
||||
message: data.toString()
|
||||
});
|
||||
});
|
||||
|
||||
// 监听 stderr
|
||||
child.stderr.on('data', (data) => {
|
||||
stderrData += data.toString();
|
||||
webContents.send('install-log', {
|
||||
id: task.id,
|
||||
time: Date.now(),
|
||||
message: data.toString()
|
||||
});
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
const success = code === 0;
|
||||
// 拼接json消息
|
||||
const messageJSONObj = {
|
||||
message: success ? '安装完成' : `安装失败,退出码 ${code}`,
|
||||
stdout: stdoutData,
|
||||
stderr: stderrData
|
||||
};
|
||||
|
||||
if (success) {
|
||||
logger.info(messageJSONObj);
|
||||
} else {
|
||||
logger.error(messageJSONObj);
|
||||
}
|
||||
|
||||
webContents.send('install-complete', {
|
||||
id: task.id,
|
||||
success: success,
|
||||
time: Date.now(),
|
||||
exitCode: code,
|
||||
message: JSON.stringify(messageJSONObj)
|
||||
});
|
||||
tasks.delete(task.id);
|
||||
idle = true;
|
||||
if (tasks.size > 0)
|
||||
processNextInQueue(0);
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.handle('check-installed', async (_event, pkgname: string) => {
|
||||
if (!pkgname) {
|
||||
logger.warn('check-installed missing pkgname');
|
||||
return false;
|
||||
}
|
||||
let isInstalled = false;
|
||||
|
||||
logger.info(`检查应用是否已安装: ${pkgname}`);
|
||||
|
||||
let child = spawn('/usr/bin/apm', ['list', '--installed', pkgname], {
|
||||
shell: true,
|
||||
env: process.env
|
||||
});
|
||||
|
||||
let output = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
child.on('close', (code) => {
|
||||
if (code === 0 && output.includes(pkgname)) {
|
||||
isInstalled = true;
|
||||
logger.info(`应用已安装: ${pkgname}`);
|
||||
} else {
|
||||
logger.info(`应用未安装: ${pkgname}`);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return isInstalled;
|
||||
});
|
||||
Reference in New Issue
Block a user