226 lines
7.3 KiB
ReStructuredText
226 lines
7.3 KiB
ReStructuredText
.. _Subprocess:
|
|
|
|
=================
|
|
Subprocess Module
|
|
=================
|
|
|
|
The Subprocess module allows a caller to spawn a native host executable, and
|
|
communicate with it asynchronously over its standard input and output pipes.
|
|
|
|
Processes are launched asynchronously ``Subprocess.call`` method, based
|
|
on the properties of a single options object. The method returns a promise
|
|
which resolves, once the process has successfully launched, to a ``Process``
|
|
object, which can be used to communicate with and control the process.
|
|
|
|
A simple Hello World invocation, which writes a message to a process, reads it
|
|
back, logs it, and waits for the process to exit looks something like:
|
|
|
|
.. code-block:: javascript
|
|
|
|
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();
|
|
|
|
Input and Output Redirection
|
|
============================
|
|
|
|
Communication with the child process happens entirely via one-way pipes tied
|
|
to its standard input, standard output, and standard error file descriptors.
|
|
While standard input and output are always redirected to pipes, standard error
|
|
is inherited from the parent process by default. Standard error can, however,
|
|
optionally be either redirected to its own pipe or merged into the standard
|
|
output pipe.
|
|
|
|
The module is designed primarily for use with processes following a strict
|
|
IO protocol, with predictable message sizes. Its read operations, therefore,
|
|
either complete after reading the exact amount of data specified, or do not
|
|
complete at all. For cases where this is not desirable, ``read()`` and
|
|
``readString`` may be called without any length argument, and will return a
|
|
chunk of data of an arbitrary size.
|
|
|
|
|
|
Process and Pipe Lifecycles
|
|
===========================
|
|
|
|
Once the process exits, any buffered data from its output pipes may still be
|
|
read until the pipe is explicitly closed. Unless the pipe is explicitly
|
|
closed, however, any pending buffered data *must* be read from the pipe, or
|
|
the resources associated with the pipe will not be freed.
|
|
|
|
Beyond this, no explicit cleanup is required for either processes or their
|
|
pipes. So long as the caller ensures that the process exits, and there is no
|
|
pending input to be read on its ``stdout`` or ``stderr`` pipes, all resources
|
|
will be freed automatically.
|
|
|
|
The preferred way to ensure that a process exits is to close its input pipe
|
|
and wait for it to exit gracefully. Processes which haven't exited gracefully
|
|
by shutdown time, however, must be forcibly terminated:
|
|
|
|
.. code-block:: javascript
|
|
|
|
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();
|
|
|
|
In the simpler case of a short-running process which takes no input, and exits
|
|
immediately after producing output, it's generally enough to simply read its
|
|
output stream until EOF:
|
|
|
|
.. code-block:: javascript
|
|
|
|
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.
|
|
|
|
|
|
Bidirectional IO
|
|
================
|
|
|
|
When performing bidirectional IO, special care needs to be taken to avoid
|
|
deadlocks. While all IO operations in the Subprocess API are asynchronous,
|
|
careless ordering of operations can still lead to a state where both processes
|
|
are blocked on a read or write operation at the same time. For example,
|
|
|
|
.. code-block:: javascript
|
|
|
|
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);
|
|
|
|
The code attempts to write 1MB of data to an input pipe, and then read it back
|
|
from the output pipe. Because the data is big enough to fill both the input
|
|
and output pipe buffers, though, and because the code waits for the write
|
|
operation to complete before attempting any reads, the ``cat`` process will
|
|
block trying to write to its output indefinitely, and never finish reading the
|
|
data from its standard input.
|
|
|
|
In order to avoid the deadlock, we need to avoid blocking on the write
|
|
operation:
|
|
|
|
.. code-block:: javascript
|
|
|
|
let size = 1024 * 1024;
|
|
proc.stdin.write(new ArrayBuffer(size));
|
|
|
|
let result = await proc.stdout.read(size);
|
|
|
|
There is no silver bullet to avoiding deadlocks in this type of situation,
|
|
though. Any input operations that depend on output operations, or vice versa,
|
|
have the possibility of triggering deadlocks, and need to be thought out
|
|
carefully.
|
|
|
|
Arguments
|
|
=========
|
|
|
|
Arguments may be passed to the process in the form an array of strings.
|
|
Arguments are never split, or subjected to any sort of shell expansion, so the
|
|
target process will receive the exact arguments array as passed to
|
|
``Subprocess.call``. Argument 0 will always be the full path to the
|
|
executable, as passed via the ``command`` argument:
|
|
|
|
.. code-block:: javascript
|
|
|
|
let proc = await Subprocess.call({
|
|
command: "/bin/sh",
|
|
arguments: ["-c", "echo -n $0"],
|
|
});
|
|
|
|
let output = await proc.stdout.readString();
|
|
assert(output === "/bin/sh");
|
|
|
|
|
|
Process Environment
|
|
===================
|
|
|
|
By default, the process is launched with the same environment variables and
|
|
working directory as the parent process, but either can be changed if
|
|
necessary. The working directory may be changed simply by passing a
|
|
``workdir`` option:
|
|
|
|
.. code-block:: javascript
|
|
|
|
let proc = await Subprocess.call({
|
|
command: "/bin/pwd",
|
|
workdir: "/tmp",
|
|
});
|
|
|
|
let output = await proc.stdout.readString();
|
|
assert(output === "/tmp\n");
|
|
|
|
The process's environment variables can be changed using the ``environment``
|
|
and ``environmentAppend`` options. By default, passing an ``environment``
|
|
object replaces the process's entire environment with the properties in that
|
|
object:
|
|
|
|
.. code-block:: javascript
|
|
|
|
let proc = await Subprocess.call({
|
|
command: "/bin/pwd",
|
|
environment: {FOO: "BAR"},
|
|
});
|
|
|
|
let output = await proc.stdout.readString();
|
|
assert(output === "FOO=BAR\n");
|
|
|
|
In order to add variables to, or change variables from, the current set of
|
|
environment variables, the ``environmentAppend`` object must be passed in
|
|
addition:
|
|
|
|
.. code-block:: javascript
|
|
|
|
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"));
|