如何在低版本node运行高版本node子进程

栏目: 编程工具 · 发布时间: 6年前

内容简介:最近在使用 Node 的子进程模块实现一些功能,对相关知识进行了一个系统的学习总结,这篇文章将会简要介绍我总结的 Node 中和进程有关的内容。包括:四个创建子进程的函数、如何在node低版本运行高版本node子进程、进程以及信号量检测。有不当之处欢迎提出,一起交流。在 Node 中,大体上有三种创建进程的方法:
  • 如何在低版本node运行高版本node子进程
    • 前言:四个创建子进程的函数
    • exec / execFile

    • spawn

    • fork
    • 总结

    • 如何在node低版本运行高版本node子进程

    • nvm介绍

    • nvm好处
    • nvm具体原理
      • download资源管理
      • shell切换环境变量(3个)
    • 结合nvm切换构建子进程环境变量
      • 注入环境变量
    • 子进程函数选择
    • node -c运行生成代码

    • 进程和信号量

    • 用于优雅的检测fis grunt退出错误

最近在使用 Node 的子进程模块实现一些功能,对相关知识进行了一个系统的学习总结,这篇文章将会简要介绍我总结的 Node 中和进程有关的内容。包括:四个创建子进程的函数、如何在node低版本运行高版本node子进程、进程以及信号量检测。有不当之处欢迎提出,一起交流。

四个创建子进程的函数

在 Node 中,大体上有三种创建进程的方法:

  • exec / execFile
  • spawn
  • fork

exec / execFile

exec(command, options, callback)execFile(file, args, options, callback) 比较类似,会使用一个 Buffer 来存储进程执行后的标准输出结果,们可以一次性在 callback 里面获取到。不太适合输出数据量大的场景。

需要注意的是, exec 会首先创建一个新的 shell 进程出来,然后执行 commandexecFile 则是直接将可执行的 file 创建为新进程执行。所以, execfile 会比 exec 高效一些。

exec 比较适合用来执行 shell 命令,然后获取输出(比如: exec('ps aux | grep "node"') ),但是 execFile 却没办法这么用,因为它实际上只接受一个可执行的命令,然后执行(没法使用 shell 里面的管道之类的东西)。

// child.js
console.log('child argv: ', process.argv);
 
// parent.js
const child_process = require('child_process');
const p = child_process.exec(
  'node child.js a b', // 执行的命令
  {},
  (err, stdout, stderr) => {
    if (err) {
      // err.code 是进程退出时的 exit code,非 0 都被认为错误
      // err.signal 是结束进程时发送给它的信号值
      console.log('err:', err, err.code, err.signal);
    }
    console.log('stdout:', stdout);
    console.log('stderr:', stderr);
  }
);
console.log('child pid:', p.pid);
 
// parent.js
const p = child_process.execFile(
  'node', // 可执行文件
  ['child.js', 'a', 'b'], // 传递给命令的参数
  {},
  (err, stdout, stderr) => {
    if (err) {
      // err.code 是进程退出时的 exit code,非 0 都被认为错误
      // err.signal 是结束进程时发送给它的信号值
      console.log('err:', err, err.code, err.signal);
    }
    console.log('stdout:', stdout);
    console.log('stderr:', stderr);
  }
);
console.log('child pid:', p.pid);
 

两个方法还可以传递一些配置项,如下所示:

{
    // 可以指定命令在哪个目录执行
    'cwd': null,
    // 传递环境变量,node 脚本可以通过 process.env 获取到         
    'env': {},
    // 指定 stdout 输出的编码,默认用 utf8 编码为字符串(如果指定为 buffer,那 callback 的 stdout 参数将会是 Buffer)       
    'encoding': 'utf8',
    // 指定执行命令的 shell,默认是 /bin/sh(unix) 或者 cmd.exe(windows)
    'shell': '',
    // kill 进程时发送的信号量
    'killSignal': 'SIGTERM',
    // 子进程超时未执行完,向其发送 killSignal 指定的值来 kill 掉进程
    'timeout': 0,
    // stdout、stderr 允许的最大输出大小(以 byte 为单位),如果超过了,子进程将被 kill 掉(发送 killSignal 值)
    'maxBuffer': 200 * 1024,
    // 指定用户 id
    'uid': 0,
    // 指定组 id
    'gid': 0
}
 

这里可以看到,我们直接传入的是个回调函数,而不是返回steam对象,这样的我们获取的日志是个字符串,这里就会被限制字符串长度 导致进程提前退出。

spawn

spawn(command, args, options) 适合用在进程的输入、输出数据量比较大的情况(因为它支持以 stream 的使用方式),可以用于任何命令。

