Node.js/TypeScript: 使用子进程来启动和停止另一个服务
由ChatGPT 和 Dalle 3。
你有这样一个 Node.JS 应用,需要运行特定的命令行来启动一个额外的服务器或服务,而且你需要确保其他所有处理都依赖于该服务正常运行吗?
我做到了!
我原本以为只需要运行 exec("node index.js")
或者类似命令来启动服务,但实际上要复杂一点,原因如下。
- 服务需要的端口可能已被占用
- 发出启动命令后,服务并不会立即启动。在开始监听端口之前,它需要进行一些准备工作
- 在做其他处理时,我们还需要服务一直运行
那么!这是我们将在本文中要做的事情。让我们看看如何使用子进程来做一些事情吧。
- 检查端口是否已被占用
- 如果已被占用,则强制关闭该端口
- 发送启动服务/服务器的命令
- 等待设置完成,再执行其他代码
- 完成后停止服务
准备,开始!
开始设置咱们创建一个简单的CommandLineService
类来帮助我们处理所有逻辑相关的事项!
import { ChildProcess, spawn } from 'child_process';
export class CommandLineService {}
现在完全空的呢!我们会边走边加点东西!
查看端口:我在Windows上运行这个程序,所以要检查端口是否被占用并获取其PID,我将使用[netstat](https://www.ibm.com/docs/ja/aix/7.1?topic=n-netstat-command)
。如果你用的是Mac或Linux,你可能更愿意使用lsof
。
// ...
async getPids(port: number): Promise<string[]> {
var resolver: (value: string[]) => void, promise = new Promise<string[]>(function (resolve) {
resolver = resolve;
});
const findstr = spawn('findstr', [":".concat(`${port}`, ".*")], {
stdio: ['pipe'],
});
const netstat = spawn('netstat', ['-ano'], {
stdio: ['ignore', findstr.stdin],
});
var result = '';
findstr.stdout.on('data', function (data) { return (result += data); }); // 将 findstr 的输出数据累加到 result 中
findstr.on('close', function () {
const pids = result.trim().match(/\d+$/gm); // 匹配行尾的数字
const uniquePids = pids?.filter((value: string, index: number, array: string[]) => {
return array.indexOf(value) === index;
})
console.log(`端口 ${port} 对应的任务ID:${uniquePids}`);
return resolver(uniquePids ?? []);
});
findstr.stdin.end(); // netstat 命令输出忽略,只将输入重定向到 findstr
return promise;
}
注意,我们不能像在 PowerShell 中那样直接运行命令 netstat -ano | findstr :{port}
。这就是为什么我们没有直接使用 [execSync](https://nodejs.org/api/child_process.html#child_processexecsynccommand-options)
命令,而是使用 spawn + promise
方式,以便我们可以将 netstat
的 stdio
设置为与 findstr
相同,并在 netstat
关闭时返回结果。
用用它
// 获取25个进程ID
const pids = await this.getPids(25);
基于PID杀死进程
再次强调,我正在使用Windows,因此我们在这里将使用taskkill
。如果你在Mac/Linux上,请将其改为kill
。
幸运的是,这次没什么特别的!我们可以直接使用 execSync
,然后等着它完成。
killPids(pids: string[]) {
for (let index of pids) {
this.killPid(index)
}
}
killPid(pid: string) {
const output = execSync(`taskkill /PID ${pid} /F`, {
encoding: 'utf-8'
});
console.log('已杀死的PID: ', pid);
console.log('输出结果: ', output);
}
就用它
this终止进程ID(pids)
启动服务
我们现在端口已经空闲了,我们可以开始服务了!
我们将在这里使用[spawn](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options)
命令。不同于exec
,它更适合长运行的进程。这正是我们所需要的!
我将在这里举一个例子,比如通过运行一个可执行文件来启动的邮件服务器。
private subprocess: ChildProcess | null = null;
async startMailServer(): Promise<void> {
const exePath = process.env.MAIL_SERVER_EXE_PATH;
var resolver: (value: void) => void, promise = new Promise<void>(function (resolve) {
resolver = resolve;
});
/* 邮件服务器端口 */
const mailPortPids = await this.getPids(25);
this.killPids(mailPortPids)
const subprocess = spawn(exePath, {
detached: true,
});
subprocess.stdout?.on('data', (data) => {
console.log(`stdout: ${data}`);
if (`${data}`.includes(`现在正在监听: http://localhost:25`)) {
return resolver();
}
});
subprocess.stderr?.on('data', (data) => {
console.error(`stderr: ${data}`);
});
subprocess.on('close', (code) => {
console.log(`控制台日志(子进程退出,状态码为 ${code})`);
});
this.subprocess = subprocess;
return promise;
}
嘿!有几个重要的要点!
我们在这里使用了 resolver
和 promise
,因为服务器在执行命令后可能需要一些时间来设置并开始监听端口。我现在通过查看 Now listening on: http://localhost:25
这样的输出来进行确认,但是你应该将其更改为实际启动服务时预期在 stdout
中看到的具体字符串。
我们不能在close
时返回resolver
,因为我们需要让subprocess
继续运行,而且它绝对不能在我们手动结束它之前关闭,除非我们强制结束它!
由于同样的原因,我们在 [detached: true](https://nodejs.org/api/child_process.html#optionsdetached)
这里设置了 detached
选项。请注意,这个选项在 Windows 和其他平台上表现会有所不同。
- 在 Windows 上,将
options.detached
设置为true
可以使子进程在父进程退出后继续运行。子进程将拥有自己的独立控制台窗口。一旦启用了此选项,子进程将无法再切换回非分离模式。 - 在非 Windows 平台上,如果
options.detached
被设置为true
,子进程将会成为新的进程组和会话的领导者。无论这个子进程是否已经被分离,它都可以在父进程退出后继续运行。有关此功能的更多详细信息,请参考[setsid(2)](http://man7.org/linux/man-pages/man2/setsid.2.html)
。
因为我们之后需要回来终止我们的 subprocess
,我在我类中保留了一个指向它的引用。我们在主 Node.js 应用程序运行结束后回来终止 subprocess
。
为了结束这一天的工作,当我们完成时,我们需要停止刚才启动的服务/服务器。这可以通过调用 [kill](https://nodejs.org/api/child_process.html#subprocesskillsignal)
在我们刚才创建的 subprocess
上来实现。
stopMailServer() {
const killed = this.subprocess?.kill();
console.log(`邮件服务器已成功停止: ${killed} `);
}
kill
函数在 [kill(2)](http://man7.org/linux/man-pages/man2/kill.2.html)
调用成功时返回 true
,否则返回 false
。
让我们来总结一下上面的内容,这里我们开始了!我们的CommandLineService
类。
import { ChildProcess, spawn } from 'child_process';
export class CommandLineService {
private subprocess: ChildProcess | null = null;
async startMailServer(): Promise<void> {
const exePath = 'server.exe';
var resolver: (value: void) => void, promise = new Promise<void>(function (resolve) {
resolver = resolve;
});
// 邮件服务器端口
const mailPortPids = await this.getPids(25);
this.killPids(mailPortPids)
const subprocess = spawn(exePath, {
detached: true,
});
subprocess.stdout?.on('data', (data) => {
console.log(`stdout: ${data}`);
if (`${data}`.includes(`正在监听 http://localhost:25`)) {
return resolver();
}
});
subprocess.stderr?.on('data', (data) => {
console.error(`stderr: ${data}`);
});
subprocess.on('close', (code) => {
console.log(`子进程退出码是 ${code}`);
});
this.subprocess = subprocess;
return promise;
}
stopMailServer() {
const killed = this.subprocess?.kill();
console.log(`邮件服务器停止成功:${killed} `);
}
killPids(pids: string[]) {
for (let index in pids) {
this.killPid(pids[index])
}
}
killPid(pid: string) {
const output = execSync(`taskkill /PID ${pid} /F`, {
encoding: 'utf-8'
});
console.log('杀死PID:', pid);
console.log('输出:', output);
}
async getPids(port: number): Promise<string[]> {
var resolver: (value: string[]) => void, promise = new Promise<string[]>(function (resolve) {
resolver = resolve;
});
const findstr = spawn('findstr', [":".concat(`${port}`, ".*")], {
stdio: ['pipe'],
});
const netstat = spawn('netstat', ['-ano'], {
stdio: ['ignore', findstr.stdin],
});
var result = '';
findstr.stdout.on('data', function (data) { return (result += data); });
findstr.on('close', function () {
const pids = result.trim().match(/\d+$/gm);
const uniquePids = pids?.filter((value: string, index: number, array: string[]) => {
return array.indexOf(value) === index;
})
console.log(`端口 ${port} 的 PID 列表: ${uniquePids}`);
return resolver(uniquePids ?? []);
});
findstr.stdin.end();
return promise;
}
}
我们就可以从代码的其他部分像下面这样调用,如下所示。
const service = new CommandLineService();
// 开始邮件服务器
await service.startMailServer(); // 等待邮件服务器启动
// 停止邮件服务器
service.stopMailServer();
谢谢阅读!
今天就说这吧!
祝你好运,成功生成!像丧尸一样!
共同学习,写下你的评论
评论加载中...
作者其他优质文章