记录electron动态启动后端服务过程

618 阅读2分钟

故事背景:

使用【electron】开发的软件S中集成了一个新的功能A;新功能需要一个【单独的后端服务BackA】支持。评审决定BackA的启动需要纳入【权限系统】中。也就是说BackA需要根据网络请求的结果【动态启动】;本文记录了实现此需求的过程,以及实现过程中踩的坑。

 

痛点问题:

1. electron中如何发送【网络请求】

2. 如何启动或者关闭一个后端服务

3. 当软件【异常退出】的时候,动态启动的BackA如何关闭

4. BackA的【信息】如何保存

 

解决方案:

1. 针对electron发送网络请求的问题,讨论之后最终决定使用axios实现。

2. 使用nodejs中内置的child_process实现进程的动态启停

3. 将BackA作为软件S的子进程运行,这样当S异常退出的时候BackA也能自行关闭;此外在动态启动BackA之前根据BackA的进程名获取进程号,然后逐个关闭。

4. 建立独立的日志系统保存BackA服务信息,用到了winston库。

 

相关代码:

1. axios实现网络请求


const axios = require('axios');

    axios({

        method: 'get',

        url: backAUrl,

    })

        .then((data) => {...})

2. 使用child_process库中的spawn方法和execSync方法完成对服务的动态启停:

辅助函数:


// 获取backA的启动程序名称

const getBackA = () => 'BackA.exe';

 

// 获取backA启动程序目录

const getBackADir = () =>

    process.env.NODE_ENV === 'development'

        ? '***'

        : path.resolve(__dirname, '***');

 

// 获取backA的启动位置

const getBackAPath = () => {

    return path.resolve(getBackADir(), getBackA());

};

启动服务:

try {
    const { spawn } = require('child_process');
    
    // 开启子进程
    const childProcess = spawn(getBackAPath(), []);

    // 保存此子进程的pid
    backaId = childProcess.pid;

    childProcess.stdout.on('data', (data) => {
        // 这里的vLogger是记录backA信息的日志对象
        vLogger.log('info', `子进程输出: ${data}`);
    });



    // 处理错误
    childProcess.on('error', (error) => {
        console.error('error', `子进程错误: ${error}`);
        throw new Error('launch voice assistant failed');
    });


    // // 断线通知
    // childProcess.on('close', (code) => {
    //     console.log('info', `子进程退出,退出码: ${code}`);
    // });
} catch (e: unknown) {
    console.log('error', 'launch backA failed');
}

关闭服务:

const exec = require('child_process').execSync;

try {
    // 这里的backaId是backA进程的pid
    exec(`taskkill /F /PID ${backaId}`);

} catch (e: unknown) {
    console.log('Error:删除backA进程失败!');
}

3. 启动backA之前关闭已经挂住的服务

const processName = getBackA ();
const command = 'wmic process where "name=\'' + processName + '\'" get ProcessId /VALUE';
try {
    const output = exec(command).toString('utf8');
    const pidLine = output
        .trim()
        .split('=')
        .filter((v: string) => v !== 'ProcessId');
    if (pidLine?.forEach) {
        pidLine.forEach((pid: string) => exec(`taskkill /F /PID ${parseInt(pid, 10)}`));
    }
} catch (e: unknown) {
    return;
}

4. 记录backA服务的日志

const { createLogger, transports, format } = require('winston');
const vLoggerName = path.resolve(
    getBackADir(),
    'Log',
    getBackA() + '_' + new Date().toLocaleString().replace(/[:/ ]/g, '_') + '_runtime.log',
);

const vLogger = createLogger({
    level: 'info', // 设置日志输出级别
    format: format.combine(
        format.timestamp(), // 添加时间戳
        format.simple(), // 使用简单的文本格式输出日志信息
    ),
    transports: [
        // new transports.Console(), // 输出到控制台
        new transports.File({
            // 输出到文件
            filename: vLoggerName,
        }),
    ],
});

执行时机:

1. 是否动态启动backA的请求R在app.whenReady()的回调中执行;

2. 动态启动backA服务在R的回调中执行;

3. 清除已经挂住的backA服务在启动backA之前进行,同样也是在R的回调中进行;

4. 关闭backA服务在主进程退出之前进行;

 

欢迎各位大佬批评指正~