Node.js的 spawn在执行优雅关闭时遇到问题。

前端开发 2026-07-07

我现在在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_PROCESSCREATE_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 是从父进程发送到子进程的流,因此它不能从子进程接收数据。)

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章