pub struct Subprocess {
writer_tx: UnboundedSender<WriterCmd>,
write_bound: Duration,
reader_rx: UnboundedReceiver<Result<Option<String>, Error>>,
writer_task: Option<JoinHandle<()>>,
stderr_task: Option<JoinHandle<String>>,
child: Option<Child>,
pgid: Option<u32>,
session: Arc<RwLock<String>>,
closed: bool,
}Expand description
A live claude subprocess driven over channels.
Spawned via Subprocess::spawn. Every read goes through the
reader task → mpsc; every write goes through the writer task ←
mpsc. Drop hits kill_on_drop cleanup; close
gives a graceful exit path with a 5s timeout before SIGKILL.
Fields§
§writer_tx: UnboundedSender<WriterCmd>Outbound channel into the writer task. Cloned by
clone_writer so external
dispatchers can write without contending on &mut self.
write_bound: DurationHow long one write to stdin may take. The shipping bound is
STDIN_WRITE_BOUND; a test injects a shorter one so it can watch
the bound fire without waiting thirty seconds out.
A caller waits twice this (Subprocess::ack_wait), because the
task’s verdict is the authoritative one for a line it dequeued: a
writer that started a line and timed out reports it before the caller
gives up, so no line is written after its caller was told it failed.
A line still queued behind two slow successful writes can outlast
even that wait; its caller is told the wait’s own bound, not the
writer’s.
reader_rx: UnboundedReceiver<Result<Option<String>, Error>>Inbound channel from the reader task. Single-consumer.
writer_task: Option<JoinHandle<()>>Writer task handle. close aborts this so the
child sees stdin EOF promptly even when external writer clones
still hold writer_tx clones - without the abort the writer
task would wait for every clone to drop and the child wouldn’t
see EOF on stdin until then.
stderr_task: Option<JoinHandle<String>>Stderr drain task - best-effort logged. Joined during
close, which takes its returned tail for
Error::Process.
child: Option<Child>The child handle. close waits on it (with
timeout) and SIGKILLs on hang.
pgid: Option<u32>The child’s process group, when this transport put it in one of
its own. None means no group was claimed and teardown must not
signal one.
session: Arc<RwLock<String>>The claude session this child carries, as the container the reader
and the dispatcher fill. Shared, not copied: /new swaps the
occupant without swapping the process, so a label read at spawn names
a session the record is not about - and the long-lived session is
exactly where a stall happens.
closed: boolIdempotency guard for close.
Implementations§
Source§impl Subprocess
impl Subprocess
Sourcepub async fn spawn(options: &Options) -> Result<Self, Error>
pub async fn spawn(options: &Options) -> Result<Self, Error>
Spawn the claude binary with stream-json flags.
§Errors
Error::CliNotFoundwhen the binary isn’t on PATH or the given path doesn’t exist.Error::CwdNotFoundwhencwdnames a directory that isn’t there.Error::Iofor other spawn failures.
Sourcepub fn child_pid(&self) -> Option<u32>
pub fn child_pid(&self) -> Option<u32>
Read one line (without the trailing \n) from the subprocess stdout.
Operating-system PID of the spawned claude child, when one is
still attached. Returns None after close or
when the child has already been reaped.
Used by the Inspector pane’s PROCESSES section to anchor an
OS-level walk of the descendant tree (the forge-agent
crate’s env::processes scanner - not linked here because the
dependency direction forbids forge-sdk from naming forge-agent
types). The PID is stable for the lifetime of the subprocess
so the scanner can cache its snapshot across polls keyed off
this value.
Sourcepub(crate) fn session_id(&self) -> Arc<RwLock<String>> ⓘ
pub(crate) fn session_id(&self) -> Arc<RwLock<String>> ⓘ
The session this child carries, as the container the reader and the
dispatcher fill: a consumer that records a problem about this child
reads it at the failure rather than at the spawn, because /new swaps
the occupant.
Sourcefn ack_wait(&self) -> Duration
fn ack_wait(&self) -> Duration
How long a caller waits for a write’s ack: twice the writer’s own bound, so the task’s verdict lands first for a line it dequeued and no line is written after its caller was told it failed.
Sourcepub async fn write_line(&mut self, line: &str) -> Result<(), Error>
pub async fn write_line(&mut self, line: &str) -> Result<(), Error>
Write one line of stream-json to the subprocess stdin.
The caller is responsible for including the trailing \n - this
matches the contract of
codec::encode_user_prompt.
§Errors
Error::Io on write failure, including writes issued after
end_input or close.
Sourcepub async fn end_input(&mut self) -> Result<(), Error>
pub async fn end_input(&mut self) -> Result<(), Error>
Close the subprocess’s stdin. Safe to call multiple times.
Subsequent write_line calls will fail
with a BrokenPipe Io error.
§Errors
Error::Io on flush failure.
Sourcepub(crate) fn clone_writer(&self) -> Arc<SharedWriter> ⓘ
pub(crate) fn clone_writer(&self) -> Arc<SharedWriter> ⓘ
Hand out a clonable SharedWriter backed by the same writer
task. Multiple clones can write concurrently; the writer task
serialises onto the child’s stdin in arrival order. Used by
detached control-request dispatch so a slow callback can’t
block the reader / command loop.
Sourcepub async fn close(&mut self) -> Result<(), Error>
pub async fn close(&mut self) -> Result<(), Error>
Graceful shutdown: close stdin, wait for the subprocess to exit (5s timeout, SIGKILL fallback), SIGKILL whatever it left in its process group, drain the stderr task. Idempotent - subsequent calls are no-ops.
§Errors
Error::Process when the subprocess exits non-zero or hits
the SIGKILL fallback; Error::Io on wait failure.
Sourceasync fn join_stderr_tail(&mut self) -> String
async fn join_stderr_tail(&mut self) -> String
Join the stderr drain task and take its retained tail. Errors are best-effort - it’s a logging sink, so a dead task costs the tail rather than the close.