1. child_process.spawn
  2. child_process.exec
  3. child_process.execFile
  4. child_process.fork
    1. inspect 导致端口被占用的解决办法

child_process

本文环境:

  • windows
  • node v12.16.1

本文使用的模块:

1
2
3
const cp = require("child_process");
const path = require("path");
const iconv = require("iconv-lite");

child_process 模块提供了 4 种方法创建子进程:

  • child_process.spawn
  • child_process.execFile
  • child_process.exec
  • child_process.fork

除此之外, 还用 3 种同步方法:

  • child_process.spawnSync
  • child_process.execFileSync
  • child_process.execSync

child_process.spawn

使用 spawn 执行 cmd 命令:
在 window 下需要使用 iconv.decode 将其输出解码为 gbk, 否则会乱码.

1
2
3
4
const child = cp.spawn("ping", ["www.baidu.com"]);
child.stdout.on("data", data => {
process.stdout.write(iconv.decode(data, "gbk"));
});

1.PNG

使用 spawn 执行 cmd/bat 脚本文件
子进程 echo.bat 代码:

1
echo "bat 启动"

主进程代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// spawn
const child = cp.spawn("cmd.exe", ["/c", "echo.bat"]);
// or
// const child = cp.spawn("echo.bat");
// or
// 绝对路径
// const child = cp.spawn(path.join(__dirname, "echo.bat"));

child.stdout.on("data", data => {
process.stdout.write(iconv.decode(data, 'gbk'));
});

child.on("exit", code => {
console.log(`退出码 ${code}`);![3.PNG](/img/22a0f95f.PNG)
});

2.PNG

注意: spawn 不支持诸如 ./ 的路径.

1
2
3
4
5
const child = cp.spawn("./echo.bat");

child.stderr.on("data", data => {
console.log(iconv.decode(data, "gbk"));
});

捕获.PNG

使用 stderr 捕获错误
子进程 error.bat 代码:

1
printt www.baidu.com

主进程代码:

1
2
3
4
5
6
7
8
9
10
// spawn
const child = cp.spawn("cmd.exe", ["/c", "error.bat"]);

child.stderr.on("data", data => {
console.log("stderr: ", iconv.decode(data, "gbk"));
});

child.on("exit", code => {
console.log(`退出码 ${code}`);
});

4.PNG

正常退出时退出码为 0, 出错退出时退出码为 1.

使用 spawn 运行 js 文件
子进程 child.js 的代码:

1
console.log("子进程 启动");

主进程代码:

1
2
3
4
const child = cp.spawn("node", ["./child.js"]);
child.stdout.on("data", data => {
console.log(String(data)); // 子进程启动
});

让 spawn 支持 ipc 通信
子进程代码:

1
2
3
4
5
6
7
console.log("子进程启动");
// 发送消息
process.send("foo");
// 接收消息
process.on("message", message => {
console.log("子进程收到消息: ", message);
});

主进程代码:

1
2
3
4
5
6
7
8
9
const child = cp.spawn("node", ["./child.js"], {
// 建立通信管道
stdio: [0, 1, 2, "ipc"],
});
// 发送消息
child.send("bar");
child.on("message", message => {
console.log("父进程收到消息: ", message);
});

输出结果,代码运行完成后不会自动退出.

5.PNG

options.stdio | Node.js API 文档
Electron 中使用 fork()函数的坑

child_process.exec

child_process.exec 在内部调用 child_process.execFile

exec 执行 cmd 命令:

1
2
3
4
const child = cp.exec("ping www.baidu.com", { encoding: "buffer" });
child.stdout.on("data", data => {
process.stdout.write(`${iconv.decode(data, "gbk")}`);
});

注意:
exec 或默认设置 encoding 为 utf-8, 这可能会导致控制台乱码.
为了避免输出乱码, 需要设置 encoding: 'buffer', 同时用 iconv.decode 解码为 gbk.

Node.js 调用 cmd 输出中文乱码_JavaScript_liuyaqi1993 的博客-CSDN 博客

exec 不需要把执行命令的参数拆开来写, 直接用空格分开即可.

1
2
3
cp.spawn("ping", ["www.baidu.com"]);

cp.exec("ping www.baidu.com");

输出错误信息:

1
2
3
4
5
6
7
8
const child = cp.exec("pingt www.baidu.com", { encoding: "buffer" });
child.stderr.on("data", data => {
process.stdout.write(`stderr: ${iconv.decode(data, "gbk")}`);
});

child.on("close", code => {
process.stdout.write(`退出码 ${code}`);
});

6.PNG

exec 方法的第三个参数接收一个回调函数, 这个函数会在子进程运行结束后被调用.