// child.js
console.log('child argv: ', process.argv);
process.stdin.pipe(process.stdout);
 
// parent.js
const p = child_process.spawn(
  'node', // 需要执行的命令
  ['child.js', 'a', 'b'], // 传递的参数
  {}
);
console.log('child pid:', p.pid);
p.on('exit', code => {
  console.log('exit:', code);
});
 
// 父进程的输入直接 pipe 给子进程(子进程可以通过 process.stdin 拿到)
process.stdin.pipe(p.stdin);
 
// 子进程的输出 pipe 给父进程的输出
p.stdout.pipe(process.stdout);
/* 或者通过监听 data 事件来获取结果
var all = '';
p.stdout.on('data', data => {
    all += data; 
});
p.stdout.on('close', code => {
    console.log('close:', code);
    console.log('data:', all);
});
*/
 
// 子进程的错误输出 pipe 给父进程的错误输出
p.stderr.pipe(process.stderr);
 

我们可以执行 cat bigdata.txt | node parent.js 来进行测试,bigdata.txt 文件的内容将被打印到终端。

spawn 方法的配置(options)如下:

{
    // 可以指定命令在哪个目录执行
    'cwd': null,
    // 传递环境变量,node 脚本可以通过 process.env 获取到         
    'env': {},
    // 配置子进程的 IO
    'stdio': 'pipe',
    // 为子进程独立运行做好准备
    'detached': false,
    // 指定用户 id
    'uid': 0,
    // 指定组 id
    'gid': 0
}
 

我们这里主要介绍下 detachedstdio 这两个配置。

stdio

stdio 用来配置子进程和父进程之间的 IO 通道,可以传递一个数组或者字符串。比如, ['pipe', 'pipe', 'pipe'] ,分别配置:标准输入、标准输出、标准错误。如果传递字符串,则三者将被配置成一样的值。我们简要介绍其中三个可以取的值:

  • pipe(默认):父子进程间建立 pipe 通道,可以通过 stream 的方式来操作 IO
  • inherit:子进程直接使用父进程的 IO
  • ignore:不建立 pipe 通道,不能 pipe、不能监听 data 事件、IO 全被忽略

比如上面的代码如果改写成下面这样,效果完全一样(子进程直接使用了父进程的 IO):

const p = child_process.spawn(
  'node', ['child.js', 'a', 'b'],
  {
    // 'stdio': ['inherit', 'inherit', 'inherit']
    'stdio': 'inherit'
  }
);
console.log('child pid:', p.pid);
 
p.on('exit', code => {
  console.log('exit:', code);
});
 

detached

detached 配置主要用来创建常驻的“后台”进程,比如下面的代码:

// child.js
setInterval(() => {
  console.log('child');
}, 1000);
 
// parent.js
const p = child_process.spawn(
  'node', ['child.js', 'a', 'b'],
  {
    'stdio': 'ignore', // 父子进程间不建立通道
    'detached': true   // 让子进程能在父进程退出后继续运行
  }
);
// 默认情况,父进程会等子进程,这个方法可以让子进程完全独立运行
p.unref();
 
console.log('child pid:', p.pid);
 
p.on('exit', code => {
  console.log('exit:', code);
});
 

这样就实现了常驻的后台进程,父进程退出了、shell 关掉了,子进程都会一直运行,直到手动将它 kill 掉。

fork

fork(modulePath, args, options) 实际上是 spawn 的一个“特例”,会创建一个新的 V8 实例,新创建的进程只能用来运行 Node 脚本,不能运行其他命令。并且会在父子进程间建立 IPC 通道,从而实现进程间通信。

// child.js
console.log('child argv: ', process.argv);
process.stdin.pipe(process.stdout);
 
// parent.js
const p = child_process.fork(
  'child.js', // 需要执行的脚本路径
  ['a', 'b'], // 传递的参数
  {}
);
console.log('child pid:', p.pid);
 
p.on('exit', code => {
  console.log('exit:', code);
});
 

上面代码的效果和使用 spawn 并配置 stdio: inherit 的效果是一致的。我们看下该方法的配置(options)就知道原因了:

{
    // 可以指定命令在哪个目录执行
    'cwd': null,
    // 传递环境变量,node 脚本可以通过 process.env 获取到         
    'env': {},
    // 创建子进程使用的 node 的执行路径(默认是:process.execPath)
    'execPath': '',
    // 创建子进程时,传递给执行程序的参数(默认是:process.execArgv)
    'execArgv': [],
    // 设置为 true 时,父子间将建立 IO 的 pipe 通道(pipie);设置为 false 时(默认),子进程直接使用父进程的 IO(inherit)
    'silent': false,
    // 指定用户 id
    'uid': 0,
    // 指定组 id
    'gid': 0
}
 

