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 的错误对象可能仍带 stdout、stderr、code、signal,日志中应做长度限制和脱敏。
常见错误
- 用
exec()执行可由用户控制的字符串。 - 用
execFile()获取数 GB 输出,忽视maxBuffer。 - 在 Express 路由里使用同步 API。
- 认为
fork()会共享父进程变量。 - 只写 TypeScript 消息类型,不做运行时校验。
最佳实践
- 只有确需 Shell 语法时才使用
exec()。 - 小输出选
execFile(),大输出或实时输出选spawn()。 fork()的消息加type、requestId/taskId,并验证未知输入。- 显式设置
timeout、maxBuffer、cwd和必要的env。
练习
- 分别用
execFile()与spawn()获取 Node.js 版本,对比结果接口。 - 构造超过
maxBuffer的子脚本,观察错误。 - 用
fork()实现带requestId的加法请求/响应。
验收清单
- [ ] 能解释
exec()为什么有注入风险。 - [ ] 能根据输出大小选择 Buffer 模式或 Stream 模式。
- [ ] 能说明
fork()与 Worker Thread 的差异。 - [ ] 不在服务请求处理中使用同步 API。