The Process facade runs other programs: a build tool, git, an image
converter, a script. It captures what the program writes, ends it when it
runs too long, cleans up everything it started, runs several side by side,
and fakes them all in tests. It is Laravel's Process facade with the
command and the shell line kept apart.
use Process;
let status = command
.path
.run
.await?;
if status.successful
Commands and shell lines
Process::command takes the program and its arguments. Each argument
reaches the program as it is, through no shell, so a value from a form or
a file name with spaces is never split or run:
use Process;
// `name` might hold "x; rm -rf ~": it is still one file name to convert.
command.run.await?;
Process::shell takes a command line and runs it through the system
shell, sh -c (cmd /C on Windows), the way Laravel runs a string
command. Pipes, &&, redirects, $VAR and globs work:
use Process;
shell.run.await?;
Never build a shell line from input you do not control: the shell runs
whatever the line holds. Use Process::command for that.
Results
run waits for the program and returns a ProcessResult:
| Method | Returns |
|---|---|
successful() / failed() |
whether the exit code is 0 |
exit_code() |
Some(code), or None when a signal ended it |
output() / error_output() |
everything written to standard output and standard error, as text |
output_bytes() / error_output_bytes() |
the same, byte for byte, for output that is not text |
see_in_output(text) / see_in_error_output(text) |
whether the output contains text |
command() |
the command line that ran |
A nonzero exit is a result that reports failure, not an error. Call
throw() to turn a failed result into a ProcessError::Failed that
carries the exit code and both outputs, so ? stops on it:
let built = command.run.await?.throw?;
run returns an error only when the program did not give a result: it
could not be started (ProcessError::NotStarted, which names the
program), or it was killed for a timeout.
Options
Every option takes the builder and returns it:
| Option | Effect |
|---|---|
path(dir) |
the working directory |
env(key, value) |
adds a variable to the environment the program inherits |
input(bytes) |
writes to standard input, then closes it; without it, standard input is empty |
timeout(duration) |
the longest it may run; 60 seconds unless set, and zero means none, as in Laravel |
forever() |
no timeout |
idle_timeout(duration) |
the longest it may go without writing output |
quietly() |
keeps no output: it is read and thrown away, and no output callback is called |
tty() |
hands the program this terminal, for a program that talks to the user; nothing is captured, and an idle timeout is refused, since nothing can watch the terminal. Process::supports_tty() says whether there is a terminal |
run_with(callback) calls the callback with each chunk of output as it
arrives, marked OutputKind::Out or OutputKind::Err:
use ;
command
.run_with
.await?;
Timeouts and cleanup
A program that runs past its timeout is killed and run returns
ProcessError::TimedOut, naming the command and the timeout. One that
writes nothing for its idle timeout is killed the same way, with
ProcessError::IdleTimedOut. Both errors carry the output written before
the kill.
A kill reaches everything the program started: a script that started
background jobs takes them with it. On Unix each program gets a process
group of its own, and the group is killed. A tty() program stays in the
terminal's group, which it must share to read the terminal, so its
descendants are found in the process table and killed one by one. On
Windows taskkill /T ends the tree. The same cleanup happens when the
future of run is dropped before it completes, as tokio::time::timeout
or a select! drops it, and when a started process is dropped. A program
that exits by itself is waited on until its output closes, and nothing it
left behind is killed.
A child in a group of its own does not get the SIGINT a terminal sends
on Ctrl-C. The server and the workers end their children when they shut
down; a console command that Ctrl-C kills outright leaves its children
running unless it handles the signal, for instance with
tokio::signal::ctrl_c() in a select! beside the run.
Started processes
start returns the program running, as an InvokedProcess:
use Duration;
use ;
let mut worker = command
.forever
.start?;
println!;
while worker.running
let result = worker.wait.await?;
output() and error_output() return everything so far, and
latest_output() and latest_error_output() what came since the last
call. signal(Signal::Term) signals the program itself; stop(grace)
sends a terminate signal to it and everything it started, then a kill to
whatever is left after grace, and returns the result.
wait_until(|kind, chunk| ...) waits until the callback returns true for
a chunk of output. The timeouts hold for a started process whether or not
anything waits on it: a watchdog kills it at its timeout, and wait then
returns the timeout error. Call start inside a Tokio runtime: the output
is read by tasks of its own.
Pools
Process::pool() runs processes side by side. Add each under a key, or
push it to be keyed by its position:
use Process;
let results = pool
.add
.add
.push
.concurrency
.run
.await;
if !results.successful
results.get(key) returns that process's result, or its error when it
could not run or was killed for its timeout. A failure stops nothing else.
A pushed process is keyed by its position, or the next free number when a
key already holds it, and adding a key twice replaces the first process.
With concurrency(n) at most n run at once and the rest wait for a
slot; without it every process starts at once. start() returns the pool
running, as an InvokedPool, with running, signal, stop and
wait; each running() call starts waiting processes in the slots that
freed, so a pool can be polled to the end.
Pipes
Process::pipe() runs processes in order, each with the previous one's
output as its input, and returns the last result:
use Process;
let sorted = pipe
.push
.push
.run
.await?;
The first process that fails ends the pipe and its result comes back; the
processes after it do not run. The output passes byte for byte, so a pipe
can carry an archive or an image. Each process runs to its end before the
next starts, as in Laravel. For a streaming pipe, use Process::shell
with |.
Testing
Process::fake() stops every process from running while its guard lives.
A command whose command line matches a pattern gets that result; any other
gets an empty successful one:
use Process;
let fake = fake;
fake.when;
fake.when;
deploy.await?; // runs git and npm
fake.assert_ran;
fake.assert_ran_times;
fake.assert_not_ran;
The command line is the arguments joined by spaces, or the shell line as
given, and * matches any run of characters; the first matching pattern
wins. fake.prevent_stray_processes() turns an unmatched command into
ProcessError::Stray.
Process::describe() builds a process line by line, for a started process
that reports itself running for a number of running() calls:
fake.when;
.id(n) sets the process id a described process reports, and
.replace_output(text) and .replace_error_output(text) set all of its
lines at once. A faked started process records the signals sent to it, for
has_received_signal(Signal::Term).
Process::sequence([...]) answers its results in turn, one a run, and
.push(result) adds one at the end. A run after the last is
ProcessError::FakeExhausted, unless .dont_fail_when_empty() makes it
an empty success or .when_empty(result) gives the result to answer.
The assertions are assert_ran, assert_ran_with(|process| ...),
assert_ran_times, assert_ran_in_order, assert_not_ran (and
assert_didnt_run) and assert_nothing_ran; recorded() returns every
faked process with its command line, working directory, environment,
input and result. Runs, starts, pools and pipes are all faked and
recorded.
The fake is process-global, like Storage::fake, and its guard
serializes the tests that take one. A test that runs real processes at
the same time as a faked test would be faked too, so keep real-process
tests out of a binary whose tests fake, or mark them #[serial].
Why Suprnova diverges
- The command and the shell line are two methods. Laravel's
runtakes either a string, run through a shell, or an array, run as it is, and the difference is easy to miss when a value is interpolated.commandandshellmake the choice visible. - A kill reaches everything the program started. Laravel kills the program; a background job it started keeps running.
- Pools take a concurrency limit. Laravel starts every process in a pool at once.
- A started process's timeout holds without a wait. Laravel checks it only while something waits on the process.
- The fake is a guard.
Process::fake()returns the fake, and the assertions are its methods, so a test cannot assert against a fake it did not install.
Next
- Queues - running the work in the background instead
- Task Scheduling - running a command on a schedule
- Testing - the other fakes
