Leak-free external process execution.
Every command runs under a small Rust shim (a port program, not a NIF)
that places the child in its own process group via setsid and kills
the whole group, SIGTERM then SIGKILL, when the run times out or the
BEAM dies. The shim detects BEAM death as stdin EOF, so cleanup happens
even on kill -9 of the VM.
Why not System.cmd in a Task?
Task.shutdown closes the Erlang port, and closing a port closes pipes;
it sends no signal. The external process runs on until it next touches a
closed pipe, and its children are never signaled at all. See the process
groups guide for the full mechanism.
Modes
Forcola.run/2- bounded one-shot run, mandatory timeoutForcola.Stream- line-by-line output as anEnumerableForcola.Daemon- long-running server under a supervision treeForcola.Duplex- bidirectional stdin/stdout session
Maintainers of CLI wrapper libraries who want to offer Forcola-backed execution without a mandatory dependency: see the adoption guide.
Summary
Types
Errors a bounded run can return.
Functions
Run argv ([binary | args]) to completion under the shim.
Types
@type run_error() :: {:timeout, Forcola.Result.t()} | {:spawn, term()}
Errors a bounded run can return.
The spawn reason is one of :shim_not_found, a reason string reported
by the shim, or {:shim_exited, %Forcola.Result{}}; see run/2.
Functions
@spec run( [String.t(), ...], keyword() ) :: {:ok, Forcola.Result.t()} | {:error, run_error()}
Run argv ([binary | args]) to completion under the shim.
Options
:timeout_ms- required. On expiry the child's process group is killed (SIGTERM, then SIGKILL after the kill grace) and{:error, {:timeout, partial_result}}is returned with output captured so far. The group is confirmed dead before the call returns, with one exception: if the shim itself never reports back, an Elixir-side backstop returns a result whose status is{:signal, :unconfirmed}, meaning death was not confirmed (seeForcola.Result). A child that exits exactly at the timeout boundary can be reported as a timeout whose result carries the normal exit status, includingstatus: 0.:kill_grace_ms- SIGTERM-to-SIGKILL grace in milliseconds, default5_000. Also accepted byForcola.Stream.lines/2.:cd- working directory.:env- list of{name, value}strings.:merge_stderr- route stderr into stdout; defaultfalse.:user- run the child as this user, a string username or an integer uid. The user's primary gid and supplementary groups are taken from the passwd/group database unless:groupoverrides the gid. See "Running as a different user".:group- run the child with this group as its primary gid, a string group name or an integer gid. Given without:userit sets the gid (and clears supplementary groups to just that gid) without changing the uid; given with:userit overrides the user's primary gid.
Running as a different user
:user/:group make the shim drop privileges (setgroups, then
setgid, then setuid, in that order) in the child before exec. This
is POSIX-only, a one-way drop, and requires the shim process itself to
run with enough privilege to drop: root, or CAP_SETUID/CAP_SETGID
on Linux. Requesting the user the shim already runs as is a no-op and
always succeeds.
It fails closed. If the user or group cannot be resolved, or the shim
lacks the privilege to drop, the child is never executed and the call
returns the mode's normal spawn error ({:error, {:spawn, reason}} for
run/2). The command never runs as the shim's own (possibly
privileged) user when a different user was requested. Names are
resolved in the parent before fork; only the numeric syscalls run in
the child.
Any exit status is {:ok, %Forcola.Result{}}; callers branch on
:status. A non-zero exit is a result, not an error.
Spawn errors
{:error, {:spawn, :shim_not_found}}- no shim binary exists for this target (neither downloaded nor built).{:error, {:spawn, reason}}wherereasonis a string - the shim reported the spawn failure, for example a missing or non-executable command.{:error, {:spawn, {:shim_exited, %Forcola.Result{}}}}- the shim exited without reporting an exit or error, for example because it was SIGKILLed. The result's status is{:signal, :unconfirmed}. A SIGKILLed shim gets no chance to kill the group, so the child may survive, reparented to pid 1.