子进程模块

子进程模块允许调用方生成一个本地主机可执行文件,并通过其标准输入和输出管道异步地与其通信。

进程是异步启动的,使用 Subprocess.call 方法,基于单个选项对象的属性。该方法返回一个 Promise,一旦进程成功启动,就会解析为一个 Process 对象,该对象可用于与进程通信和控制进程。

一个简单的 Hello World 调用,它向一个进程写入消息,读取它,记录它,并等待进程退出,看起来像这样

let proc = await Subprocess.call({
  command: "/bin/cat",
});

proc.stdin.write("Hello World!");

let result = await proc.stdout.readString();
console.log(result);

proc.stdin.close();
let {exitCode} = await proc.wait();

输入和输出重定向

与子进程的通信完全通过与其标准输入、标准输出和标准错误文件描述符绑定的单向管道进行。虽然标准输入和输出始终重定向到管道,但默认情况下,标准错误继承自父进程。但是,标准错误可以选择重定向到其自己的管道或合并到标准输出管道中。

该模块主要设计用于与遵循严格 IO 协议且消息大小可预测的进程一起使用。因此,其读取操作要么在读取指定的确切数据量后完成,要么根本不完成。对于不需要这种情况的情况,可以不带任何长度参数调用 read()readString,它们将返回任意大小的数据块。

进程和管道生命周期

进程退出后,其输出管道中的任何缓冲数据仍然可以读取,直到管道被显式关闭。但是,除非管道被显式关闭,否则必须从管道读取任何挂起的缓冲数据,否则与管道关联的资源将不会被释放。

除此之外,进程或其管道不需要显式清理。只要调用方确保进程退出,并且其 stdoutstderr 管道上没有待读取的输入,所有资源都将自动释放。

确保进程退出的首选方法是关闭其输入管道并等待其正常退出。但是,在关闭时间之前尚未正常退出的进程必须强制终止。

let proc = await Subprocess.call({
  command: "/usr/bin/subprocess.py",
});

// Kill the process if it hasn't gracefully exited by shutdown time.
let blocker = () => proc.kill();

AsyncShutdown.profileBeforeChange.addBlocker(
  "Subprocess: Killing hung process",
  blocker);

proc.wait().then(() => {
  // Remove the shutdown blocker once we've exited.
  AsyncShutdown.profileBeforeChange.removeBlocker(blocker);

  // Close standard output, in case there's any buffered data we haven't read.
  proc.stdout.close();
});

// Send a message to the process, and close stdin, so the process knows to
// exit.
proc.stdin.write(message);
proc.stdin.close();

在更简单的短运行进程(不接受输入,并在生成输出后立即退出)的情况下,通常只需简单地读取其输出流直到 EOF 即可。

let proc = await Subprocess.call({
  command: await Subprocess.pathSearch("ifconfig"),
});

// Read all of the process output.
let result = "";
let string;
while ((string = await proc.stdout.readString())) {
  result += string;
}
console.log(result);

// The output pipe is closed and no buffered data remains to be read.
// This means the process has exited, and no further cleanup is necessary.

双向 IO

执行双向 IO 时,需要特别注意避免死锁。虽然 Subprocess API 中的所有 IO 操作都是异步的,但操作的粗心排序仍然可能导致两种进程同时阻塞在读或写操作上的状态。例如,

let proc = await Subprocess.call({
  command: "/bin/cat",
});

let size = 1024 * 1024;
await proc.stdin.write(new ArrayBuffer(size));

let result = await proc.stdout.read(size);

代码尝试向输入管道写入 1MB 的数据,然后从输出管道读取它。但是,由于数据足够大,可以填充输入和输出管道缓冲区,并且由于代码在尝试任何读取操作之前等待写入操作完成,因此 cat 进程将无限期地阻塞,试图写入其输出,并且永远不会完成从其标准输入读取数据。

为了避免死锁,我们需要避免阻塞写入操作。

let size = 1024 * 1024;
proc.stdin.write(new ArrayBuffer(size));

let result = await proc.stdout.read(size);

但是,对于这种情况,没有避免死锁的万灵丹。任何依赖于输出操作的输入操作,反之亦然,都有可能触发死锁,需要仔细考虑。

参数

可以以字符串数组的形式将参数传递给进程。参数永远不会被拆分,也不会受到任何 shell 展开的影响,因此目标进程将接收传递给 Subprocess.call 的完全相同的参数数组。参数 0 将始终是可执行文件的完整路径,通过 command 参数传递。

let proc = await Subprocess.call({
  command: "/bin/sh",
  arguments: ["-c", "echo -n $0"],
});

let output = await proc.stdout.readString();
assert(output === "/bin/sh");

进程环境

默认情况下,进程使用与父进程相同的环境变量和工作目录启动,但如有必要,可以更改两者。可以通过简单地传递 workdir 选项来更改工作目录。

let proc = await Subprocess.call({
  command: "/bin/pwd",
  workdir: "/tmp",
});

let output = await proc.stdout.readString();
assert(output === "/tmp\n");

可以使用 environmentenvironmentAppend 选项更改进程的环境变量。默认情况下,传递 environment 对象会将进程的整个环境替换为该对象中的属性。

let proc = await Subprocess.call({
  command: "/bin/pwd",
  environment: {FOO: "BAR"},
});

let output = await proc.stdout.readString();
assert(output === "FOO=BAR\n");

为了向当前的环境变量集中添加变量或更改变量,必须另外传递 environmentAppend 对象。

let proc = await Subprocess.call({
  command: "/bin/pwd",
  environment: {FOO: "BAR"},
  environmentAppend: true,
});

let output = "";
while ((string = await proc.stdout.readString())) {
  output += string;
}

assert(output.includes("FOO=BAR\n"));