arcbox_computer_runtime/agent.rs
1//! The guest-agent port: how the runtime reaches the agent inside a Computer.
2//!
3//! Everything above this seam — the boot readiness gate, exec, files, clock
4//! sync, the post-restore network reconfiguration — speaks [`GuestAgent`]
5//! and [`GuestFiles`]. Nothing above it knows that today's only
6//! implementation, [`vm_proto`], reaches the guest over a vsock connection
7//! the driver port hands out. A Computer that is not reachable that way —
8//! no vsock device at all, or an agent that answers on its own network
9//! address — supplies another factory through
10//! [`NodeEnvironment::agent`](crate::environment::NodeEnvironment::agent).
11//!
12//! Readiness is the part that differs most between transports, so it has a
13//! vocabulary of its own: [`Readiness`] says *how* a guest announces that
14//! it is serving, [`GuestAgentFactory::arm_readiness`] arms the observer
15//! before the guest starts, and [`ReadyGate::wait`] is what the boot flow
16//! waits on afterwards under its own deadline.
17
18use std::sync::Arc;
19use std::time::Duration;
20
21use arcbox_vm_driver::net::NetworkIdentity;
22use arcbox_vm_driver::{PreparedVm, VmHandle};
23use async_trait::async_trait;
24use tokio::sync::mpsc;
25
26use crate::boot_proto::NetReconfigCommand;
27use crate::error::Result;
28use crate::file_proto::{FileStatDto, FsEventDto};
29
30pub mod vm_proto;
31
32pub use vm_proto::{VmProtoAgent, VmProtoAgentFactory};
33
34/// The command vocabulary the guest agent starts processes from, shared
35/// with `vm-agent` through `arcbox-vm-proto`.
36pub use arcbox_vm_proto::exec::StartCommand;
37
38/// The write half of an interactive session: stdin bytes, TTY resizes,
39/// signals, and the EOF that closes the process's stdin.
40pub type ExecInput = mpsc::Sender<ExecInputMsg>;
41
42/// Builds the agent client for one Computer, and knows how that Computer's
43/// guest announces it is serving.
44#[async_trait]
45pub trait GuestAgentFactory: Send + Sync {
46 /// How a guest built by this factory announces readiness.
47 fn readiness(&self) -> Readiness;
48
49 /// Arm the readiness observer **before the guest starts**.
50 ///
51 /// [`Readiness::DialOut`] has no other option: the guest dials the host
52 /// the moment it is serving, and a VMM forwards that connect only if
53 /// someone is already listening — otherwise the guest is reset and the
54 /// one readiness event is lost. The other shapes bind nothing here and
55 /// do their work in [`ReadyGate::wait`].
56 async fn arm_readiness(&self, prepared: &dyn PreparedVm) -> Result<Box<dyn ReadyGate>>;
57
58 /// The agent client for a VM that has booted or restored.
59 ///
60 /// `net` is what the guest was told over its interface, for an agent
61 /// reached at that address rather than through the VM handle. `None`
62 /// when the Computer is networkless — or when it is networked but no
63 /// address names it yet, as a legacy-addressed restore is until its
64 /// re-address RPC lands and it is reconnected with the settled one.
65 fn connect(
66 &self,
67 handle: Arc<dyn VmHandle>,
68 net: Option<&NetworkIdentity>,
69 ) -> Result<Arc<dyn GuestAgent>>;
70}
71
72/// How a guest announces that its agent is serving.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum Readiness {
75 /// The guest dials the host on `port` once it is serving. The host must
76 /// be listening before the guest starts, so the gate is armed pre-boot
77 /// and the wait is a single `accept`. This is [`vm_proto`]'s answer.
78 DialOut { port: u32 },
79 /// The host dials the guest's `port` until the agent accepts. Nothing
80 /// to arm — the wait needs the running VM.
81 Poll { port: u32 },
82 /// No transport-level announcement: the gate is one agent round trip
83 /// ([`ProbeGate`]), because a Computer that cannot announce itself
84 /// still must not be called ready before it answers.
85 Probe,
86}
87
88/// An armed readiness observer, awaited once.
89#[async_trait]
90pub trait ReadyGate: Send {
91 /// Wait for the guest agent to be serving.
92 ///
93 /// Both arguments describe the VM that booted *after* this gate was
94 /// armed, which is why they arrive here rather than at arming time: a
95 /// [`Readiness::DialOut`] gate uses neither (its observer has been
96 /// listening since before the guest started), [`Readiness::Poll`]
97 /// dials the guest over `handle`'s transport, and [`Readiness::Probe`]
98 /// round-trips `agent`. The caller owns the deadline.
99 async fn wait(&mut self, handle: &Arc<dyn VmHandle>, agent: &Arc<dyn GuestAgent>)
100 -> Result<()>;
101}
102
103/// [`Readiness::Probe`]: one agent round trip, which is the whole of what
104/// this shape can observe.
105///
106/// [`GuestAgent::sync_clock`] is that round trip — its `Ok` means the agent
107/// accepted a connection, parsed a frame and replied — and it is what the
108/// cold-boot path used as its readiness gate before the guest learned to
109/// dial out. Its side effect is one the boot wants anyway.
110///
111/// Transport-independent, so every factory that answers
112/// [`Readiness::Probe`] returns this rather than its own.
113pub struct ProbeGate;
114
115#[async_trait]
116impl ReadyGate for ProbeGate {
117 async fn wait(
118 &mut self,
119 _handle: &Arc<dyn VmHandle>,
120 agent: &Arc<dyn GuestAgent>,
121 ) -> Result<()> {
122 agent.sync_clock().await.map(drop)
123 }
124}
125
126/// The agent inside one running Computer.
127#[async_trait]
128pub trait GuestAgent: Send + Sync {
129 /// Run a command and stream its output. The final [`OutputChunk`] is
130 /// always [`OutputChunk::Exit`].
131 async fn run(&self, start: StartCommand) -> Result<mpsc::Receiver<Result<OutputChunk>>>;
132
133 /// Start an interactive session: input sink, output stream.
134 async fn exec(
135 &self,
136 start: StartCommand,
137 ) -> Result<(ExecInput, mpsc::Receiver<Result<OutputChunk>>)>;
138
139 /// Set the guest wall clock to the host's now.
140 ///
141 /// `Err` is a failed round trip; [`ClockSync::AgentError`] is an agent
142 /// that answered but could not set the clock, which still proves
143 /// liveness.
144 async fn sync_clock(&self) -> Result<ClockSync>;
145
146 /// Re-address the guest after a restore onto a fresh network.
147 async fn reconfigure_network(&self, cmd: &NetReconfigCommand) -> Result<()>;
148
149 /// Wait until the guest's own listen table has a listener on `port` —
150 /// never a connect probe, which would perturb the workload.
151 async fn wait_for_port(&self, port: u16, timeout: Duration) -> Result<PortWait>;
152
153 /// The file channel into this Computer.
154 fn files(&self) -> &dyn GuestFiles;
155}
156
157/// File operations inside one running Computer.
158#[async_trait]
159pub trait GuestFiles: Send + Sync {
160 /// Read a whole file.
161 async fn read(&self, path: &str) -> Result<Vec<u8>>;
162 /// Write a whole file, creating it with `mode`.
163 async fn write(&self, path: &str, mode: u32, data: &[u8]) -> Result<()>;
164 /// Stat one path; symlinks are reported, not followed.
165 async fn stat(&self, path: &str) -> Result<FileStatDto>;
166 /// List a directory, non-recursively, with full per-entry metadata.
167 async fn list(&self, path: &str) -> Result<Vec<FileStatDto>>;
168 /// Create a directory and any missing parents; succeeds if it exists.
169 async fn make_dir(&self, path: &str, mode: u32) -> Result<()>;
170 /// Remove a file, symlink or directory; a non-empty directory needs
171 /// `recursive`.
172 async fn remove(&self, path: &str, recursive: bool) -> Result<()>;
173 /// Rename an entry within the Computer.
174 async fn rename(&self, from: &str, to: &str) -> Result<()>;
175 /// Open a directory watch. The stream ends cleanly when the Computer
176 /// stops; dropping it cancels the watch.
177 async fn watch(&self, path: &str, recursive: bool) -> Result<DirWatch>;
178}
179
180/// A live directory watch inside one Computer.
181///
182/// Dropping it cancels the watch — for the vm-proto client that is the
183/// connection close the agent tears its inotify watch down on.
184pub struct DirWatch(Box<dyn FsEvents>);
185
186impl DirWatch {
187 /// Wrap an implementation's event stream. This is what makes
188 /// [`GuestFiles::watch`] implementable off this crate's transport.
189 #[must_use]
190 pub fn new(events: Box<dyn FsEvents>) -> Self {
191 Self(events)
192 }
193
194 /// Next filesystem event. `Ok(None)` is the clean end of the stream —
195 /// the guest side closed it because the Computer stopped.
196 pub async fn next_event(&mut self) -> Result<Option<FsEventDto>> {
197 self.0.next_event().await
198 }
199}
200
201impl std::fmt::Debug for DirWatch {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 f.debug_struct("DirWatch").finish_non_exhaustive()
204 }
205}
206
207/// One implementation's directory-watch event stream.
208#[async_trait]
209pub trait FsEvents: Send {
210 /// Next event, or `Ok(None)` once the stream ends cleanly.
211 async fn next_event(&mut self) -> Result<Option<FsEventDto>>;
212}
213
214/// How a guest workload terminated.
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum ExitStatus {
217 /// The process exited normally with this code.
218 Code(i32),
219 /// The process was killed by this POSIX signal.
220 Signaled(i32),
221}
222
223impl ExitStatus {
224 /// Shell-convention scalar: the exit code itself, or `128 + signal` for a
225 /// signal death. For consumers that can only carry one integer.
226 #[must_use]
227 pub const fn conventional_code(self) -> i32 {
228 match self {
229 Self::Code(code) => code,
230 Self::Signaled(signal) => 128 + signal,
231 }
232 }
233}
234
235/// A chunk of output emitted by a guest process.
236#[derive(Debug, Clone)]
237pub enum OutputChunk {
238 /// Bytes from the process's stdout (the merged PTY stream for `tty` sessions).
239 Stdout(Vec<u8>),
240 /// Bytes from the process's stderr (never emitted for `tty` sessions).
241 Stderr(Vec<u8>),
242 /// The process terminated. Always the final chunk of a session.
243 Exit(ExitStatus),
244}
245
246/// A message the host sends to the guest during an exec/run session.
247#[derive(Debug)]
248pub enum ExecInputMsg {
249 /// Raw bytes to forward to the process's stdin.
250 Stdin(Vec<u8>),
251 /// Resize the pseudo-TTY.
252 Resize { width: u16, height: u16 },
253 /// Deliver a POSIX signal to the workload's process group.
254 Signal(i32),
255 /// Signal EOF on the process's stdin.
256 Eof,
257}
258
259/// Outcome of a guest listen-table wait.
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261pub enum PortWait {
262 /// A listener on the port exists.
263 Listening,
264 /// The deadline elapsed with no listener.
265 Deadline,
266}
267
268/// Outcome of a completed clock-sync round trip.
269///
270/// Both variants prove liveness — the agent accepted the connection, parsed
271/// the frame, and replied — which is what a readiness gate needs. Only
272/// [`ClockSync::Synced`] means the guest wall clock was actually set.
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum ClockSync {
275 /// The agent set the clock.
276 Synced,
277 /// The agent answered but could not set the clock (e.g. `clock_settime`
278 /// failed); it carries the agent-reported exit code.
279 AgentError(i32),
280}