为了账号安全,请及时绑定邮箱和手机立即绑定

Node.js/TypeScript: 使用子进程来启动和停止另一个服务

ChatGPT 和 Dalle 3。

你有这样一个 Node.JS 应用,需要运行特定的命令行来启动一个额外的服务器或服务,而且你需要确保其他所有处理都依赖于该服务正常运行吗?

我做到了!

我原本以为只需要运行 exec("node index.js") 或者类似命令来启动服务,但实际上要复杂一点,原因如下。

  • 服务需要的端口可能已被占用
  • 发出启动命令后,服务并不会立即启动。在开始监听端口之前,它需要进行一些准备工作
  • 在做其他处理时,我们还需要服务一直运行

那么!这是我们将在本文中要做的事情。让我们看看如何使用子进程来做一些事情吧。

  1. 检查端口是否已被占用
  2. 如果已被占用,则强制关闭该端口
  3. 发送启动服务/服务器的命令
  4. 等待设置完成,再执行其他代码
  5. 完成后停止服务

准备,开始!

开始设置

咱们创建一个简单的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 方式,以便我们可以将 netstatstdio 设置为与 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;  
    }
嘿!有几个重要的要点!

我们在这里使用了 resolverpromise,因为服务器在执行命令后可能需要一些时间来设置并开始监听端口。我现在通过查看 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();

谢谢阅读!

今天就说这吧!

祝你好运,成功生成!像丧尸一样!

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消