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
impl ExecHandle
pub fn exec_id(&self) -> &str
Sourcepub async fn poll(&self) -> Result<ExecResult, Error>
pub async fn poll(&self) -> Result<ExecResult, Error>
Reads current status and output. Read-only server-side; safe to spin on.
Sourcepub async fn wait(&self, timeout: Duration) -> Result<ExecResult, Error>
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.
Sourcepub fn stream(&self) -> impl Stream<Item = Result<ExecEvent, Error>>
pub fn stream(&self) -> impl Stream<Item = Result<ExecEvent, Error>>
Yields output as it arrives, reconnecting at the last good offset.
Sourcepub fn stream_with(
&self,
options: StreamOptions,
) -> impl Stream<Item = Result<ExecEvent, Error>>
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.
Sourcepub async fn for_each_event<F>(
&self,
options: StreamOptions,
on_event: F,
) -> Result<StreamEnd, Error>
pub async fn for_each_event<F>( &self, options: StreamOptions, on_event: F, ) -> Result<StreamEnd, Error>
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.
Sourcepub async fn for_each_event_async<F, Fut>(
&self,
options: StreamOptions,
on_event: F,
) -> Result<StreamEnd, Error>
pub async fn for_each_event_async<F, Fut>( &self, options: StreamOptions, on_event: F, ) -> Result<StreamEnd, Error>
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.
Sourcepub async fn write_stdin(
&self,
data: &[u8],
eof: bool,
) -> Result<StdinResponse, Error>
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.
Sourcepub async fn close_stdin(&self) -> Result<StdinResponse, Error>
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.
Sourcepub async fn ack(&self) -> Result<ExecResult, Error>
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.
Sourcepub async fn kill(&self) -> Result<bool, Error>
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.
Sourcepub async fn wait_and_ack(&self, timeout: Duration) -> Result<ExecResult, Error>
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
impl ExecHandle
Sourcepub fn split(
self: Arc<ExecHandle>,
options: StreamOptions,
policy: GapPolicy,
) -> Split
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§
Auto Trait Implementations§
impl !RefUnwindSafe for ExecHandle
impl !UnwindSafe for ExecHandle
impl Freeze for ExecHandle
impl Send for ExecHandle
impl Sync for ExecHandle
impl Unpin for ExecHandle
impl UnsafeUnpin for ExecHandle
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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