总结

  • exec / execFile:使用 Buffer 来存储进程的输出,可以在回调里面获取输出结果,不太适合数据量大的情况;可以执行任何命令;不创建 V8 实例
  • spawn:支持 stream 方式操作输入输出,适合数据量大的情况;可以执行任何命令;不创建 V8 实例;可以创建常驻的后台进程
  • fork:spawn 的一个特例;只能执行 Node 脚本;会创建一个 V8 实例;会建立父子进程的 IPC 通道,能够进行通信
  • spawn是最原始的创建子进程的函数,其他三个都是对spawn不同程度的封装

如何在node低版本运行高版本node子进程

nvm介绍

目前主流的node版本管理 工具 有两种,nvm和n。两者差异挺大的,具体分析可以参考一下淘宝FED团队的一篇文章:

管理 node 版本,选择 nvm 还是 n?

总的来说,nvm有点类似于 Python 的 virtualenv 或者 Ruby 的 rvm,每个node版本的模块都会被安装在各自版本的沙箱里面(因此切换版本后模块需重新安装)

使用nvm轻松切换node版本

在介绍使用方法前,简单说明一下nvm的工作原理:

按照官方安装方法之后,nvm会将各个版本的node安装在 ~/.nvm/versions/node 目录下,我们可以打开这个目录看看有些什么东西:

➜  ~  ls -a ~/.nvm/versions/node
.      ..     v4.1.0 v7.9.0
 

事实上 v4.1.0v5.5.0 这两个目录分别存放node的binary档,nvm会在 $PATH 前面安插指定版本的目录,透过这种方式在使用node命令时就会用指定版本的node来运行了。

可以确认实际的 $PATH 看看:

➜  ~  echo $PATH
/Users/***/.nvm/versions/node/v7.9.0/bin:...
 

.../v7.9.0/bin 这个就是我们当前使用的node版本(还可通过 nvm ls 命令查看当前已安装的所有node版本)。

所以它的实现原理就是在一个目录下存放多个版本的目录,在切换时候将相应的版本路径加入PATH中,从而实现版本的切换。

结合nvm切换构建子进程环境变量

那么我们如何结合nvm来进行在低版本node里边跑高版本node子进程

首先讲一下背景,node发展非常快,我们三年前的写的程序现在还跑在线上,但是还在添加新功能,有时候需要跑指定版本的node进行构建任务,而构建可能还需要跑多个shell命令,所以无法使用fork,只能用exec/swawn跑子进程。那么我们怎么吧进程的环境变量指向 我们所需的node版本,只需要修改传入的 process.env ,也就是子进程调用的参数 env 。直接运行以下代码,修改子进程的环境变量 NVM_BIN , NVM_PATH 等。

注入环境变量

//../libs/modify_nvm_env.js
var _ = require('lodash');
var env = process.env;
var obj = {
    MANPATH: env.MANPATH,
    NVM_BIN: env.NVM_BIN,
    PATH: env.PATH,
    NVM_PATH: env.NVM_PATH,
};
 
module.exports = function (version) {
    if (!/v(\d+\.)?(\d+\.)?(\*|\d+)/g.test(version)) {
        console.log('version number is unavailable, just use like v6.2.0')
        console.log(env);
        return env
    }
    var objNew = _.extend({}, obj);
    Object.keys(objNew).forEach(function (key) {
        objNew[key] = (objNew[key] || '').replace(/(node(\\|\/))v(\d+\.)?(\d+\.)?(\*|\d+)/g, '$1' + version);
    });
    console.log(objNew);
    objNew = _.extend({}, env, objNew);
    return objNew;
};
 
 
var modifyNvmEnv = require('../libs/modify_nvm_env');
var exec = require('child_process').exec;
 
exec('node -e "console.log(process.version)"', {
    // shell: '/root/.nvm/versions/node/v7.9.0/bin/node',
}, console.log);
var data = {
    nodeVersion:'v7.9.0'
}
var nodeVersion = data.nodeVersion;
var extraExecOption = {};
 
if (/v(\d+\.)?(\d+\.)?(\*|\d+)/g.test(nodeVersion)) {
    extraExecOption.env = modifyNvmEnv(nodeVersion)
}
 
const codeString = "console.log(process.version)";
exec("node -e '" + codeString + "'", extraExecOption, console.log)
 

运行子进程,输出 process.version 即为我们所需的node版本 V7.9.0

