mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 01:10:16 +08:00
398 lines
11 KiB
TypeScript
398 lines
11 KiB
TypeScript
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
|
||
|
||
const checkSuperUserCommand = async (): Promise<string> => {
|
||
let superUserCmd = '';
|
||
const execAsync = promisify(exec);
|
||
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!');
|
||
}
|
||
}
|
||
return superUserCmd;
|
||
}
|
||
|
||
const runCommandCapture = async (execCommand: string, execParams: string[], envOverride?: NodeJS.ProcessEnv) => {
|
||
return await new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
|
||
const child = spawn(execCommand, execParams, {
|
||
shell: true,
|
||
env: { ...process.env, ...(envOverride || {}) }
|
||
});
|
||
|
||
let stdout = '';
|
||
let stderr = '';
|
||
|
||
child.stdout?.on('data', (data) => {
|
||
stdout += data.toString();
|
||
});
|
||
|
||
child.stderr?.on('data', (data) => {
|
||
stderr += data.toString();
|
||
});
|
||
|
||
child.on('error', (err) => {
|
||
resolve({ code: -1, stdout, stderr: err.message });
|
||
});
|
||
|
||
child.on('close', (code) => {
|
||
resolve({ code: typeof code === 'number' ? code : -1, stdout, stderr });
|
||
});
|
||
});
|
||
};
|
||
|
||
const parseInstalledList = (output: string) => {
|
||
const apps: Array<{ pkgname: string; version: string; arch: string; flags: string; raw: string }> = [];
|
||
const lines = output.split('\n');
|
||
for (const line of lines) {
|
||
const trimmed = line.trim();
|
||
if (!trimmed) continue;
|
||
if (trimmed.startsWith('Listing')) continue;
|
||
if (trimmed.startsWith('[INFO]')) continue;
|
||
|
||
const match = trimmed.match(/^(\S+)\/\S+,\S+\s+(\S+)\s+(\S+)\s+\[(.+)\]$/);
|
||
if (!match) continue;
|
||
apps.push({
|
||
pkgname: match[1],
|
||
version: match[2],
|
||
arch: match[3],
|
||
flags: match[4],
|
||
raw: trimmed
|
||
});
|
||
}
|
||
return apps;
|
||
};
|
||
|
||
const parseUpgradableList = (output: string) => {
|
||
const apps: Array<{ pkgname: string; newVersion: string; currentVersion: string; raw: string }> = [];
|
||
const lines = output.split('\n');
|
||
for (const line of lines) {
|
||
const trimmed = line.trim();
|
||
if (!trimmed) continue;
|
||
if (trimmed.startsWith('Listing')) continue;
|
||
if (trimmed.startsWith('[INFO]')) continue;
|
||
if (trimmed.includes('=') && !trimmed.includes('/')) continue;
|
||
|
||
if (!trimmed.includes('/')) continue;
|
||
|
||
const tokens = trimmed.split(/\s+/);
|
||
if (tokens.length < 2) continue;
|
||
const pkgToken = tokens[0];
|
||
const pkgname = pkgToken.split('/')[0];
|
||
const newVersion = tokens[1] || '';
|
||
const currentMatch = trimmed.match(/\[(?:upgradable from|from):\s*([^\]\s]+)\]/i);
|
||
const currentToken = tokens[5] || '';
|
||
const currentVersion = currentMatch?.[1] || currentToken.replace('[', '').replace(']', '');
|
||
|
||
if (!pkgname) continue;
|
||
apps.push({ pkgname, newVersion, currentVersion, raw: trimmed });
|
||
}
|
||
return apps;
|
||
};
|
||
|
||
// Listen for download requests from renderer process
|
||
ipcMain.on('queue-install', async (event, download_json) => {
|
||
const download = JSON.parse(download_json);
|
||
const { id, pkgname, upgradeOnly } = 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;
|
||
|
||
// 开始组装安装命令
|
||
let superUserCmd = await checkSuperUserCommand();
|
||
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;
|
||
});
|
||
|
||
ipcMain.on('remove-installed', async (_event, pkgname: string) => {
|
||
const webContents = _event.sender;
|
||
if (!pkgname) {
|
||
logger.warn('remove-installed missing pkgname');
|
||
return;
|
||
}
|
||
logger.info(`卸载已安装应用: ${pkgname}`);
|
||
|
||
let superUserCmd = await checkSuperUserCommand();
|
||
let execCommand = '';
|
||
let execParams = [];
|
||
if (superUserCmd.length > 0) {
|
||
execCommand = superUserCmd;
|
||
execParams.push('/usr/bin/apm');
|
||
} else {
|
||
execCommand = '/usr/bin/apm';
|
||
}
|
||
let child = spawn(execCommand, [...execParams, 'remove', '-y', pkgname], {
|
||
shell: true,
|
||
env: process.env
|
||
});
|
||
let output = '';
|
||
|
||
child.stdout.on('data', (data) => {
|
||
output += data.toString();
|
||
});
|
||
|
||
child.on('close', (code) => {
|
||
const success = code === 0;
|
||
// 拼接json消息
|
||
const messageJSONObj = {
|
||
message: success ? '卸载完成' : `卸载失败,退出码 ${code}`,
|
||
stdout: output,
|
||
stderr: ''
|
||
};
|
||
|
||
if (success) {
|
||
logger.info(messageJSONObj);
|
||
} else {
|
||
logger.error(messageJSONObj);
|
||
}
|
||
|
||
webContents.send('remove-complete', {
|
||
id: 0,
|
||
success: success,
|
||
time: Date.now(),
|
||
exitCode: code,
|
||
message: JSON.stringify(messageJSONObj)
|
||
});
|
||
});
|
||
});
|
||
|
||
ipcMain.handle('list-upgradable', async () => {
|
||
const listCommand = 'source /opt/apm-store/transhell.sh; load_transhell_debug; amber-pm-debug aptss list --upgradable';
|
||
const { code, stdout, stderr } = await runCommandCapture(
|
||
'/bin/bash',
|
||
['-lc', listCommand],
|
||
{ LANGUAGE: 'en_US' }
|
||
);
|
||
if (code !== 0) {
|
||
logger.error(`list-upgradable failed: ${stderr || stdout}`);
|
||
return {
|
||
success: false,
|
||
message: stderr || stdout || `list-upgradable failed with code ${code}`,
|
||
apps: []
|
||
};
|
||
}
|
||
|
||
const apps = parseUpgradableList(stdout);
|
||
return { success: true, apps };
|
||
});
|
||
|
||
ipcMain.handle('list-installed', async () => {
|
||
const superUserCmd = await checkSuperUserCommand();
|
||
const execCommand = superUserCmd.length > 0 ? superUserCmd : '/usr/bin/apm';
|
||
const execParams = superUserCmd.length > 0
|
||
? ['/usr/bin/apm', 'list', '--installed']
|
||
: ['list', '--installed'];
|
||
|
||
const { code, stdout, stderr } = await runCommandCapture(execCommand, execParams);
|
||
if (code !== 0) {
|
||
logger.error(`list-installed failed: ${stderr || stdout}`);
|
||
return {
|
||
success: false,
|
||
message: stderr || stdout || `list-installed failed with code ${code}`,
|
||
apps: []
|
||
};
|
||
}
|
||
|
||
const apps = parseInstalledList(stdout);
|
||
return { success: true, apps };
|
||
});
|
||
|
||
ipcMain.handle('uninstall-installed', async (_event, pkgname: string) => {
|
||
if (!pkgname) {
|
||
logger.warn('uninstall-installed missing pkgname');
|
||
return { success: false, message: 'missing pkgname' };
|
||
}
|
||
|
||
const superUserCmd = await checkSuperUserCommand();
|
||
const execCommand = superUserCmd.length > 0 ? superUserCmd : '/usr/bin/apm';
|
||
const execParams = superUserCmd.length > 0
|
||
? ['/usr/bin/apm', 'remove', '-y', pkgname]
|
||
: ['remove', '-y', pkgname];
|
||
|
||
const { code, stdout, stderr } = await runCommandCapture(execCommand, execParams);
|
||
const success = code === 0;
|
||
|
||
if (success) {
|
||
logger.info(`卸载完成: ${pkgname}`);
|
||
} else {
|
||
logger.error(`卸载失败: ${pkgname} ${stderr || stdout}`);
|
||
}
|
||
|
||
return {
|
||
success,
|
||
message: success ? '卸载完成' : (stderr || stdout || `卸载失败,退出码 ${code}`)
|
||
};
|
||
}); |