尽管发明 Node.js 的初衷主要是为了编写Web 服务器,但开发人员又发现了其他适用(和不适用!)Node 的用途。令人觉得惊喜的是,这些用途中有一个是编写shell 脚本。并且那确实有意义:Node 的跨平台支持已经相当好了,既然前端和后端都用JavaScript 写了,如果构建系统也用JavaScript 写不是更好吗,对吧?
异步对shell 脚本的坏处
在这一用途上值得称道的库是 Grunt ,它是构建在 ShellJS 之上的。然而 ShellJS 有一块硬骨头要啃:Node 迫使它用异步 I/O。尽管对于 Web 服务器来说异步 I/O 很棒,因为它必须随时做出响应,但对于需要逐步执行的 shell 脚本来说,异步 I/O 意义不大。
所以,ShellJS 的作者们发现了一个“有趣的”解决办法,让它可以运行一个 shell 命令,然后等着命令完成。大致上是下面这样的代码:
var child_process = require('child_process'); var fs = require('fs'); function execSync(command) { // 在子 shell 中运行命令 child_process.exec(command + ' 2>&1 1>output && echo done! > done'); // 阻塞事件循环,知道命令执行完 while (!fs.existsSync('done')) { // 什么都不做 } // 读取输出 var output = fs.readFileSync('output'); // 删除临时文件。 fs.unlinkSync('output'); fs.unlinkSync('done'); return output; }
换句话说,在 shell 执行你的命令时,ShellJS 依然在运行,并持续不断地轮询着文件系统,检查是否能找到表明命令已经完成的那个文件。有点儿像驴子。
这种效率低下又丑陋不堪的解决办法让 Node 核心团队受刺激了,实现了一个真正的解决方案 – Node v0.12 最终应该会支持同步运行子进程。实际上这个特性已经在路线图上放了很长时间了– 我记得是在2011 年的 JSConf.eu 上 (!) ,跟现在已经退休的 Node 维护者 Felix Geisendoerfer 坐在一起,勾勒出了一个实现 execSync 的办法。在过了两年多以后,这一特性现在终于出现在了 master 分支上。
恭喜,ShellJS 的人们挑了一个很好的刺儿! :)
同步对 shell 脚本的好处
我们刚加上的 API spawnSync 跟它的异步小伙伴类似,它提供的底层 API 让你可以完全掌控子进程的设置。它还会返回所有我们能够收集的信息:退出码、终止信号、可能的启动错误,以及这个进程的全部输出。当然,在流中使用 spawnSync 没有任何意义 - 它是同步的,所以事件处理器不能在进程退出前运行 - 所以进程的所有输出会被缓冲到一个单例字符串或缓冲对象中。
并且就像众所周知的 exec(运行 shell 命令)和 execFile(用于运行一个可执行文件) 方法一样,我们为常见的情况添加了 execSync 和 execFileSync,它们比 spawnSync 更易用。如果你用了这些 API,Node 会假定你关心的只是进程写到 stdout 中的数据。如果进程或 shell 返回了非零的退出码,node 会认为出现错误了,exec(Sync) 会抛出。
比如获取项目 git 历史的代码就像下面这样简单:
var history = child_process.execSync('git log', { encoding: 'utf8' }); process.stdout.write(history);
现在你可能在想“怎么要用这么长时间?”从表面上看,启动一个子进程并读取它的输出看起来简直是小菜一碟。也确实是这样 - 如果你只关心非常常见的情况。但是,我们不想做出来的解决方案只是一半。
当需要同时发送输入并读取一或多个输出流时,有两个选择:用线程 - 或者用事件循环。比如 Python 的实现,我们发现他们或者用事件循环(在 Unix 系的平台上)或者用线程(在 Windows 上)。并且它的实现可真不是一碟小菜。
2011 年我们就意识到 Node 已经有一个非常棒的事件循环库了,即 libuv 。理论上已经具备了实现这一特性的所有条件。然而总是有或大或小的问题,让它并不能真正可靠地工作。
比如说,当子进程退出时,kernel 会给 node 发送一个 SIGCHLD 信号通知它,但当有多个事件循环存在时,有很长一段时间 libuv 都不能正确处理信号。还有,删除事件循环并且不留下堆栈跟踪的能力也是最近才加上的。之前 Node 根本不管,它只是在某点退出,然后让 OS 打扫战场。如果我们需要一个临时的事件循环,并且在不需要它后仍然继续运行,这种策略就不太合适了。
慢慢的,随着时间的推移,所有这些问题都被解决了。所以如果你现在再设法看看过去那些缓冲区管理、参数解析、超时处理等诸如此类的东西,你会发现这个特性的核心只是一个事件循环,带子进程、计时器,还有一堆附着在它上面的管道。
如果你不关心它都是如何运作的,只需要看看文档,让node 为控制子进程提供的丰富选项震你一下吧。现在谁愿意去把ShellJS 修好?:)
作者简介
本文最初由 Bert Belder 发表在 StrongLoop 上。Bert Belder 从 2010 年就开始做 Node.js 了,并且他还是 libuv 的主要编写者之一,Node.js 就是在这个库上构建的。他除了是 StrongLoop 和 Node 核心的技术领导者,他正在做的特性还会让 Node 处于创新的最前沿,甚至是在 1.0 版出来之后。 StrongLoop 降低了在 Node 中开发APIs 的难度,还添加了监测、集群化以及私有注册的支持等DevOps 能力。
查看英文原文: What’s New in Node.js v0.12 – execSync: a Synchronous API for Child Processes 2014 年 3 月 12 日
评论