[root@TENCENT64 /data/frontend/install/alloydist_oa_com]# node experimental_code/exec_specify_node_version.js 
{ MANPATH: '/root/.nvm/versions/node/v7.9.0/share/man:/usr/local/share/man:/usr/share/man/overrides:/usr/share/man',
  NVM_BIN: '/root/.nvm/versions/node/v7.9.0/bin',
  PATH: '/root/.nvm/versions/node/v7.9.0/bin:/usr/local/bin/ruby:/usr/local/mongodb/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin',
  NVM_PATH: '/root/.nvm/versions/node/v7.9.0/lib/node' }
null 'v7.9.0\n' ''
null 'v0.12.14\n' ''
 

直接运行node代码

有时候,我们需要直接运行生成的高版本node代码,我们可以这样子,使用

node -e "your code"
 

node -存在的问题,你需要手动编码下代码字符串中的引号。

codeString.replace(/"/g, "'")
 

最后讲一下

进程及信号量

我们和进程通信,是通过发送一种叫做信号量的“消息”来告知进程某些事件发生了。一般会使用 kill [sid] [pid] 命令来发送信号量,一些常见的信号量如下:

kill [sid][pid] process.on(evt) 说明
kill -1 / kill -HUP process.on(‘SIGHUP’) 一般表示进程需要重新加载配置
kill -2 / kill -SIGINT / ctrl+c process.on(‘SIGINT’) 退出进程
kill -15 / kill -TERM process.on(‘SIGTERM’) 停止进程(kill 的默认信号)
kill -9 / kill -KILL 监听不到 kernel 直接停掉进程,并且不通知进程

实际上 process 还可以监听 exit 事件,监听 exit 事件和监听信号量事件是不一样的。 exit 事件只有在执行 process.exit() 或者进程结束时才会触发。

所以,一个“优雅”的进程一般会绑定 exitSIGINTSIGTERM 事件,在 exit 事件中处理进程的清理工作,然后在 SIGTERMSIGINT 事件中调用 process.exit() 来让进程真正退出。(如果你想耍流氓,可以绑定 SIGTERMSIGINT 事件,然后啥也不做,这样除非使用 kill -9 ,你的进程将永远不会退出……)

除了通过 kill 命令发送信号量,我们也可以使用子进程的 .kill(sig) 方法来发送信号,比如: p.kill('SIGINT') ;或者 processprocess.kill(pid, 'SIGINT')

那么我们的子进程如果报错了退出,我们也可以通过监听上述事件来做处理。

const exec = require('child_process').exec;
const ps = exec('node -h');
ps.on('SIGTERM', () => {
    console.log('parent catch SIGTERM')
});
ps.on('SIGINT', () => {
    console.log('parent catch SIGTERM')
});
ps.on('SIGKILL', () => {
    console.log('parent catch SIGTERM')
});
ps.on('message', console.log)
 
ps.on('exit',function (code, signal) {
    console.log('Child exited:', code, signal);
    if (arguments && arguments[1] == 'SIGTERM') {
        //子进程退出不正常,反馈给前台
        console.log(140);
    } else if (arguments && arguments[0] !== 0) {
        //子进程退出不正常,反馈给前台
        console.log(140);
    }
});
 

用于检测fis、grunt、以及各种自研程序退出错误,尤其是fis这种开了verbose也打不出什么有用信息的,grunt无论异常与否都是finish,输出信息根本判断不了是不是异常退出的奇葩。就不再需要我们繁琐的检测各种错误退出的日志可能。

总结

最后,我们总结下如何在node低版本运行高版本node子进程。

  • 四个创建子进程的函数
  • 结合nvm切换构建子进程环境变量
  • 进程和信号量,进程异常退出的检测好方法

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

高效团队开发

高效团队开发

[日] 池田尚史、[日] 藤仓和明、[日] 井上史彰 / 严圣逸 / 人民邮电出版社 / 2015-7 / 49.00

本书以团队开发中所必需的工具的导入方法和使用方法为核心,对团队开发的整体结构进行概括性的说明。内容涉及团队开发中发生的问题、版本管理系统、缺陷管理系统、持续集成、持续交付以及回归测试,并且对“为什么用那个工具”“为什么要这样使用”等开发现场常有的问题进行举例说明。 本书适合初次接手开发团队的项目经理,计划开始新项目的项目经理、Scrum Master,以及现有项目中返工、延期问题频发的开发人......一起来看看 《高效团队开发》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

MD5 加密
MD5 加密

MD5 加密工具

SHA 加密
SHA 加密

SHA 加密工具