ProcessLauncher.js 3.22 KB
/**
 * ProcessLauncher — spawn 本地子进程
 *
 * start() 返回 { process, stdin, stdout, stderr }
 * 供 PipeTransport 绑定 stdin/stdout 通信
 */

const { spawn } = require('child_process');
const { Launcher } = require('./Launcher');

class ProcessLauncher extends Launcher {
  constructor() {
    super();
    this.child = null;
  }

  /**
   * @param {object} options
   * @param {string} options.command - 可执行命令
   * @param {string[]} options.args - 参数列表
   * @param {object} [options.env] - 环境变量
   * @param {string} [options.cwd] - 工作目录
   * @returns {Promise<{ process, stdin, stdout, stderr }>}
   */
  async start(options = {}) {
    const { command, args = [], env, cwd } = options;

    const processEnv = { ...process.env, ...(env || {}) };
    delete processEnv.NODE_OPTIONS;
    delete processEnv.ELECTRON_RUN_AS_NODE;
    delete processEnv.CLAUDECODE;

    console.log(`[ProcessLauncher] 启动: ${command} ${args.join(' ')}`);

    this.child = spawn(command, args, {
      stdio: ['pipe', 'pipe', 'pipe'],
      env: processEnv,
      cwd,
    });

    // 等待进程启动(简单延时,进程 error 会 reject)
    await new Promise((resolve, reject) => {
      this.child.on('error', reject);
      setTimeout(resolve, 300);
    });

    return {
      process: this.child,
      stdin: this.child.stdin,
      stdout: this.child.stdout,
      stderr: this.child.stderr,
    };
  }

  /**
   * 终止子进程并等待其完全退出
   *
   * 关闭策略(确保 Node.js event loop 不被悬挂的子进程阻塞):
   *   1. 关闭 stdin(发送 EOF,给子进程优雅退出的机会)
   *   2. 发送 SIGTERM
   *   3. 超时后强制 SIGKILL
   *   4. 等待 exit 事件确保进程完全退出
   *   5. 销毁 stdio 流,释放 event loop 引用
   *
   * @param {number} [timeoutMs=5000] - SIGTERM 超时时间,超时后发送 SIGKILL
   */
  async stop(timeoutMs = 5000) {
    if (!this.child) return;

    const child = this.child;
    this.child = null;

    // 已经退出,直接清理流
    if (child.exitCode !== null || child.signalCode !== null) {
      this._destroyStreams(child);
      return;
    }

    return new Promise((resolve) => {
      // 超时后 SIGKILL 强杀
      const forceKillTimer = setTimeout(() => {
        console.log('[ProcessLauncher] SIGTERM 超时,发送 SIGKILL');
        try { child.kill('SIGKILL'); } catch { }
      }, timeoutMs);

      // 等待进程退出
      child.once('exit', (code, signal) => {
        clearTimeout(forceKillTimer);
        this._destroyStreams(child);
        console.log(`[ProcessLauncher] 子进程已退出 (code=${code}, signal=${signal})`);
        resolve();
      });

      // 关闭 stdin,通知子进程 EOF
      try { child.stdin.end(); } catch { }

      // 发送 SIGTERM
      try { child.kill('SIGTERM'); } catch { }
    });
  }

  /**
   * 销毁子进程的 stdio 流,释放 event loop 引用
   * @private
   */
  _destroyStreams(child) {
    for (const stream of [child.stdin, child.stdout, child.stderr]) {
      if (stream && !stream.destroyed) {
        try {
          stream.removeAllListeners();
          stream.destroy();
        } catch { }
      }
    }
  }
}

module.exports = { ProcessLauncher };