Forcola (forcola v0.1.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.

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.