Node.js的 spawn在执行优雅关闭时遇到问题。
我现在在Windows系统上尝试用Node.js搭建一个服务器,运行没问题,但在关闭服务器方面遇到了一些困难。
下面是我目前使用方式的一个非常简单的示例:
const path = require('path');
const { spawn } = require('child_process');
const installPath = "C:\\SteamCMD\\steamapps\\common\\Soulmask Dedicated Server for Windows";
const buildPath = path.join(installPath, "WS\\Binaries\\Win64", "WSServer-Win64-Shipping.exe");
const launchParameters = "Level01_MAIN -server %* -log -UTF8Output -MULTIHOME=0.0.0.0 -EchoPort=18888 -forcepassthrough -Port=8777 -QueryPort=27015 -SteamServerName=ServerName -MaxPlayers=10 -adminpsw=adminpsw -pve -saving=300 -backup=900 -online=Steam -serverpm=1";
const childProcess = spawn(buildPath, launchParameters.split(' '), {
cwd: installPath,
stdio: ['pipe', 'pipe', 'pipe']
});
childProcess.stdout.on('data', (data) => {
console.log('stdout:', data.toString());
});
childProcess.stderr.on('data', (data) => {
console.log('stderr:', data.toString());
});
childProcess.stdin.on('data', (data) => {
console.log('stdin:', data.toString());
});
childProcess.on('error', (data) => {
console.log('error:', data);
});
childProcess.on('exit', (data) => {
console.log('exit:', data);
});
childProcess.on('close', (data) => {
console.log('close:', data);
});
// test 15 秒后关闭服务器
setTimeout(() => {
childProcess.kill('SIGINT');
}, 1000 * 15);
通常不杀死进程,在从 WSServer-Win64-Shipping.exe 的控制台按Ctrl+C时就可以关闭服务器,这也是实现优雅关机的方式。
但当我尝试在Node.js中关闭它,例如执行 childProcess.kill('SIGINT') 时,进程会立即结束。
我也尝试了一些方法,比如 childProcess.kill(),以及SIGINT、SIGTERM和 SIGKILL。
我还尝试了一些向控制台输入内容的操作,依我之见都完全不起作用:childProcess.stdin.write('\x03\n'),连同shutdown、exit、close、quit、kill、stop等。
如果有人有思路,欢迎给出我可以尝试的任何建议。
解决方案
TL;DR: 使用包 ctrlc-windows。
在Linux上,发送 SIGINT 会起作用,但在这里的问题很可能是Windows上 Node的 SIGINT 不是一个合成的Ctrl+C。它被实现为无条件的进程终止,而交互式的Ctrl+C是一个Windows控制台事件。(Windows没有真正的POSIX信号,Node只是对它们在某种程度上进行“模拟”。)
另外,childProcess.stdin.write('\x03') 不起作用,因为它只把字节0x03写入管道。子进程的标准输入不是Windows的控制台输入缓冲区,因此Windows不会把该字节转换为 CTRL_C_EVENT。这就是为什么这两种方法都无法像在服务器窗口中按下Ctrl+C一样工作。
你需要的是Windows API函数 GenerateConsoleCtrlEvent 来合成一个 CTRL_C_EVENT。
然而,它有若干约束:
- 发送方必须附着在与目标相同的控制台上。
CTRL_C_EVENT不能被限制在一个进程组中。它会被发送给附着在该控制台的所有符合条件的进程。CTRL_BREAK_EVENT可以目标一个进程组,但目标必须将Ctrl+Break视为一个优雅关闭事件。- 子进程必须实际拥有一个控制台。使用
DETACHED_PROCESS或CREATE_NO_WINDOW启动的进程没有控制台(但在这里看起来并非如此)。
幸运地,已经有人把所有复杂性处理好,做成了一个名为 ctrlc-windows 的npm包。
它的使用非常简单。你只需要这样:
import { ctrlc } from 'ctrlc-windows'
// ...
// 发送 Ctrl+C
ctrlc(childProcess.pid)
它内部的实现逻辑是:暂时让父进程忽略Ctrl+C(否则我们自己也会被终止),启动一个辅助可执行程序,将该辅助程序从当前控制台分离出来,附加到目标PID的控制台,调用 GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0),然后再还原父进程对Ctrl+C的处理。
(顺便说一句,你应当再检查一下你的参数列表,它包含 %*,在这里没有意义,在批处理文件中才有特殊含义;另外,childProcess.stdin.on('data', ...) 这种用法也不合理,stdin 是从父进程发送到子进程的流,因此它不能从子进程接收数据。)