Electron 中的 exec 方法详解

exec 是 Electron 中通过 Node.js 的 child_process 模块提供的一个方法,用于执行系统命令。与 fork 不同,exec 更适合执行 shell 命令而非 Node.js 脚本。

基本概念

child_process.exec() 方法会生成一个 shell 并在该 shell 中运行命令,当完成时将 stdout 和 stderr 传递给回调函数。

基本语法

const { exec } = require('child_process');

exec(command [, options] [, callback])

使用方法

基本示例(ohos electron)

  • 具体实现
const { app, BrowserWindow, ipcMain, Tray, nativeImage } = require('electron');
const { exec } = require('child_process');
const path = require('path');

let mainWindow, tray;

function createWindow() {
    tray = new Tray(nativeImage.createFromPath(path.join(__dirname, 'electron_white.png')));
    mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: false,
            contextIsolation: true,
        }
    });
    console.log('electron-demo start');
    mainWindow.loadFile('index.html');
}

app.whenReady().then(() => {
    createWindow();

    app.on('activate',() => {
        if(BrowserWindow.getAllWindow().length === 0) createWindow();
    });

    app.on('window-all-closed', () => {
        if (process.platform !== 'darwin') app.quit();
    });
});

// exec
console.log('electron-demo 即将开始exec!' + __dirname);
const child_echo = exec('echo "test"', (error, stdout, stderr) => {
    if (error) {
        console.error(`exec error: ${error}`);
        return;
    }
    console.log(`stdout: ${stdout}`);
    console.error(`stderr: ${stderr}`);
});

// ohos electron上ls命令应参照如下实现,否则会有权限问题
const child_ls =  exec('ls', { cwd: __dirname, env: { NODE_ENV: 'test' },
  stdio: 'pipe' }, (error, stdout, stderr) => {
    if (error) {
        console.error(`electron-demo 执行错误: ${error}`);
        return;
    }
    console.log(`electron-demo 输出: ${stdout}`);
    if (stderr) console.error(`electron-demo 错误: ${stderr}`);
});

// 监听 exit 事件
child_echo.on('exit', (code, signal) => {
    console.log(`echo 子进程退出,退出码: ${code}`);
    if (signal) {
        console.log(`echo 进程被信号终止: ${signal}`);
    }
    console.log('echo child.exitCode: ', child_echo.exitCode)
});

child_ls.on('exit', (code, signal) => {
    console.log(`ls 子进程退出,退出码: ${code}`);
    if (signal) {
        console.log(`ls 进程被信号终止: ${signal}`);
    }
    console.log('ls child.exitCode: ', child_ls.exitCode)
});

  • 运行效果

image

使用注意事项

1. 安全性问题

  • 永远不要直接执行用户输入:这可能导致命令注入攻击
    // 危险示例!
    const userInput = 'some; rm -rf /';
    exec(`ls ${userInput}`, (error, stdout) => { /* ... */ });
    
    // 安全做法:使用参数化或严格验证
    const { execFile } = require('child_process');
    execFile('ls', [validatedInput], (error, stdout) => { /* ... */ });
    

2. 性能考虑

  • exec 会启动一个 shell,比 execFile 开销更大
  • 对于高频或性能敏感的操作,考虑使用 execFile

3. 跨平台兼容性

  • 命令在不同操作系统上可能表现不同
    const command = process.platform === 'win32' ? 'dir' : 'ls';
    exec(command, (error, stdout) => { /* ... */ });
    

4. 输出限制

  • 默认缓冲区大小为 200KB (1MB 从 Node.js v12.x 开始)
  • 大数据量输出可能导致内存问题
    // 增加缓冲区大小
    exec('ls -lh', { maxBuffer: 1024 * 1024 * 5 }, (error, stdout) => {
      /* ... */
    });
    

5. 错误处理

  • 必须处理 error 和 stderr
  • exit code 不为 0 时会触发 error

最佳实践

1. 使用 execFile 替代 exec(当不需要 shell 特性时)

const { execFile } = require('child_process');

// 更安全,不启动 shell
execFile('ls', ['-lh'], (error, stdout, stderr) => {
  /* ... */
});

2. 使用 util.promisify 包装为 Promise

const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);

async function runCommand() {
  try {
    const { stdout, stderr } = await execAsync('ls -lh');
    console.log(stdout);
  } catch (error) {
    console.error(error);
  }
}

3. 设置超时防止挂起

exec('sleep 10', { timeout: 2000 }, (error) => {
  if (error?.killed) {
    console.log('命令因超时被终止');
  }
});

4. 清理子进程

const child = exec('long-running-command');

// 应用退出时清理
process.on('exit', () => {
  child.kill();
});

// 或者设置超时
setTimeout(() => {
  child.kill();
}, 5000);

5. 工作目录与环境变量

exec('ls -lh', {
  cwd: '/path/to/directory', // 设置工作目录
  env: { ...process.env, CUSTOM_ENV: 'value' } // 环境变量
}, (error, stdout) => {
  /* ... */
});

测试验证方法

1. 基本功能测试

// test-exec.js
const { exec } = require('child_process');
const assert = require('assert');

// 测试正常命令
exec('echo "hello"', (error, stdout, stderr) => {
  assert(!error, '不应返回错误');
  assert.strictEqual(stdout.trim(), 'hello', '输出应匹配');
  assert.strictEqual(stderr, '', '不应有 stderr 输出');
  console.log('基本功能测试通过');
});

// 测试错误命令
exec('non-existent-command', (error) => {
  assert(error, '应返回错误');
  console.log('错误处理测试通过');
});

2. 安全性测试

// 测试命令注入
const maliciousInput = '; echo "hacked" > test.txt';
exec(`echo "safe"${maliciousInput}`, (error, stdout) => {
  // 检查是否创建了 test.txt 文件
  // 预期结果: 不应创建文件
});

3. 性能测试

// 测试缓冲区限制
const largeOutputCommand = 'yes "This is a long line" | head -n 100000';

console.time('exec');
exec(largeOutputCommand, { maxBuffer: 1024 * 1024 }, (error) => {
  console.timeEnd('exec');
  if (error?.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
    console.log('正确捕获了缓冲区溢出错误');
  }
});

4. 超时测试

// timeout-test.js
exec('sleep 5', { timeout: 1000 }, (error) => {
  assert(error?.killed, '进程应因超时被终止');
  assert(error?.signal === 'SIGTERM', '应收到 SIGTERM 信号');
  console.log('超时测试通过');
});

总结

Electron 中的 exec 方法是一个强大的工具,可以执行系统命令并与操作系统交互,但使用时需要注意:

  1. 安全第一:永远不要直接执行未经验证的用户输入
  2. 合理使用:根据需求选择 execexecFile
  3. 完善处理:正确处理错误、超时和子进程生命周期
  4. 性能考量:注意缓冲区限制和跨平台差异

通过遵循最佳实践和进行充分的测试验证,可以安全有效地在 Electron 应用中使用 exec 方法。


返回子进程目录 | 返回文档首页