Skip to main content

ExecHandle

Struct ExecHandle 

Source
pub struct ExecHandle { /* private fields */ }
Expand description

One exec, addressed by its caller-minted id.

The id is the idempotency key, so a handle survives a process restart: rebuild it with the same id and every method still addresses the same server-side exec.

Implementations§

Source§

impl ExecHandle

Source

pub fn exec_id(&self) -> &str

Source

pub async fn poll(&self) -> Result<ExecResult, Error>

Reads current status and output. Read-only server-side; safe to spin on.

Source

pub async fn wait(&self, timeout: Duration) -> Result<ExecResult, Error>

Polls until the exec is done, or fails with a client-side timeout.

A timeout here has not touched the exec — polling is read-only and the output lives until it is acked — so a caller that gives up can come back and poll again, and the message says so.

A retryable error mid-wait is swallowed rather than raised: a VM under load drops a connection occasionally, and the whole point of a read-only poll is that repeating it costs nothing. A fatal one ends the wait.

Source

pub fn stream(&self) -> impl Stream<Item = Result<ExecEvent, Error>>

Yields output as it arrives, reconnecting at the last good offset.

Source

pub fn stream_with( &self, options: StreamOptions, ) -> impl Stream<Item = Result<ExecEvent, Error>>

Self::stream with the knobs. See StreamOptions.

Written as a generator over an explicit state machine rather than as a hand-rolled Stream impl: the reconnect logic is a loop with an await in the middle of it, and expressing that as a poll_next would mean storing the in-flight attach as a pinned field — which is where a self-referential-future bug lives.

Source

pub async fn for_each_event<F>( &self, options: StreamOptions, on_event: F, ) -> Result<StreamEnd, Error>
where F: FnMut(ExecEvent) -> ControlFlow<()>,

Drives a stream to its end, handing every event to on_event.

The same state machine Self::stream_with runs, driven by an ordinary loop instead of wrapped in a Stream. Everything about the wire behaviour is identical — same reconnects, same cursor, same terminal-event rule — and both share Self::advance, so there is one implementation of the property and two ways to consume it.

§Why a callback exists beside the Stream

Because Stream is not in std. A consumer of Self::stream_with has to name the crate that defines the trait in order to call poll_next, so a client that wants to print a line per event acquires a dependency to advance a loop — microvms-cli carried futures-util for exactly that and nothing else, and tests/thinness.rs (CLI-2) asserts that crate’s dependency set exactly. This method is that dependency’s replacement: ControlFlow and async fn are both std, so a caller needs nothing but this crate.

A synchronous callback is the right shape for a caller whose per-event work is synchronous — the CLI writes a line and returns. A caller whose per-event work is an await (both bindings: a bounded-channel send) wants Self::for_each_event_async, which is the same loop with an awaited callback. Using this one there would mean blocking_send on a runtime worker, which is why the overload exists.

§What the return value says that a tally cannot

StreamEnd::reason distinguishes an exit event from a cut body from a caller that stopped reading. A caller counting events cannot: a cut stream and a finished one differ only in the terminal event, which is the argument for SSE framing here in the first place. StreamEnd::cursor is the state machine’s own offset, so a caller resuming does not maintain a second one.

An error ends the drive with Err and the events already handed over stay handed over. That asymmetry is deliberate: the bytes a callback already wrote are real output the caller received, and there is nothing to unwind them with.

Source

pub async fn for_each_event_async<F, Fut>( &self, options: StreamOptions, on_event: F, ) -> Result<StreamEnd, Error>
where F: FnMut(ExecEvent) -> Fut, Fut: Future<Output = ControlFlow<()>>,

Self::for_each_event for a callback that awaits.

The same loop and the same Self::advance state machine; the only difference is that the per-event callback answers a future, which is .awaited before the next step is taken. So the events are still delivered strictly in order and still one at a time — a callback that awaits between events holds the drive rather than racing it, which is what makes this usable as backpressure.

§Why the overload exists, and what it closed

