jsernews 1.0.1

<~>

ts 221 days ago.

原文链接:https://blog.safia.rocks/post/169346741925/node-module-deep-dive-childprocess

你好,朋友!

是的!我带来了另一篇深入 Node 模块。就像我在上一篇文章中提到的那样,我希望能够在本月的其余时间发布 Node 中的部分代码解析。我希望在星期一,星期三和星期五发布它们,我为保持这一承诺感到非常自豪。

现在是阅读(并剖析)一些代码的时候了!这篇文章,我开始于问自己一个非常基本的问题。使用 child_process.exec 执行命令时会发生什么?对于那些可能不熟悉的人,child_process.exec 是一个让您能够从 Node 执行 shell 命令的功能。你可以做这样的事情。

> const { exec } = require('child_process');
undefined
> exec('echo "Hello there!"', (error, stdout, stderr) => {
... if (error) console.log(error);
... console.log(`${stdout}`);
... console.log(`${stderr}`);
... });
> Hello there!

漂亮整洁,是吧?我想是这样。当我构建 giddy(一个小小的 Node CLI,它为 git 增加了一些有用的功能)的时候,我使用了这个函数很多次。

如往常一样,我转向了 GitHub 上的 Node.js 仓库,并浏览到 child_process 的源文件。在最近几篇文章中,我通过检查模块的导出来开始读取代码。这一次,我对查看什么代码有了一个不错的主意,所以我直奔在模块上定义的 exec 函数。

exports.exec = function(command /*, options, callback*/) {
  var opts = normalizeExecArgs.apply(null, arguments);
  return exports.execFile(opts.file,
                          opts.options,
                          opts.callback);
};

我认为这很有趣,尽管 exec 命令可以接受三个参数(要执行的 command,可选的 options 以及调用的 callback),但它只设置了一个参数。看来要提取这三个参数,将在 arguments 对象上调用 normalizeExecArgs 函数。然后,normalizeExecArgsarguments 对象中传递的每个字段提取到一个对象,并打上适合的标签。

function normalizeExecArgs(command, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }

  // Make a shallow copy so we don't clobber the user's options object.
  options = Object.assign({}, options);
  options.shell = typeof options.shell === 'string' ? options.shell : true;

  return {
    file: command,
    options: options,
    callback: callback
  };
}

我认为这是一件很奇怪的事情。在代码库的其他部分,这种类型的检查 — 用来正确匹配一个函数被调用时是否只有 commandscallback 但没有 options 或是只有 commandsoptions 但没有 callback 等等 — 通常在函数内部完成。 在这里,它似乎已经委托给一个外部工具函数。这个函数(normalizeExecArgs)被调用两次,一次在 exec 中,一次在 execSync 中,可能将处理逻辑提取出来,以使代码保持 DRY。 无论如何,当这些都完事之后,现在看来我们已经有了一个对象 opts,它包含我们想要执行的命令,相关的选项以及调用的回调。

exec 函数将这些选项传递给 execFile 函数......这可是一段多达193行的代码!没关系。我是一个勇敢的女人,我已经完成了七次这样的代码解析,所以我肯定可以处理这个。你准备好了吗?好吧,我们走吧。

execFile 函数的开头几行似乎在做一些基本的选项设置和更多的 arguments 解析。此刻,我对于为什么需要再次解析传递的位置参数感到有点困惑,因为它们刚刚在 exec 函数中被解析。这是不寻常的,但我不会让它耽搁睡眠,然后我们继续...

所以这时,我们得到 —

哦,等等!停下!我突然意识到为什么在 execFile 中有一组多余的参数解析逻辑。虽然 execFile 仅由 exec 函数在 child_process 模块​​内部调用,但它是一个可能由开发人员调用的导出函数。因此,函数需要解析开发人员提供的参数。

所以在此时,我们得到一个选项对象和一个回调函数来调用。接下来的几行将验证开发人员提供的选项并对其进行 santize。

// Validate the timeout, if present.
validateTimeout(options.timeout);

// Validate maxBuffer, if present.
validateMaxBuffer(options.maxBuffer);

options.killSignal = sanitizeKillSignal(options.killSignal);

下一行用给定的参数调用 spawn

var child = spawn(file, args, {
  cwd: options.cwd,
  env: options.env,
  gid: options.gid,
  uid: options.uid,
  shell: options.shell,
  windowsHide: !!options.windowsHide,
  windowsVerbatimArguments: !!options.windowsVerbatimArguments
});

spawn 是一个灵活的小函数,它创建一个新的 ChildProcess 对象并使用传递给它的参数调用它的 spawn 方法。

也许我会在某个时候阅读 ChildProcess 对象的代码。暂时它不在我的阅读列表中,如果您有兴趣看到这篇文章,请在 Twitter 上告诉我。

var spawn = exports.spawn = function(/*file, args, options*/) {
  var opts = normalizeSpawnArguments.apply(null, arguments);
  var options = opts.options;
  var child = new ChildProcess();

  debug('spawn', opts.args, options);

  child.spawn({
    file: opts.file,
    args: opts.args,
    cwd: options.cwd,
    windowsHide: !!options.windowsHide,
    windowsVerbatimArguments: !!options.windowsVerbatimArguments,
    detached: !!options.detached,
    envPairs: opts.envPairs,
    stdio: options.stdio,
    uid: options.uid,
    gid: options.gid
  });

  return child;
};

一旦创建了这个 ChildProcess 对象,execFile 函数体的其余部分主要负责在新的 ChildProcess 对象上配置事件处理程序。例如,将一个退出处理程序附加到子进程以监听退出事件,处理程序内部会调用传递给 execFile 函数的回调函数。它还附带一个错误处理程序,它根据开发人员在 options 参数中提供的编码正确编码 stderr

child.addListener('close', exithandler);
child.addListener('error', errorhandler);

总而言之,child_process 模块​​中的 exec 函数是 execFile 函数的一个包装,后者继而依赖 child_process 模块​​中的 spawn 函数完成的一些工作,而 spawn 函数则依赖于 ChildProcess 对象中实现的 spawn 逻辑。像洋葱一般层层剥落下来并不像我想象的那么糟糕。