Skip to main content

Subprocess

Struct Subprocess 

Source
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: Duration

How 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: bool

Idempotency guard for close.

Implementations§

Source§

impl Subprocess

Source

pub async fn spawn(options: &Options) -> Result<Self, Error>

Spawn the claude binary with stream-json flags.

§Errors
Source

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.

Source

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.

Source

pub async fn read_line(&mut self) -> Result<Option<String>, Error>

Returns Ok(None) at end-of-stream. Cancel-safe.

§Errors

Error::Io on read failure.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Trait Implementations§

Source§

impl Debug for Subprocess

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more