Forcola.Duplex (forcola v0.1.0)

Copy Markdown View Source

A bidirectional stdin/stdout session with an external process.

For interactive CLIs driven over stdin (agent CLIs in stream-json mode). The owner writes lines in and receives lines out; the child runs in its own process group and dies with the session.

{:ok, session} = Forcola.Duplex.open(["claude", "--input-format", "stream-json"], [])
:ok = Forcola.Duplex.send_line(session, json)
receive do
  {:forcola_line, ^session, line} -> line
end
:ok = Forcola.Duplex.close(session)

Messages

The process that called open/2 (the owner) receives:

  • {:forcola_line, session, line} - a stdout line, without its trailing newline. A partial line held across frames is delivered once its newline arrives; a final partial line is delivered before the exit message.
  • {:forcola_stderr, session, line} - a stderr line, unless merge_stderr: true routed stderr into :forcola_line.
  • {:forcola_exit, session, status} - the child exited on its own; status is the exit code, {:signal, n} for death by signal, {:spawn_error, reason} if it never started, or :shim_exited if the shim died without reporting. The session is over; close/1 is not required (but is harmless).

Kill discipline

close/1 kills the child's process group (SIGTERM, then SIGKILL after the kill grace) and blocks until the shim confirms the group is dead. The session monitors its owner: owner death takes the same path. If the session process itself is killed brutally, or the whole BEAM dies, the port closes, the shim sees stdin EOF, and the group is killed anyway.

For CLIs that exit when their stdin closes, send_eof/1 closes the child's stdin without killing anything; the child's own exit then arrives as a :forcola_exit message.

Summary

Types

An open duplex session.

Functions

Returns a specification to start this module under a supervisor.

Close the session and kill the child's process group.

Open a duplex session running argv; the caller becomes the owner.

Close the child's stdin without killing the group.

Write a line to the child's stdin.

Types

session()

@opaque session()

An open duplex session.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

close(duplex)

@spec close(session()) :: :ok

Close the session and kill the child's process group.

Blocks until the shim confirms the group is dead. Idempotent: closing a session that is already over returns :ok.

open(argv, opts)

@spec open(
  [String.t(), ...],
  keyword()
) :: {:ok, session()} | {:error, term()}

Open a duplex session running argv; the caller becomes the owner.

Options

  • :cd, :env, :merge_stderr - as in Forcola.run/2.
  • :kill_grace_ms - SIGTERM-to-SIGKILL grace, default 5_000.

There is no :timeout_ms; the session is bounded by its owner process and close/1. Passing :timeout_ms raises ArgumentError.

A spawn failure (e.g. a missing binary) is asynchronous: open/2 still returns {:ok, session} and the failure arrives as {:forcola_exit, session, {:spawn_error, reason}}.

send_eof(duplex)

@spec send_eof(session()) :: :ok | {:error, term()}

Close the child's stdin without killing the group.

For CLIs that finish and exit when their input ends; the child's exit then arrives as a :forcola_exit message. Returns {:error, :closed} if the session is already over.

send_line(duplex, line)

@spec send_line(session(), iodata()) :: :ok | {:error, term()}

Write a line to the child's stdin.

A newline is appended. Returns {:error, :closed} once the session is over or the child's stdin has been closed with send_eof/1.