09. exec()execFile()fork() 与同步 API

如何选择

需要实时处理或输出很大?       → spawn()
明确执行某程序且输出很小?     → execFile()
确实需要 Bash 管道/重定向?    → exec(),命令必须可信
启动另一个 Node.js 模块并 IPC?→ fork()
启动期间必须同步拿到结果?     → *Sync,仅限 CLI/初始化等受控场景

exec():由 Shell 解释命令字符串

import { exec } from "node:child_process";
import { promisify } from "node:util";

const execAsync = promisify(exec);

async function showGitVersion(): Promise<void> {
  const { stdout, stderr } = await execAsync("git --version", {
    encoding: "utf8",
    timeout: 5_000,
    maxBuffer: 1024 * 1024,
  });

  console.log(stdout.trim());
  if (stderr.length > 0) console.warn(stderr);
}

exec() 先缓存 stdout 和 stderr,再把完整结果交给回调。超过 maxBuffer 会报错并终止子进程,因此不适合未知大小输出。

// 严重错误:用户输入进入 Shell,可以注入其他命令。
exec(`convert ${request.fileName}`);

Shell 的引号和转义在 Bash、cmd.exe、PowerShell 中又不同,试图自己“正确转义所有输入”很脆弱。优先换成 execFile()spawn()

execFile():明确程序与参数

import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

async function inspectFile(filePath: string): Promise<string> {
  const { stdout } = await execFileAsync("python", [
    "scripts/inspect.py",
    "--input",
    filePath,
  ], {
    encoding: "utf8",
    timeout: 10_000,
    maxBuffer: 2 * 1024 * 1024,
    shell: false,
  });

  return stdout;
}

参数数组降低 Shell 注入风险,但仍须校验路径穿越、危险业务参数和输出覆盖。

fork():Node.js 子进程与 IPC

import { fork } from "node:child_process";

type WorkerMessage =
  | { type: "ready" }
  | { type: "result"; taskId: string; total: number };

const worker = fork("dist/workers/sum-worker.js", [], {
  stdio: ["ignore", "inherit", "inherit", "ipc"],
});

worker.on("message", (unknownMessage: unknown) => {
  const message = unknownMessage as WorkerMessage;
  console.log(message);
});

worker.send({
  type: "sum",
  taskId: "task-1",
  values: [1, 2, 3],
});

子模块:

process.on("message", (message: unknown) => {
  // 生产代码必须做运行时校验。
  console.log("收到消息", message);
  process.send?.({ type: "ready" });
});

fork() 不是 Unix fork(2) 的直接封装,也不是 worker_threads。它会启动全新的 Node.js 进程,默认建立 IPC。项目以 TypeScript + CommonJS 运行时,生产环境通常先编译,再 fork dist/*.js;直接 fork .ts 需要显式配置 TypeScript 运行器,不宜隐式依赖开发环境。

同步 API

import { execFileSync } from "node:child_process";

const version = execFileSync(process.execPath, ["--version"], {
  encoding: "utf8",
  timeout: 3_000,
});
console.log(version.trim());

同步 API 返回前,当前 JavaScript 线程不能处理计时器、HTTP 请求或其他回调。适合构建脚本和短 CLI,不适合服务器热路径。

时间线与错误结果

exec() / execFile() Promise:

启动 → 缓存 stdout/stderr → 进程结束
  ├─ code 0:resolve({ stdout, stderr })
  └─ 启动失败、非零退出、超时、maxBuffer:reject(error)

reject 的错误对象可能仍带 stdoutstderrcodesignal,日志中应做长度限制和脱敏。

常见错误

最佳实践

练习

  1. 分别用 execFile()spawn() 获取 Node.js 版本,对比结果接口。
  2. 构造超过 maxBuffer 的子脚本,观察错误。
  3. fork() 实现带 requestId 的加法请求/响应。

验收清单