1
2
3
4
5
6
7
cp.exec(
"ping www.baidu.com",
{ encoding: "buffer" },
(error, stdout, stderr) => {
console.log(`stdout: ${iconv.decode(stdout, "gbk")}`);
}
);

使用 exec 运行 js 文件:

1
2
3
4
const child = cp.exec("node child.js");
child.stdout.on("data", data => {
console.log(data);
});

exec execFile fork 对于 stdout 和 stderr 接收的数据有大小限制(spawn 没有这个限制) ,可通过 maxBuffer 控制, 默认值为 1024 x 1024.
超过 maxBuffer 限制会导致如下错误:

1
RangeError [ERR_CHILD_PROCESS_STDIO_MAXBUFFER]: stdout maxBuffer length exceeded

该错误须通过调用 exec 时传入的回调函数获取, child.stderr 无法抓取到该错误.
子进程 child.js 内容:

1
2
console.log("abc");
console.log("def"); // 没有被执行

主进程代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const child = cp.exec(
"node child.js",
{ maxBuffer: 1 },
(err, stdout, stderr) => {
console.log("callback stdout", stdout);
console.log("callback stderr", stderr);
console.log("callback err", err); // RangeError
}
);
child.stdout.on("data", data => {
console.log("stdout", data); // abc
});
child.stderr.on("data", data => {
// 没有获取到错误信息
console.log("stderr", data);
});
child.on("code", code => {
console.log("code", code); // null
});

7.PNG

child_process.execFile

child_process.execFile 在内部调用 child_process.spawn 方法

child_process.execFile 类似 child_process.exec, 区别如下:
1 execFile 不会衍生 shell 进程,效率稍微比 exec 高
2 execFile 在 windows 下不支持 I/O 重定向和文件通配等行为,例如下列命令 execFile 就无法执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cp.execFile("dir", { encoding: "buffer" }, (err, stdout) => {
console.log(err);
console.log(iconv.decode(stdout, "gbk"));
}); // Error: spawn dir ENOENT

// 也可以通过设置 shell: true 使其正常工作
cp.execFile("dir", { encoding: "buffer", shell: true }, (err, stdout) => {
console.log(err);
console.log(iconv.decode(stdout, "gbk"));
});

cp.exec("dir", { encoding: "buffer" }, (err, stdout) => {
console.log(err);
console.log(iconv.decode(stdout, "gbk"));
});

在 windows 环境下没必要使用 execFile

node.js - exec vs execFile nodeJs - Stack Overflow

child_process.fork

child_process.exec 方法专门用于生成 node 进程, 在内部调用 child_process.exec 方法.

子进程 child.js 代码:

1
2
3
4
5
6
7
console.log("子进程启动");
// 发送消息
process.send("foo");
// 接收消息
process.on("message", message => {
console.log("子进程收到消息: ", message);
});

主进程 index.js 代码:

1
2
3
4
5
6
const child = cp.fork("./child.js");
// 发送消息
child.send("bar");
child.on("message", message => {
console.log("父进程收到消息: ", message);
});

11.PNG

inspect 导致端口被占用的解决办法

使用 fork 时, 子进程会默认使用主进程的 execArgv 参数.当子进程启动时, 主进程的 inspect 已经占用了 9229 端口,使子进程无法启动.

例如使用如下命令启动 inde.js:

1
node --inspect index.js

主进程代码:

1
2
3
4
5
6
7
const child = cp.fork("./child.js");
// 发送消息
child.send("bar");
child.on("message", message => {
console.log("父进程收到消息: ", message);
});
child.on("exit", code => console.log("退出码: ", code));

端口被占用,无法启动
1.PNG

解决办法 1,更换端口:

1
2
3
const child = cp.fork("./child.js", [], {
execArgv: ["--inspect=9555"],
});

inspect 的非默认端口需要在 chrome://inspect 页面的 configure 选项中手动添加

或是覆盖掉原来的 execArgv:

1
2
3
const child = cp.fork("./child.js", [], {
execArgv: [],
});

debugging - How to debug Node.JS child forked process? - Stack Overflow

另外测试时发现 --inspect 启动相对较慢, 有些 debugger 似乎被忽略掉了. 最好用 --inspect-brk 代替.

1
2
3
4
5
6
console.log("子进程启动");
// 发送消息
debugger; // 没有办法在这停下来
setTimeout(() => {
debugger; // 这个倒是没有问题
}, 5000);

用 --inspect-brk 代替, 保证遇到 debugger 能停下来等待调试.

1
2
3
const child = cp.fork("./child.js", [], {
execArgv: ["--inspect-brk=9555"],
});

用 ndb 调试多进程比用 inspect 调试好很多. 如果使用 ndb 调试, 务必要保证 execArgv 里没有 --inspect, 否则遇到 debugger 关键字时不会自动停下来.