Thirty seconds.
Long enough for a real encode of a short clip, short enough that a task
parked on a program that will never finish is eventually collected. A
command that genuinely takes longer says so with with_timeout_ms.
:= {
program : Str,
args : List(Str),
envs : List({ name : Str, value : Str }),
clear_envs : Bool,
working_dir : [Inherit, Set(Str)],
timeout_ms : U64,
stdout_limit_bytes : U64,
stderr_limit_bytes : U64,
}
Run an external program and read back what it produced.
A command is built and then run:
encode! : Str => Msg
encode! = |input| {
cmd = Cmd.new("ffmpeg")
.with_args(["-y", "-i", input, "out.mp4"])
.with_timeout_ms(120_000)
match io.commands().run!(cmd) {
Ok(output) if output.exit_code == 0 => Encoded("out.mp4")
Ok(output) => EncodeRefused(Str.from_utf8_lossy(output.stderr))
Err(err) => EncodeFailed(err)
}
}
run! waits, so it belongs inside Task.spawn!, where the frame loop keeps
drawing while the child runs. It is legal in init!, where it blocks
startup, and in tasks, where it parks the task; it is refused in update!
and render!.
A non-zero exit code is a completed run and arrives as Ok(output). Err
means the process did not produce an exit status, such as a spawn failure,
timeout, or output-limit violation.
No shell is involved. program names the executable and args are passed
verbatim; spaces, globs, pipes, and redirects are not interpreted.
A child inherits this process's user, working directory, and, unless
with_clear_envs is used, environment. The program must be declared with
Command, matched exactly as the app names it; a child then has
all of the user's authority, which no declaration can narrow.
Standard input is closed. Standard output and error are captured up to the configured limits rather than inherited.
Orderly shutdown terminates children still managed by the host. Detached descendants and forced process termination are outside this guarantee.
Thirty seconds.
Long enough for a real encode of a short clip, short enough that a task
parked on a program that will never finish is eventually collected. A
command that genuinely takes longer says so with with_timeout_ms.
Eight megabytes, applied to each captured stream on its own.
The same figure Http caps a response body at, for the same reason: it
holds a large text or JSON payload and not an accidental video.
A command to run program with no arguments.
Every bound is set here, so there is no way to build a command without
a deadline or without output limits. program is looked up on this
process's PATH when it contains no path separator, and used as a path
when it does.
Append one argument, exactly as the child will see it.
Append several arguments, in order.
Set one environment variable for the child.
A name that is empty, or that contains = or a NUL, cannot be set on
any operating system this platform runs on, and fails the run with
SpawnFailed rather than being dropped silently.
Set several environment variables for the child.
with_clear_envs : Cmd, Bool -> Cmd
Whether to drop this process's environment before applying with_env.
Bool.True gives the child only what the command names, which is what
a reproducible invocation of a tool wants. It does not remove the
child's other inheritances: the working directory, the user account and
the open standard streams are unchanged.
with_working_dir : Cmd, Str -> Cmd
Run the child in this directory instead of this process's own.
Only the child moves; nothing changes the working directory of the
application itself, so a path this app uses afterwards still resolves
the way every other path in it does. A relative program is still
resolved against this process's directory, not against this one.
with_timeout_ms : Cmd, U64 -> Cmd
How long to wait for the child before killing it.
The deadline covers the whole run, and 0 is normalized to 1: there
is no way to ask for no deadline, because a task parked forever on a
program that never exits is a leak the app cannot see.
with_stdout_limit : Cmd, U64 -> Cmd
The most standard output to capture, in bytes.
A child that writes more fails the run with StdoutLimitExceeded
rather than handing back a prefix. The host clamps this to sixty-four
megabytes, so asking for more than it will hold gets that figure and
the refusal arrives there instead.
with_stderr_limit : Cmd, U64 -> Cmd
The most standard error to capture, in bytes. StderrLimitExceeded
otherwise, on the same terms as with_stdout_limit.
What a child produced: its exit status and both captured streams.
exit_code is the status the operating system reported. On a child
killed by a signal it is the negated signal number, so a segmentation
fault is -11 rather than being confused with an ordinary exit; a
child killed at the deadline reports -1.
stdout and stderr are captured separately and neither is
interleaved into the other, so a program that writes progress to one
and data to the other stays readable. Both are ordinary Roc byte lists.
Utf8Output : {
exit_code : I64,
stdout : Str,
stderr : Str,
}
The same, decoded as text by run_utf8!.
CmdErr : [
CommandNotFound,
AccessRefused,
PermissionDenied,
SpawnFailed,
Busy,
Unavailable,
Timeout(Output),
StdoutLimitExceeded,
StderrLimitExceeded,
]
Why the command produced no exit status.
A non-zero exit status is not one of these; it is an Ok.
CommandNotFound is no such executable on PATH or at that path, and
AccessRefused is one that is there and the operating system will not
start. PermissionDenied is a program the app did not declare with
Command; nothing was started.
SpawnFailed is every other refusal to start the child or to run it to
its end, including a working directory that is not there and an
environment name this operating system cannot represent -- one that is
empty, or contains = or a NUL.
Timeout holds what the child had written before the
deadline killed it, because a program that hangs after printing why is
the ordinary case. StdoutLimitExceeded and StderrLimitExceeded are
refusals rather than truncations: half a stream decodes into wrong data
rather than into an error, so nothing is returned.
Busy is the host already running as many children as it will run at
once; nothing was started, and the same command run later can succeed.
Unavailable is the app shutting down while the child was running.
Cmd.Runner :: # (opaque)
Opaque process authority supplied by App.Io, scoped by the app's declared
Command entries.
for_host : Authority -> Runner
Private platform construction; no application can manufacture the argument.
Run the command and wait for the child to finish.
At most eight children run at once. The slot is taken before anything
is started and released when the child has been reaped, so a Busy
means precisely that no process was created. Bounding what is started
is what keeps a task per child from becoming a process per task.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
update! = |model, input, io| {
if input.devices.key_pressed(KeyE) {
Task.spawn!(
input,
|| match io.commands().run!(Cmd.new("git").with_args(["rev-parse", "HEAD"])) {
Ok(output) => Revision(Str.from_utf8_lossy(output.stdout))
Err(err) => RevisionFailed(err)
},
)
}
Ok(model)
}
Run the command and read both streams as text.
The decoding is lossy: bytes that are not valid UTF-8 become the
replacement character rather than failing the run, because a tool that
puts one stray byte in a progress line has still told the app what it
needed to know. Use run! when a stream has to survive exactly.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.