process
import process from "process";
import io from "io";
import os from "os";
import time from "time";
// Build app.tape with the tape that runs this script, logging to a file
var build: process.Command = process.command(os.env("TAPE")!);
build.args(["build", "app.tape", "-o", "app"]);
build.stdout_to("build.log");
build.stderr_to_stdout();
let s: process.Status = build.run() or { return 1; };
if (!s.success()) { io.println("build {s.describe()}"); return 1; }
// Run something that may hang: give it 60 seconds
var vm: process.Command = process.command("qemu-system-x86_64");
vm.args(["-nographic", "-kernel", "kernel.elf"]);
vm.stdout_to("serial.log");
var child: process.Child = vm.spawn() or { return 1; };
let done: ?process.Status = child.wait_timeout(time.seconds(60)) or { return 1; };
if (done == null) {
child.kill() or {};
child.wait() or {};
io.println("timed out");
}process starts other programs and waits for them. It is a capability
module. There is no shell: the program gets exactly the arguments you give
it, and nothing is split, quoted or expanded on the way.
Commands
A Command describes what to start. Make one with command, then add to
it:
| Function | Does |
|---|---|
command(program) -> Command | A command for program: a path, or a bare name looked up on PATH |
c.arg(a) | Append one argument |
c.args(list) | Append several (c.args(["-o", "out"])) |
c.env(name, value) | Set an environment variable for the child only |
c.cwd(dir) | The child’s working directory |
c.stdin_from(path) / c.stdin_null() | Read stdin from a file / from nothing |
c.stdin_piped() | Read stdin from what this program writes to child.stdin() |
c.stdout_to(path) / c.stdout_append(path) / c.stdout_null() | Send stdout to a file (emptied first), to the end of a file, or nowhere |
c.stderr_to(path) / c.stderr_append(path) / c.stderr_null() | The same for stderr |
c.stderr_to_stdout() | stderr goes where stdout goes, like 2>&1 |
c.detached() | Share nothing of this program’s console (below) |
Every stream is inherited unless you say otherwise, so by default the
child prints to the same console. The child gets this program’s
environment plus your env settings, which also apply to the PATH that
program is looked up in.
A detached child gets nothing of this program’s console or terminal. On Windows it opens a console window of its own: when no stream is redirected its streams are that console’s, otherwise the ones not redirected go nowhere. On Linux and macOS it starts a session of its own, and the streams not redirected go nowhere.
A relative program path (./tool, bin/tool) is relative to this
program’s working directory even when cwd gives the child another one.
The builder functions don’t fail. Text containing a NUL byte, or an
environment name that is empty or contains =, makes spawn report
InvalidInput.
Starting and waiting
| Function | Does |
|---|---|
c.spawn() -> Child or io.IoError | Start it and return at once |
c.run() -> Status or io.IoError | Start it and wait for it |
run(program, args) -> Status or io.IoError | The same, everything inherited |
child.wait() -> Status or io.IoError | Wait until it ends |
child.wait_timeout(d) -> ?Status or io.IoError | Wait at most d; null if it’s still running |
child.try_wait() -> ?Status or io.IoError | Check without waiting |
child.kill() -> void or io.IoError | End it now |
child.close() | Stop tracking it without waiting |
child.stdin() -> io.Writer | Write to its stdin (stdin_piped) |
child.close_stdin() | Close its stdin: it reads the end of its input |
child.pid() -> i64 | Its process id |
spawn fails with NotFound when there is no such program (or no such
cwd or stdin_from file), PermissionDenied when it can’t be run,
NotADirectory when cwd is a file, and the errors of opening a
redirection file.
Once wait (or wait_timeout, try_wait) has seen the child end, the
status is kept and the child’s resources are released: waiting again
returns the same status. A child you start must be waited for or closed.
close leaves it running.
With stdin_piped, what you write to child.stdin() is the child’s
input; close_stdin() ends it. wait() and close() close it first, so
a child that reads to the end of its input doesn’t wait for you. A write
fails with BrokenPipe once the child has stopped reading:
var c = process.command("ffmpeg");
c.args(["-f", "gdigrab", "-i", "desktop", "out.mp4"]);
c.stdin_piped();
var rec: process.Child = c.spawn() or return;
// ... later: ask ffmpeg to stop, then wait for it
rec.stdin().write_all("q") or {};
let s: process.Status = rec.wait() or return;kill doesn’t wait: call wait afterwards for the status. Killing a child
that has already ended does nothing.
Status
pub tagged Status {
Exited { code: i64; }
Signaled { signal: i64; }
}| Function | Returns |
|---|---|
s.success() -> bool | Exited with code 0 |
s.code() -> ?i64 | The exit code, null when a signal ended the child |
s.describe() -> string | exited with code 3, killed by signal 9 |
match (s) {
process.Status.Exited { code } => { io.println("exit code {code}"); }
process.Status.Signaled { signal } => { io.println("signal {signal}"); }
}Exit codes are 0-255 on Linux and macOS and 32 bits on Windows.
Signaled happens only on Linux and macOS (kill gives signal 9); on
Windows a killed child has exited with code 1.
Platforms
- Windows takes one command line rather than a list of arguments.
processquotes each argument so that a program reading its command line the way the C runtime does (C and C++ programs, tape programs, most others) gets it back exactly. Batch files (.bat,.cmd) are refused withInvalidInput, becausecmd.exereads their arguments by other rules; runcmd.exeyourself if you need one. A bare program name is searched in tape’s (or your program’s) directory, the current directory, the system directories and thenPATH, with.exeadded. That is Windows’s own search, and it honours Windows’s switch: when the environment variableNoDefaultCurrentDirectoryInExePathis set, the current directory is skipped, ascmd.exeskips it, and a program in the current directory is run as./name. - Linux and macOS pass the arguments as they are and search
PATH.wait_timeoutnotices the child’s end at once on Linux 5.3 and newer; on macOS (and older Linux) it checks every few milliseconds, so it can return up to 10 ms late. - Uusi has no processes: everything reports
Unsupported.
Starting tape itself
tape run and tape test put the path of the running tape binary in the
environment variable TAPE, so scripts and tests start the same compiler
with process.command(os.env("TAPE")!). Capturing output works through
files: send it to a file with stdout_to, then read the file with
fs.read_file.
Last modified: