Forcola (forcola v0.2.0)

Copy Markdown View Source

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

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

run_error()

@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

run(argv, opts)

@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 (see Forcola.Result). A child that exits exactly at the timeout boundary can be reported as a timeout whose result carries the normal exit status, including status: 0.
  • :kill_grace_ms - SIGTERM-to-SIGKILL grace in milliseconds, default 5_000. Also accepted by Forcola.Stream.lines/2.
  • :cd - working directory.
  • :env - list of {name, value} strings.
  • :merge_stderr - route stderr into stdout; default false.
  • :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 :group overrides 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 :user it sets the gid (and clears supplementary groups to just that gid) without changing the uid; given with :user it 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}} where reason is 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.