持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
在 Node.js 中,通过 spawn 可以创建子进程,返回一个 child_process 对象,该对象上面提供了很多的事件,例如:
- close
- error
- exit
- message
- disconnect
这里比较容易搞混的就是 close 和 exit 的区别,从字面上理解一个叫做「关闭」,一个叫做「退出」,那你可能会问:
- 进程退出了就意味着关闭了吗?
- 进程是先关闭还是先退出呢?
今天笔者就遇到了一个案例:一个通过 spawn 创建的子进程,只监听了 close 事件,没有监听 exit 事件,发现进程已经退出了,但 close 事件迟迟没有触发。
于是想就刨根问底把这个事情搞明白,就先去查阅了官方文档,发现官方文档里面有这么一句话,差点把我给误导了:
The
closeevent will always emit afterexitwas already emitted orerrorif the child failed to spawn
可能是我英文不好,第一次读的时候以为:exit或 error事件触发之后一定会触发 exit事件。
其实并不是这样,它这句话主要想表达这三个事件发生的次序:即 close永远在 exit和 error之后触发。但是并不意味着发生了 exit或 error事件就必然有 close事件!
听着感觉有点绕,没关系,我们用案例来研究一下:
const cp = require('child_process').spawn('sleep', ['100'])
cp.on('exit', console.log.bind(console, 'exited'))
cp.on('close', console.log.bind(console, 'closed'))
setTimeout(function () {
console.log('Going to kill')
cp.kill()
}, 500)
得到的结果是:
Going to kill
exited null SIGTERM
closed null SIGTERM
可以看到,在 kill 之后,先触发了 exit 事件,紧接着触发了 close 事件,一切正常。我们把代码改一下,将子进程的 stdout 重定向到另外一个子进程的 stdin 里面:
const cp = require('child_process').spawn('yes')
cp.on('exit', console.log.bind(console, 'exited'))
cp.on('close', console.log.bind(console, 'closed'))
const cpNext = require('child_process').spawn('cat')
cp.stdout.pipe(cpNext.stdin)
setTimeout(function () {
console.log('Going to kill')
cp.kill()
}, 500)
此时得到的结果是:
Going to kill
exited null SIGTERM
有没有觉得很奇怪,此时进程已经被 kill 掉了,但是并没有触发 close 事件,这是为什么呢?其实官方文档中也给出了明确的解释:
The
closeevent is emitted after a process has ended and the stdio steams of child process have been closed.
即只有当进程退出,且输入输出流关闭了之后才会触发 close事件。我们把上面的代码改造一下:
const cp = require('child_process').spawn('yes')
cp.on('exit', console.log.bind(console, 'exited'))
cp.on('close', console.log.bind(console, 'closed'))
const cpNext = require('child_process').spawn('cat')
cp.stdout.pipe(cpNext.stdin)
setTimeout(function () {
console.log('Going to kill')
cp.kill()
setTimeout(function () {
cp.stdout.destroy() // 关闭了 stdout 之后,才会触发 close 事件
}, 500)
}, 500)
过了 500 毫秒之后,触发了 close 事件:
Going to kill
exited null SIGTERM
closed null SIGTERM
你可能会问,如果不 destroy 掉子进程的 stdout,而是 kill 另外一个接收输入的子进程,会是什么结果呢?
const cp = require('child_process').spawn('yes')
cp.on('exit', console.log.bind(console, 'exited'))
cp.on('close', console.log.bind(console, 'closed'))
const cpNext = require('child_process').spawn('cat')
cp.stdout.pipe(cpNext.stdin)
setTimeout(function () {
console.log('Going to kill')
cp.kill()
setTimeout(function () {
cpNext.kill()
}, 500)
}, 500)
此时会报错:
Going to kill
exited null SIGTERM
node:events:368
throw er; // Unhandled 'error' event
^
Error: write EPIPE
at WriteWrap.onWriteComplete [as oncomplete] (node:internal/stream_base_commons:98:16)
Emitted 'error' event on Socket instance at:
at Socket.onerror (node:internal/streams/readable:773:14)
at Socket.emit (node:events:390:28)
at emitErrorNT (node:internal/streams/destroy:157:8)
at emitErrorCloseNT (node:internal/streams/destroy:122:3)
at processTicksAndRejections (node:internal/process/task_queues:83:21) {
errno: -32,
code: 'EPIPE',
syscall: 'write'
}
write EPIPE 的意思是:当你尝试写入一个读端已经被关闭的管道时,就会出现这个错误。虽然进程已经退出,但是管道还在,因此正确的方式为在 kill 之前,销毁输入输出流:
const cp = require('child_process').spawn('yes')
cp.on('exit', console.log.bind(console, 'exited'))
cp.on('close', console.log.bind(console, 'closed'))
const cpNext = require('child_process').spawn('cat')
cp.stdout.pipe(cpNext.stdin)
setTimeout(() => {
console.log('Going to kill')
cp.stdout.destroy()
cp.kill()
setTimeout(() => {
cpNext.stdin.destroy()
cpNext.kill()
}, 500)
}, 500)
到这里,相信大家已经看明白了,触发了 exit 事件并不意味着一定会触发 close 事件。所以在选择监听事件的时候,如果只是关心进程是否存在,只需要监听 exit 事件即可,如果不关心进程是否存在,而是关心输入输出,则需要监听 close 事件。