Both bindings consume a stream by pushing into a capacity-1 mpsc channel that a foreign-language iterator drains (bindings/microvms-py/src/exec.rs, bindings/microvms-js/src/exec.rs). The channel bound is deliberate — the daemon’s SSE body is the backpressure signal — and it means the channel is full whenever the host-language consumer is even slightly behind, i.e. normally. With Self::for_each_event the only available send is blocking_send, which would block the very runtime worker the driver is running on; so both bindings kept the Stream path, and futures-util with it, for a reason about backpressure rather than about the trait. send(..).await inside an AsyncFnMut yields the worker instead, and both bindings dropped the dependency on 2026-08-09.

§FnMut(ExecEvent) -> Fut rather than AsyncFnMut

Measured, not stylistic, and the measurement is the reason the signature looks older than the edition. AsyncFnMut is the obvious spelling and it compiles here — but a caller cannot tokio::spawn a drive that uses it. Proving the returned future Send requires naming F::CallRefFuture<'a> under a for<'a> bound, which is the unstable async_fn_traits feature; without it the spawn fails with “Send would have to be implemented for the type &ExecHandle… but Send is actually implemented for &'0 ExecHandle, for some specific lifetime”. Both bindings spawn this drive onto a runtime, so that is not a corner they can avoid, and the unstable bound additionally forces the callback’s captures to 'static.

So Fut is a plain type parameter. The cost is that the callback’s future cannot borrow from the closure’s captures — it has no lifetime to name them with — so a caller sending on a channel clones the sender per event rather than holding &sender. For a tokio::sync::mpsc::Sender that is one atomic increment, which is the right price for a signature that can cross a spawn.

The return value and the error asymmetry are Self::for_each_event’s, unchanged.

Source

pub async fn write_stdin( &self, data: &[u8], eof: bool, ) -> Result<StdinResponse, Error>

Writes to the child’s stdin.

Requires the exec to have been started with stdin: true, or the daemon answers 409. eof in the same call is the common case for feeding a prompt: two round trips would leave a window where the child has the bytes but not the EOF that tells it the input is complete.

Source

pub async fn close_stdin(&self) -> Result<StdinResponse, Error>

Sends EOF.

Nothing else closes stdin: the daemon’s copy of the pipe outlives Child::wait(), so a child blocked reading stdin hangs until its timeout unless someone calls this.

Source

pub async fn ack(&self) -> Result<ExecResult, Error>

Releases the buffered output and starts the TTL clock.

409 means either the exec has not exited — output is still being written — or an earlier ack already took it. Both are real states, not the same state, and the daemon’s detail string distinguishes them.

Source

pub async fn kill(&self) -> Result<bool, Error>

Signals the whole process group, not just the direct child.

Returns whether anything was signalled. false means no pgid was ever captured, i.e. the child had already been reaped — which is the outcome a kill wanted, and is why it is a 200 rather than an error.

Source

pub async fn wait_and_ack(&self, timeout: Duration) -> Result<ExecResult, Error>

Waits, then acks, returning the result that carries the output.

Which result is returned matters: the ack response carries the released output, and a poll issued after the ack reports acked with no output at all. Returning the wrong one is a silent empty-output bug, so the sequencing lives here rather than at every call site.

Source§

impl ExecHandle

Source

pub fn split( self: Arc<ExecHandle>, options: StreamOptions, policy: GapPolicy, ) -> Split

This exec’s output as two byte channels over one drive. See the module docs.

options is how the drive reads the stream (offset, reconnects, idle timeout), except error_on_gap, which policy replaces. A drive error (a spent reconnect budget, a refused reconnect) ends both channels with that error, so a truncated stream never reads as a finished one.

Trait Implementations§

Source§

impl Debug for ExecHandle

Source§

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

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.

Source§

impl<T> Instrument for T

Source§

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

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

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> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<Unshared, Shared> IntoShared<Shared> for Unshared
where Shared: FromUnshared<Unshared>,

Source§

fn into_shared(self) -> Shared

Creates a shared type from an unshared type.
Source§

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

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
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.
Source§

impl<T> WithSubscriber for T

Source§

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
Source§

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

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