likes
comments
collection
share

node中spawn启动exe程序(exec所在问题)

作者站长头像
站长
· 阅读数 2

目的: 开发electron桌面应用,服务端(c++开发的exe程序)随着打包客户端一并打包。想要在用户打开客户端的时候,同时将桌面应用的服务端启动,并使服务端exe程序隐藏运行。并且服务端程序要随着客户端的关闭而关闭。

做法: 启动客户端的同时,使用node来启动服务端exe程序。在node.js中,需要使用child_process模块来启动一个子进程(服务端exe程序),阅读文档发现可以使用child_process中的execspawn来实现打开服务端exe程序

execspawn的区别?

官方文档:child_process 子进程 | Node.js v20 文档 (nodejs.cn)

  1. 运行命令参数:

    1. exec 参数为要运行的command命令,参数以空格分隔
    2. spawn 第一个参数为要运行command命令,参数使用字符串数组(string[])的形式传入第二个参数当中
  2. 配置项:

    1. exec的配置项使用对象的形式传入第二个参数,部分配置:

      配置项类型原理
      timeout<number>运行超时时间。默认值:0
      maxBuffer<number>标准输出或标准错误上允许的最大数据量(单位:字节)。如果超过,则子进程将终止并截断任何输出,默认1024*1024
      windowsHide<number>隐藏通常在 Windows 系统上创建的子进程控制台窗口。默认值:false
    2. spawn的配置项使用对象的形式传入第三个参数,部分配置:

      配置项类型原理
      detached<boolean>准备子进程独立于其父进程允许
      windowsHide<boolean>隐藏通常在 Windows 系统上创建的子进程控制台窗口。默认值:false
      signal<AbortSignal>允许使用 AbortSignal 中止子进程

启动服务端exe程序:

针对打开客户端的时候运行服务端,对execspawn的第一个参数传入正确的服务端路径就可以成功打开exe服务端程序

const { spawn } = require('child_process')
const path = require('path')
// 服务端路径
const CPLUSPLUS_EXECUTABLE_PATH = path.join(__dirname, '..', 'grpc_server', 'FileTransGRPCServer.exe')
let childProcess = spawn(CPLUSPLUS_EXECUTABLE_PATH)

此时就可以成功的启动指定路径的FileTransGRPCServer.exe程序,我已将该服务文件,放入electron客户端开发目录下。

配置子进程:

  1. exe程序在Windows上隐藏运行,使用到了spawn的配置项windowsHide: true传入spawn的第三个配置项中
  2. detached设为true让子进程独立于父进程运行
  3. 第二个参数为空,则使用[]占位
const { spawn } = require('child_process')
const path = require('path')
// 服务端路径
const CPLUSPLUS_EXECUTABLE_PATH = path.join(__dirname, '..', 'grpc_server', 'FileTransGRPCServer.exe')
// 配置项
const EXECUTION_OPTIONS = {
  windowsHide: true,
  detached: true, // 让子进程独立于父进程运行
}
let childProcess = spawn(CPLUSPLUS_EXECUTABLE_PATH, [], EXECUTION_OPTIONS)

此时可以在启动客户端时,在 Windows系统上隐藏并且独立运行exe服务端程序。

关闭客户端同时关闭服务端:

使用spawn创建的子进程,会返回一个ChildProcess,使用变量接收。

关闭子进程的三种方法:

  1. spawn创建子进程中配置signal来允许使用 AbortSignal中止子进程。

    1. 需要安装npm i abort-controller 使用

    2. // 1. 导入
      const { AbortController } = require('abort-controller')
      // 2. 创建实例化对象
      const controller = new AbortController();
      // 3. 解构 spawn 使用的配置项  signal
      const { signal } = controller;
      // 4. 配置 signal
      let childProcess = spawn(CPLUSPLUS_EXECUTABLE_PATH, [], { signal })
      // 5. 此时停止子进程
      controller.abort();
      
  2. 使用命令关闭进程:

    1. 指定pid关闭进程,taskkill /pid 10802 -f,使用childProcess.pid获取进程pid(exec创建的进程除外后续讲到)
    2. 指定进程名称关闭进程,taskkill /im 进程名称 -f
  3. 使用ChildProcess.kill()来关闭子进程


上述为使用spawn创建的子进程并且关闭子进程的操作。

在这之前,我使用的exec来去创建子进程,并且尝试关闭启动的子进程,发现无法关闭,发生错误。

关闭exec创建子进程的问题:

  1. 问题: 在使用上述方法来关闭进程时,发现报错无法关掉启动的进程。
  2. 原因:exec创建子进程返回的实例,不是真正启动的子进程,而是对子进程的套壳进程。获取的pid也为套壳进程的pid,而非子进程的pid。所以无法使用上述方法1,方法2.1,方法3来关闭子进程。

最终使用child_process下的spawn来创建子进程实现子进程(服务端exe程序)的启动和关闭!


到此,打包之后的客户端是无法访问到服务端来去启动服务端。因为在打包之后,electron客户端中的服务端被一并打包,需要在打包后的目录下暴露服务端程序,才可在打包后,启动客户端来正确的启动服务端。

暴露服务端:vue.config.js

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  pluginOptions: {
    electronBuilder: {
      // *** 省略其他配置 ***
      builderOptions: {
        extraResources: [
          { "from": "grpc_server", "to": "./grpc_server" } // 打包后暴露服务端程序
        ]
      }
    }
  }
})

这样就成功了!


完整代码

附上启动和关闭服务端exe程序文件:

const { spawn } = require('child_process');
const path = require('path');
const CPLUSPLUS_EXECUTABLE_PATH = path.join(__dirname, '..', 'grpc_server', 'FileTransGRPCServer.exe');
// 配置项
const EXECUTION_OPTIONS = {
  windowsHide: true,
  detached: true, // 让子进程独立于父进程运行
};

let childProcess;
// 启动服务端程序
function startCPlusPlusService() {
  childProcess = spawn(CPLUSPLUS_EXECUTABLE_PATH, [], EXECUTION_OPTIONS, (error, stdout, stderr) => {
    if (error) {
      console.error(`Error starting C++ service: ${error}`);
      return;
    }
    console.log(`C++ service stdout: ${stdout}`);
    console.error(`C++ service stderr: ${stderr}`);
  });
}
// 关闭服务端程序
function stopCPlusPlusService() {
  console.log(childProcess.pid)
  /**
   * 注意:想要关闭启动的进程,必须使用 spawn 来创建子进程,
   *      使用 exec 创建子进程无法使用 kill 方法关闭子进程:
   *      因为 exec 创建子进程的实例实际上不是子进程,而是套壳进程,
   *      无法获取子进程的 pid,而是在子进程的外部套壳,获取的是套壳进程的 pid
   */
  /**
   * @param 1. childProcess.exitCode 标识子进程的退出代码。如果子进程仍在运行,则返回 null
   * @param 2. childProcess.kill() 通过 pid 来关闭子进程
   */
  if (childProcess.exitCode === null) {
    childProcess.kill();
    console.log('Attempting to gracefully shut down the C++ service...');
  } else {
    console.log('C++ service has already exited.');
  }
}
module.exports = { startCPlusPlusService, stopCPlusPlusService };

electorn桌面应用的客户端主进程中,

  1. 监听打开桌面应用的方法中调用该文件中的startCPlusPlusService方法来一并启动服务
  2. 监听桌面应用客户端关闭调用stopCPlusPlusService一并关闭服务

实现功能!