| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- const { spawn, exec } = require('child_process');
- const fs = require('fs').promises;
- const path = require('path');
- class SpawnLauncher {
- constructor() {
- this.processes = new Map();
- }
-
- /**
- * 安全的启动方法
- */
- async safeLaunch(exePath, args = [], options = {}) {
- try {
- console.log(`safe start: ${exePath}`);
-
- // 1. 验证文件
- const validation = await this.validateFile(exePath);
- if (!validation.valid) {
- throw new Error(validation.error);
- }
-
- // 2. 检查权限
- const canExecute = await this.checkPermissions(exePath);
- if (!canExecute) {
- throw new Error(`None access: ${exePath}`);
- }
-
- // 3. 准备参数
- const spawnOptions = this.prepareSpawnOptions(options);
-
- // 4. 执行启动
- return await this.executeSpawn(exePath, args, spawnOptions);
-
- } catch (error) {
- console.error('start fail:', error);
-
- // 5. 尝试备用方法
- if (error.code === 'EACCES') {
- return await this.tryAlternativeMethods(exePath, args, options);
- }
-
- throw error;
- }
- }
-
- /**
- * 验证文件
- */
- async validateFile(filePath) {
- console.log(`验证文件: ${filePath}`);
-
- // 检查是否为字符串
- if (typeof filePath !== 'string' || filePath.trim() === '') {
- return { valid: false, error: 'appPath is null' };
- }
-
- // 标准化路径
- const normalizedPath = this.normalizePath(filePath);
-
- try {
- // 检查文件是否存在
- await fs.access(normalizedPath, fs.constants.F_OK);
- } catch {
- return {
- valid: false,
- error: `file not exist: ${normalizedPath}`
- };
- }
-
- // 检查是否是文件
- try {
- const stats = await fs.stat(normalizedPath);
- if (!stats.isFile()) {
- return {
- valid: false,
- error: `not a file: ${normalizedPath}`
- };
- }
- } catch {
- return {
- valid: false,
- error: `can not access file: ${normalizedPath}`
- };
- }
-
- // 检查文件扩展名
- const ext = path.extname(normalizedPath).toLowerCase();
- if (process.platform === 'win32') {
- if (ext !== '.exe' && ext !== '.bat' && ext !== '.cmd') {
- console.warn(`no support extension: ${ext}`);
- }
- }
-
- return { valid: true, normalizedPath };
- }
-
- /**
- * 检查权限
- */
- async checkPermissions(filePath) {
- try {
- // 检查读权限
- await fs.access(filePath, fs.constants.R_OK);
-
- // 检查执行权限
- await fs.access(filePath, fs.constants.X_OK);
-
- return true;
- } catch (error) {
- console.log(`check access error: ${error.message}`);
- return false;
- }
- }
-
- /**
- * 准备 spawn 选项
- */
- prepareSpawnOptions(options) {
- const defaultOptions = {
- stdio: 'ignore',
- detached: true,
- windowsHide: true,
- shell: false, // 重要:Windows 上可能需要设为 true
- cwd: process.cwd(),
- env: { ...process.env },
- ...options
- };
-
- return defaultOptions;
- }
-
- /**
- * 执行 spawn
- */
- async executeSpawn(exePath, args, options) {
- return new Promise((resolve, reject) => {
- console.log(`exec spawn: ${exePath}`, args);
-
- const processId = Date.now().toString();
- const child = spawn(exePath, args, options);
-
- // 设置超时
- const timeout = setTimeout(() => {
- reject(new Error('exec timeout'));
- }, 10000);
-
- child.on('error', (error) => {
- clearTimeout(timeout);
- console.error('Spawn error:', error);
- reject(error);
- });
-
- child.on('spawn', () => {
- clearTimeout(timeout);
- console.log(`proecess start success, PID: ${child.pid}`);
-
- // 保存进程信息
- this.processes.set(processId, {
- id: processId,
- pid: child.pid,
- process: child,
- startTime: new Date()
- });
-
- // 允许父进程独立退出
- if (options.detached) {
- child.unref();
- }
-
- resolve({
- success: true,
- processId,
- pid: child.pid,
- command: `${exePath} ${args.join(' ')}`
- });
- });
-
- // 监听退出
- child.on('exit', (code) => {
- console.log(`进程 ${processId} 退出, 代码: ${code}`);
- this.processes.delete(processId);
- });
- });
- }
-
- /**
- * 尝试备用方法
- */
- async tryAlternativeMethods(exePath, args, options) {
- console.log('try start nativeMethods...');
-
- //1: shell: true
- try {
- console.log('1. shell: true');
- const result = await this.executeSpawn(exePath, args, {
- ...options,
- shell: true
- });
- return result;
- } catch (error1) {
- console.log('shell fail:', error1.message);
- }
-
- //2: exec
- try {
- console.log('2. exec');
- const result = await this.executeWithExec(exePath, args, options);
- return result;
- } catch (error2) {
- console.log('exec fail:', error2.message);
- }
-
- // 3: 管理员身份运行
- try {
- console.log('3: admin');
- const result = await this.runAsAdmin(exePath, args);
- return result;
- } catch (error3) {
- console.log('admin fail:', error3.message);
- }
-
- throw new Error('all fail');
- }
-
- /**
- * 使用 exec
- */
- async executeWithExec(exePath, args, options) {
- return new Promise((resolve, reject) => {
- const command = this.buildCommand(exePath, args);
- console.log(`exec: ${command}`);
-
- exec(command, options, (error, stdout, stderr) => {
- if (error) {
- reject(error);
- } else {
- resolve({
- success: true,
- stdout,
- stderr,
- pid: process.pid
- });
- }
- });
- });
- }
-
- /**
- * 以管理员身份运行
- */
- async runAsAdmin(exePath, args) {
- if (process.platform !== 'win32') {
- throw new Error('only support Windows');
- }
-
- return new Promise((resolve, reject) => {
- const escapedArgs = args.map(arg =>
- arg.includes(' ') ? `"${arg}"` : arg
- ).join(' ');
-
- const command = `"${exePath}" ${escapedArgs}`;
- const psCommand = `
- $psi = New-Object System.Diagnostics.ProcessStartInfo
- $psi.FileName = '${exePath}'
- $psi.Arguments = '${escapedArgs}'
- $psi.Verb = 'RunAs'
- $psi.UseShellExecute = $true
- [System.Diagnostics.Process]::Start($psi)
- `;
-
- exec(`powershell -Command "${psCommand}"`, (error) => {
- if (error) {
- reject(error);
- } else {
- resolve({
- success: true,
- admin: true
- });
- }
- });
- });
- }
-
- buildCommand(exePath, args) {
- const escapedExe = exePath.includes(' ') ? `"${exePath}"` : exePath;
- const escapedArgs = args.map(arg =>
- arg.includes(' ') ? `"${arg}"` : arg
- ).join(' ');
-
- return `${escapedExe} ${escapedArgs}`.trim();
- }
-
- normalizePath(filePath) {
- // 解析相对路径
- if (!path.isAbsolute(filePath)) {
- filePath = path.resolve(process.cwd(), filePath);
- }
-
- // 标准化路径分隔符
- return filePath.replace(/\//g, '\\');
- }
-
- /**
- * 获取文件信息
- */
- async getFileInfo(filePath) {
- try {
- const stats = await fs.stat(filePath);
- const permissions = {
- readable: !!(stats.mode & fs.constants.R_OK),
- writable: !!(stats.mode & fs.constants.W_OK),
- executable: !!(stats.mode & fs.constants.X_OK)
- };
-
- return {
- exists: true,
- isFile: stats.isFile(),
- isDirectory: stats.isDirectory(),
- size: stats.size,
- createdAt: stats.birthtime,
- modifiedAt: stats.mtime,
- permissions,
- mode: stats.mode.toString(8) // 八进制权限
- };
- } catch (error) {
- return {
- exists: false,
- error: error.message
- };
- }
- }
- }
- module.exports = SpawnLauncher;
|