Skip to main content

microsandbox_agentd/
agent.rs

1//! Main agent loop: serial I/O, session management, heartbeat.
2
3use std::collections::HashMap;
4use std::env;
5use std::fs::{File, OpenOptions};
6use std::os::fd::AsRawFd;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::time::Instant;
10
11use chrono::Utc;
12use tokio::io::unix::AsyncFd;
13use tokio::sync::{mpsc, watch};
14use tokio::time::{self, Duration};
15
16use microsandbox_protocol::HANDOFF_POWEROFF_TIMEOUT;
17use microsandbox_protocol::bootstrap::GuestBootstrap;
18use microsandbox_protocol::codec::{self, MAX_FRAME_SIZE};
19use microsandbox_protocol::core::{
20    ClockSync, CoreError, CoreErrorKind, InitAck, InitResolved, Ping, Pong, Ready,
21    RelayClientDisconnected, ResolvedUser, Touch, Touched,
22};
23use microsandbox_protocol::exec::{
24    ExecExited, ExecFailed, ExecFailureKind, ExecRequest, ExecResize, ExecSignal, ExecStarted,
25    ExecStderr, ExecStdin, ExecStdinError, ExecStdout,
26};
27use microsandbox_protocol::fs::{FsData, FsRequest};
28use microsandbox_protocol::heartbeat::{ActivityCounters, Heartbeat};
29use microsandbox_protocol::message::{Message, MessageType};
30use microsandbox_protocol::tcp::{TcpClose, TcpConnect, TcpData, TcpEof, TcpFailed};
31
32use crate::config::{AgentdConfig, scripts_path};
33use crate::error::{AgentdError, AgentdResult};
34use crate::fs::{FsReadSession, FsState, FsStreamSession, FsWriteSession};
35use crate::process::ProcessManager;
36use crate::serial::AGENT_PORT_NAME;
37use crate::session::{
38    ExecSession, RawActivity, RawSessionCompletion, SessionOutput, resolve_default_user,
39};
40use crate::tcp::TcpSession;
41use crate::{clock, fs, handoff, heartbeat, serial};
42
43//--------------------------------------------------------------------------------------------------
44// Constants
45//--------------------------------------------------------------------------------------------------
46
47/// Heartbeat interval in seconds.
48///
49/// Keep this short so small idle timeouts (for example `--idle-timeout 1`)
50/// can be enforced without multi-second scheduling drift.
51const HEARTBEAT_INTERVAL_SECS: u64 = 1;
52
53/// Read buffer size for the serial port.
54const SERIAL_READ_BUF_SIZE: usize = 64 * 1024;
55
56/// Maximum allowed input buffer size (frame size limit + 4 bytes for length prefix).
57const MAX_INPUT_BUF_SIZE: usize = MAX_FRAME_SIZE as usize + 4;
58
59/// Maximum time to wait for the host to acknowledge the init context.
60const INIT_ACK_TIMEOUT_SECS: u64 = 60;
61
62//--------------------------------------------------------------------------------------------------
63// Types
64//--------------------------------------------------------------------------------------------------
65
66#[derive(Default)]
67struct AgentState {
68    sessions: HashMap<u32, ExecSession>,
69    write_sessions: HashMap<u32, FsWriteSession>,
70    read_sessions: HashMap<u32, FsReadSession>,
71    tcp_sessions: HashMap<u32, TcpSession>,
72    fs: FsState,
73}
74
75struct ActivityTracker {
76    activity_seq: u64,
77    counters: ActivityCounters,
78}
79
80/// Buffered console input retained across the bootstrap and init handshakes.
81///
82/// A single device read may contain multiple frames, so boot phases must share
83/// this buffer instead of dropping bytes after decoding their expected frame.
84#[derive(Default)]
85pub struct BootConsoleState {
86    input: Vec<u8>,
87}
88
89#[derive(Clone)]
90struct HeartbeatSnapshot {
91    activity_seq: u64,
92    active_exec_sessions: u32,
93    active_fs_streams: u32,
94    active_tcp_streams: u32,
95    counters: ActivityCounters,
96}
97
98//--------------------------------------------------------------------------------------------------
99// Methods
100//--------------------------------------------------------------------------------------------------
101
102impl ActivityTracker {
103    fn new() -> Self {
104        Self {
105            activity_seq: 0,
106            counters: ActivityCounters::default(),
107        }
108    }
109
110    fn record_host_message(&mut self) {
111        self.touch();
112        self.counters.host_messages = self.counters.host_messages.saturating_add(1);
113    }
114
115    fn record_guest_message(&mut self) {
116        self.touch();
117        self.counters.guest_messages = self.counters.guest_messages.saturating_add(1);
118    }
119
120    fn add_exec_output_bytes(&mut self, len: usize) {
121        self.counters.exec_output_bytes =
122            self.counters.exec_output_bytes.saturating_add(len as u64);
123    }
124
125    fn add_fs_bytes(&mut self, len: usize) {
126        self.counters.fs_bytes = self.counters.fs_bytes.saturating_add(len as u64);
127    }
128
129    fn add_tcp_bytes(&mut self, len: usize) {
130        self.counters.tcp_bytes = self.counters.tcp_bytes.saturating_add(len as u64);
131    }
132
133    fn touch(&mut self) {
134        self.activity_seq = self.activity_seq.saturating_add(1);
135    }
136}
137
138//--------------------------------------------------------------------------------------------------
139// Functions
140//--------------------------------------------------------------------------------------------------
141
142/// Runs the main agent loop.
143///
144/// Reuses the already-open virtio serial port, sends `core.ready` with boot timing data,
145/// then enters the main select loop handling serial I/O, process output, and heartbeat.
146///
147/// - `boot_time_ns`: `CLOCK_BOOTTIME` at `main()` start (kernel boot duration).
148/// - `init_time_ns`: nanoseconds spent in `init::init()`.
149pub async fn run(
150    boot_time_ns: u64,
151    init_time_ns: u64,
152    config: &AgentdConfig,
153    port_file: File,
154    boot_console: BootConsoleState,
155) -> AgentdResult<()> {
156    let process_manager = ProcessManager::get()?;
157    let mut process_manager_failure = process_manager.subscribe_failure()?;
158
159    // Set non-blocking for async I/O. Early boot handshakes use the same fd
160    // in blocking mode before it is moved into the async loop.
161    let port_fd = port_file.as_raw_fd();
162    set_nonblocking(port_fd)?;
163
164    // A single AsyncFd tracks both readable and writable readiness.
165    let async_port = AsyncFd::new(port_file)?;
166
167    // Buffer for serial reads.
168    let mut read_buf = vec![0u8; SERIAL_READ_BUF_SIZE];
169    let mut serial_in_buf = boot_console.input;
170    let mut serial_out_buf = Vec::new();
171
172    let mut state = AgentState::default();
173
174    // Channel for session output events.
175    let (session_tx, mut session_rx) = mpsc::unbounded_channel::<(u32, SessionOutput)>();
176
177    // Heartbeat/activity state.
178    let mut activity = ActivityTracker::new();
179    let (heartbeat_tx, heartbeat_rx) = watch::channel(heartbeat_snapshot(&state, &activity));
180    // The liveness pulse runs on a dedicated OS thread, NOT a Tokio task. On the
181    // single-threaded agent runtime a flood of exec output can monopolize the
182    // executor and starve a heartbeat *task*, freezing the pulse even though the
183    // agent is alive — which makes the host wrongly declare it unresponsive and
184    // kill the sandbox. A plain OS thread is scheduled by the guest kernel
185    // independently of the async runtime, so the pulse keeps ticking under load.
186    let heartbeat_shutdown = Arc::new(AtomicBool::new(false));
187    let heartbeat_thread = spawn_heartbeat_thread(heartbeat_rx, Arc::clone(&heartbeat_shutdown));
188
189    // Send core.ready with boot timing data.
190    let ready_time_ns = clock::boottime_ns();
191    let ready_msg = Message::with_payload(
192        MessageType::Ready,
193        0,
194        &Ready {
195            boot_time_ns,
196            init_time_ns,
197            ready_time_ns,
198            agent_version: env!("CARGO_PKG_VERSION").to_string(),
199        },
200    )
201    .map_err(|e| AgentdError::ExecSession(format!("encode ready: {e}")))?;
202    codec::encode_to_buf(&ready_msg, &mut serial_out_buf)
203        .map_err(|e| AgentdError::ExecSession(format!("encode ready frame: {e}")))?;
204    flush_write_buf(&async_port, &mut serial_out_buf).await?;
205
206    // Main loop.
207    'agent: loop {
208        tokio::select! {
209            failure = process_manager_failure.changed() => {
210                let error = match failure {
211                    Ok(()) => process_manager_failure
212                        .borrow()
213                        .clone()
214                        .unwrap_or_else(|| "process manager stopped without an error".to_string()),
215                    Err(error) => format!("process manager failure channel closed: {error}"),
216                };
217                return Err(AgentdError::ExecSession(error));
218            }
219
220            // Read from serial port.
221            result = async_port.readable() => {
222                let Ok(mut guard) = result else {
223                    break;
224                };
225
226                loop {
227                    match guard.try_io(|inner| read_from_fd(inner.get_ref().as_raw_fd(), &mut read_buf)) {
228                        Ok(Ok(0)) => {
229                            // EOF on serial — host disconnected.
230                            if !handoff::is_pid_1() {
231                                guard.clear_ready();
232                                drop(guard);
233                                time::sleep(Duration::from_millis(100)).await;
234                                break;
235                            }
236                            break 'agent;
237                        }
238                        Ok(Ok(n)) => {
239                            serial_in_buf.extend_from_slice(&read_buf[..n]);
240
241                            // Guard against unbounded buffer growth.
242                            if serial_in_buf.len() > MAX_INPUT_BUF_SIZE {
243                                return Err(AgentdError::ExecSession(
244                                    "serial input buffer exceeded maximum size".into(),
245                                ));
246                            }
247
248                            // Try to parse complete frames. Recoverable
249                            // message-level failures are reported on the same
250                            // correlation ID with `core.error`; unrecoverable
251                            // frame-level failures still close the agent loop.
252                            while let Some(frame) = codec::try_decode_raw_from_buf(&mut serial_in_buf)
253                                .map_err(|e| AgentdError::ExecSession(format!("decode frame: {e}")))?
254                            {
255                                let id = frame.id;
256                                let msg = match codec::raw_frame_to_message(frame) {
257                                    Ok(msg) => msg,
258                                    Err(e) => {
259                                        return Err(AgentdError::ExecSession(format!(
260                                            "decode message for id {id}: {e}"
261                                        )));
262                                    }
263                                };
264
265                                if msg.flags != msg.t.flags() {
266                                    let out_before = serial_out_buf.len();
267                                    encode_core_error_if_supported(
268                                        &msg,
269                                        msg.id,
270                                        CoreErrorKind::InvalidFlags,
271                                        format!(
272                                            "invalid flags for {}: got {}, expected {}",
273                                            msg.t.as_str(),
274                                            msg.flags,
275                                            msg.t.flags()
276                                        ),
277                                        Some(msg.t.as_str().to_string()),
278                                        &mut serial_out_buf,
279                                    )?;
280                                    record_encoded_guest_messages(
281                                        &serial_out_buf,
282                                        out_before,
283                                        &mut activity,
284                                    );
285                                    publish_heartbeat_snapshot(&heartbeat_tx, &state, &activity);
286                                    continue;
287                                }
288
289                                if message_refreshes_idle_timer(&msg.t) {
290                                    activity.record_host_message();
291                                    publish_heartbeat_snapshot(&heartbeat_tx, &state, &activity);
292                                }
293
294                                let out_before = serial_out_buf.len();
295                                handle_message(
296                                    msg,
297                                    &mut state,
298                                    &mut activity,
299                                    &session_tx,
300                                    &mut serial_out_buf,
301                                    config,
302                                ).await?;
303                                record_encoded_guest_messages(
304                                    &serial_out_buf,
305                                    out_before,
306                                    &mut activity,
307                                );
308                                publish_heartbeat_snapshot(&heartbeat_tx, &state, &activity);
309                            }
310
311                            // Flush any outgoing messages.
312                            if !serial_out_buf.is_empty() {
313                                flush_write_buf(&async_port, &mut serial_out_buf).await?;
314                            }
315                        }
316                        Ok(Err(e)) if e.kind() == std::io::ErrorKind::Interrupted => continue,
317                        Ok(Err(_)) if !handoff::is_pid_1() => {
318                            guard.clear_ready();
319                            drop(guard);
320                            time::sleep(Duration::from_millis(100)).await;
321                            break;
322                        }
323                        Ok(Err(e)) => return Err(e.into()),
324                        Err(_would_block) => break,
325                    }
326                }
327            }
328
329            // Receive output events from session reader tasks.
330            Some((id, output)) = session_rx.recv() => {
331                match output {
332                    SessionOutput::Stdout(data) => {
333                        let len = data.len();
334                        let msg = Message::with_payload(MessageType::ExecStdout, id, &ExecStdout { data })
335                            .map_err(|e| AgentdError::ExecSession(format!("encode stdout: {e}")))?;
336                        codec::encode_to_buf(&msg, &mut serial_out_buf)
337                            .map_err(|e| AgentdError::ExecSession(format!("encode stdout frame: {e}")))?;
338                        activity.record_guest_message();
339                        activity.add_exec_output_bytes(len);
340                    }
341                    SessionOutput::Stderr(data) => {
342                        let len = data.len();
343                        let msg = Message::with_payload(MessageType::ExecStderr, id, &ExecStderr { data })
344                            .map_err(|e| AgentdError::ExecSession(format!("encode stderr: {e}")))?;
345                        codec::encode_to_buf(&msg, &mut serial_out_buf)
346                            .map_err(|e| AgentdError::ExecSession(format!("encode stderr frame: {e}")))?;
347                        activity.record_guest_message();
348                        activity.add_exec_output_bytes(len);
349                    }
350                    SessionOutput::Exited(code) => {
351                        let msg = Message::with_payload(MessageType::ExecExited, id, &ExecExited { code })
352                            .map_err(|e| AgentdError::ExecSession(format!("encode exited: {e}")))?;
353                        codec::encode_to_buf(&msg, &mut serial_out_buf)
354                            .map_err(|e| AgentdError::ExecSession(format!("encode exited frame: {e}")))?;
355                        state.sessions.remove(&id);
356                        activity.record_guest_message();
357                    }
358                    SessionOutput::Raw(output) => {
359                        apply_raw_activity(output.activity, &mut activity);
360                        complete_raw_session(
361                            id,
362                            output.completion,
363                            &mut state.read_sessions,
364                            &mut state.tcp_sessions,
365                        );
366                        // Pre-encoded frame — write directly to output buffer.
367                        serial_out_buf.extend_from_slice(&output.frame);
368                    }
369                }
370                publish_heartbeat_snapshot(&heartbeat_tx, &state, &activity);
371
372                if !serial_out_buf.is_empty() {
373                    flush_write_buf(&async_port, &mut serial_out_buf).await?;
374                }
375            }
376        }
377    }
378
379    heartbeat_shutdown.store(true, Ordering::Relaxed);
380    let _ = heartbeat_thread.join();
381
382    Ok(())
383}
384
385/// Opens the agent virtio-serial port once for early boot handshakes and the agent loop.
386pub fn open_serial_port() -> AgentdResult<File> {
387    // Discover serial port.
388    let port_path = serial::find_serial_port(AGENT_PORT_NAME)?;
389
390    // Open the port once with read+write. Virtio-console multiport devices
391    // only allow a single open; a second open returns EBUSY.
392    Ok(OpenOptions::new().read(true).write(true).open(&port_path)?)
393}
394
395/// Receive and validate the first host-to-guest bootstrap frame.
396pub fn receive_bootstrap(port_file: &File) -> AgentdResult<(GuestBootstrap, BootConsoleState)> {
397    let fd = port_file.as_raw_fd();
398    set_nonblocking(fd)?;
399    let deadline = init_ack_deadline();
400    let mut state = BootConsoleState::default();
401    let msg = read_boot_message(fd, &mut state, deadline, "guest bootstrap")?;
402    let bootstrap = decode_bootstrap_message(msg)?;
403    Ok((bootstrap, state))
404}
405
406fn decode_bootstrap_message(msg: Message) -> AgentdResult<GuestBootstrap> {
407    if msg.id != 0 || msg.flags != 0 {
408        return Err(AgentdError::Config(format!(
409            "guest bootstrap requires id=0 and flags=0, got id={} flags={}",
410            msg.id, msg.flags
411        )));
412    }
413    if msg.t != MessageType::Bootstrap {
414        return Err(AgentdError::Config(format!(
415            "expected core.bootstrap as first console frame, got {}",
416            msg.t.as_str()
417        )));
418    }
419    let min_version = MessageType::Bootstrap.min_protocol_version();
420    if msg.v < min_version {
421        return Err(AgentdError::Config(format!(
422            "guest bootstrap requires protocol generation {min_version} or newer, got {}",
423            msg.v
424        )));
425    }
426
427    msg.payload::<GuestBootstrap>()
428        .map_err(|e| AgentdError::Config(format!("decode guest bootstrap payload: {e}")))
429}
430
431/// Reports init-time guest context to the host and waits for an acknowledgement.
432pub fn report_init_context(
433    port_file: &File,
434    boot_console: &mut BootConsoleState,
435    default_user: Option<&str>,
436) -> AgentdResult<()> {
437    let (uid, gid) = resolve_default_user(default_user)?;
438    let deadline = init_ack_deadline();
439    let fd = port_file.as_raw_fd();
440    set_nonblocking(fd)?;
441
442    let msg = Message::with_payload(
443        MessageType::InitResolved,
444        0,
445        &InitResolved {
446            default_user: ResolvedUser { uid, gid },
447        },
448    )
449    .map_err(|e| AgentdError::ExecSession(format!("encode init context: {e}")))?;
450
451    let mut out = Vec::new();
452    codec::encode_to_buf(&msg, &mut out)
453        .map_err(|e| AgentdError::ExecSession(format!("encode init context frame: {e}")))?;
454    write_all_to_fd(fd, &out, deadline)?;
455    wait_for_init_ack(fd, boot_console, deadline)
456}
457
458//--------------------------------------------------------------------------------------------------
459// Functions: Helpers
460//--------------------------------------------------------------------------------------------------
461
462/// Handles a single incoming message from the host.
463async fn handle_message(
464    msg: Message,
465    state: &mut AgentState,
466    activity: &mut ActivityTracker,
467    session_tx: &mpsc::UnboundedSender<(u32, SessionOutput)>,
468    out_buf: &mut Vec<u8>,
469    config: &AgentdConfig,
470) -> AgentdResult<()> {
471    match msg.t {
472        MessageType::Ping => {
473            let Some(_) = decode_payload_or_core_error::<Ping>(&msg, out_buf)? else {
474                return Ok(());
475            };
476            let reply = Message::with_payload(MessageType::Pong, msg.id, &Pong {})
477                .map_err(|e| AgentdError::ExecSession(format!("encode pong: {e}")))?;
478            codec::encode_to_buf(&reply, out_buf)
479                .map_err(|e| AgentdError::ExecSession(format!("encode pong frame: {e}")))?;
480        }
481
482        MessageType::Touch => {
483            let Some(_) = decode_payload_or_core_error::<Touch>(&msg, out_buf)? else {
484                return Ok(());
485            };
486            activity.record_host_message();
487            let reply = Message::with_payload(
488                MessageType::Touched,
489                msg.id,
490                &Touched {
491                    activity_seq: activity.activity_seq,
492                },
493            )
494            .map_err(|e| AgentdError::ExecSession(format!("encode touched: {e}")))?;
495            codec::encode_to_buf(&reply, out_buf)
496                .map_err(|e| AgentdError::ExecSession(format!("encode touched frame: {e}")))?;
497        }
498
499        MessageType::ExecRequest => {
500            let Some(mut req) = decode_payload_or_core_error::<ExecRequest>(&msg, out_buf)? else {
501                return Ok(());
502            };
503            if req.cwd.is_none() {
504                req.cwd = config.default_cwd().map(str::to_string);
505            }
506            prepend_scripts_to_path(&mut req);
507            match ExecSession::spawn(
508                msg.id,
509                &req,
510                session_tx.clone(),
511                config.user.as_deref(),
512                config.security_profile,
513            ) {
514                Ok(session) => {
515                    let reply = Message::with_payload(
516                        MessageType::ExecStarted,
517                        msg.id,
518                        &ExecStarted { pid: session.pid() },
519                    )
520                    .map_err(|e| AgentdError::ExecSession(format!("encode started: {e}")))?;
521                    codec::encode_to_buf(&reply, out_buf).map_err(|e| {
522                        AgentdError::ExecSession(format!("encode started frame: {e}"))
523                    })?;
524                    state.sessions.insert(msg.id, session);
525                }
526                Err(e) => {
527                    // Send a typed `ExecFailed` so the host can render a
528                    // useful message + hint. `ExecSpawnFailed` already
529                    // carries the structured payload; other error
530                    // variants (free-form `ExecSession(_)` etc.) get
531                    // wrapped as `Other` with the message preserved.
532                    let payload = match &e {
533                        AgentdError::ExecSpawnFailed(p) => p.clone(),
534                        other => ExecFailed {
535                            kind: ExecFailureKind::Other,
536                            errno: None,
537                            errno_name: None,
538                            message: other.to_string(),
539                            stage: None,
540                        },
541                    };
542                    let reply = Message::with_payload(MessageType::ExecFailed, msg.id, &payload)
543                        .map_err(|e| AgentdError::ExecSession(format!("encode failed: {e}")))?;
544                    codec::encode_to_buf(&reply, out_buf).map_err(|e| {
545                        AgentdError::ExecSession(format!("encode failed frame: {e}"))
546                    })?;
547                    eprintln!("failed to spawn exec session {}: {e}", msg.id);
548                }
549            }
550        }
551
552        MessageType::ExecStdin => {
553            let Some(stdin) = decode_payload_or_core_error::<ExecStdin>(&msg, out_buf)? else {
554                return Ok(());
555            };
556            if let Some(session) = state.sessions.get_mut(&msg.id) {
557                if stdin.data.is_empty() {
558                    // Empty data signals EOF — close stdin.
559                    session.close_stdin();
560                } else if let Err(e) = session.write_stdin(&stdin.data).await {
561                    let payload = stdin_error_payload(&e);
562                    eprintln!("stdin write error on session {}: {e}", msg.id);
563                    let reply =
564                        Message::with_payload(MessageType::ExecStdinError, msg.id, &payload)
565                            .map_err(|e| {
566                                AgentdError::ExecSession(format!("encode stdin error: {e}"))
567                            })?;
568                    codec::encode_to_buf(&reply, out_buf).map_err(|e| {
569                        AgentdError::ExecSession(format!("encode stdin error frame: {e}"))
570                    })?;
571                }
572            }
573        }
574
575        MessageType::ExecResize => {
576            let Some(resize) = decode_payload_or_core_error::<ExecResize>(&msg, out_buf)? else {
577                return Ok(());
578            };
579            if let Some(session) = state.sessions.get(&msg.id) {
580                let _ = session.resize(resize.rows, resize.cols);
581            }
582        }
583
584        MessageType::ExecSignal => {
585            let Some(signal) = decode_payload_or_core_error::<ExecSignal>(&msg, out_buf)? else {
586                return Ok(());
587            };
588            if let Some(session) = state.sessions.get(&msg.id) {
589                let _ = session.send_signal(signal.signal);
590            }
591        }
592
593        MessageType::FsRequest => {
594            let Some(req) = decode_payload_or_core_error::<FsRequest>(&msg, out_buf)? else {
595                return Ok(());
596            };
597            match fs::handle_fs_request(msg.id, req, &mut state.fs, out_buf, session_tx).await {
598                Ok(Some(FsStreamSession::Read(rs))) => {
599                    state.read_sessions.insert(msg.id, rs);
600                }
601                Ok(Some(FsStreamSession::Write(ws))) => {
602                    state.write_sessions.insert(msg.id, ws);
603                }
604                Ok(None) => {}
605                Err(e) => {
606                    eprintln!("fs request error for {}: {e}", msg.id);
607                }
608            }
609        }
610
611        MessageType::FsData => {
612            let Some(data) = decode_payload_or_core_error::<FsData>(&msg, out_buf)? else {
613                return Ok(());
614            };
615            let len = data.data.len();
616            if let Some(session) = state.write_sessions.get_mut(&msg.id) {
617                match fs::handle_fs_data(msg.id, data, session, out_buf).await {
618                    Ok(true) => {
619                        // Session complete — remove it.
620                        state.write_sessions.remove(&msg.id);
621                    }
622                    Ok(false) => {
623                        activity.add_fs_bytes(len);
624                    }
625                    Err(e) => {
626                        eprintln!("fs data error for {}: {e}", msg.id);
627                        state.write_sessions.remove(&msg.id);
628                    }
629                }
630            } else {
631                // No write session for this ID — send error response.
632                let resp = microsandbox_protocol::fs::FsResponse {
633                    ok: false,
634                    error: Some(format!("unknown write session: {}", msg.id)),
635                    data: None,
636                };
637                let reply = Message::with_payload(MessageType::FsResponse, msg.id, &resp)
638                    .map_err(|e| AgentdError::ExecSession(format!("encode fs error: {e}")))?;
639                codec::encode_to_buf(&reply, out_buf)
640                    .map_err(|e| AgentdError::ExecSession(format!("encode fs error frame: {e}")))?;
641            }
642        }
643
644        MessageType::TcpConnect => {
645            let Some(req) = decode_payload_or_core_error::<TcpConnect>(&msg, out_buf)? else {
646                return Ok(());
647            };
648            // The connect runs inside the session task; the agent loop never
649            // blocks on it. Success or failure arrives later as a tcp frame.
650            let session = TcpSession::open(msg.id, req, session_tx);
651            state.tcp_sessions.insert(msg.id, session);
652        }
653
654        MessageType::TcpData => {
655            let Some(data) = decode_payload_or_core_error::<TcpData>(&msg, out_buf)? else {
656                return Ok(());
657            };
658            let len = data.data.len();
659            if let Some(session) = state.tcp_sessions.get(&msg.id) {
660                if let Err(e) = session.write_data(data.data).await {
661                    state.tcp_sessions.remove(&msg.id);
662                    encode_tcp_failed(msg.id, e, out_buf)?;
663                } else {
664                    activity.add_tcp_bytes(len);
665                }
666            } else {
667                encode_tcp_failed(msg.id, format!("unknown TCP session: {}", msg.id), out_buf)?;
668            }
669        }
670
671        MessageType::TcpEof => {
672            let Some(_) = decode_payload_or_core_error::<TcpEof>(&msg, out_buf)? else {
673                return Ok(());
674            };
675            if let Some(session) = state.tcp_sessions.get(&msg.id)
676                && let Err(e) = session.close_write().await
677            {
678                state.tcp_sessions.remove(&msg.id);
679                encode_tcp_failed(msg.id, e, out_buf)?;
680            }
681        }
682
683        MessageType::TcpClose => {
684            let Some(_) = decode_payload_or_core_error::<TcpClose>(&msg, out_buf)? else {
685                return Ok(());
686            };
687            if let Some(session) = state.tcp_sessions.remove(&msg.id) {
688                session.close();
689            }
690        }
691
692        MessageType::RelayClientDisconnected => {
693            let Some(disconnected) =
694                decode_payload_or_core_error::<RelayClientDisconnected>(&msg, out_buf)?
695            else {
696                return Ok(());
697            };
698            state
699                .fs
700                .close_owner_range(disconnected.id_start, disconnected.id_end_exclusive);
701            abort_read_sessions_in_owner_range(
702                &mut state.read_sessions,
703                disconnected.id_start,
704                disconnected.id_end_exclusive,
705            );
706            state.write_sessions.retain(|_, session| {
707                let owner_id = session.owner_id();
708                owner_id < disconnected.id_start || owner_id >= disconnected.id_end_exclusive
709            });
710            close_tcp_sessions_in_owner_range(
711                &mut state.tcp_sessions,
712                disconnected.id_start,
713                disconnected.id_end_exclusive,
714            );
715        }
716
717        MessageType::ClockSync => {
718            let Some(sync) = decode_payload_or_core_error::<ClockSync>(&msg, out_buf)? else {
719                return Ok(());
720            };
721            if let Err(e) = clock::sync_realtime_unix_nanos(sync.unix_time_nanos) {
722                eprintln!("clock: failed to sync realtime clock: {e}");
723            }
724        }
725
726        MessageType::Shutdown => {
727            // Graceful shutdown — signal all sessions, then ask the guest
728            // kernel to power off so block-root filesystems can shut down
729            // cleanly instead of leaving ext4 journal recovery pending.
730            for (_, session) in state.sessions.drain() {
731                let _ = session.send_signal(15); // SIGTERM
732            }
733            state.write_sessions.clear();
734            for (_, session) in state.tcp_sessions.drain() {
735                session.close();
736            }
737            state.fs.clear();
738
739            request_guest_poweroff()?;
740            return Err(AgentdError::Shutdown);
741        }
742
743        _ => {
744            // Ignore unknown or unexpected message types.
745        }
746    }
747
748    Ok(())
749}
750
751/// Prepends `/.msb/scripts` to PATH in the exec request's environment.
752///
753/// If the request already has a PATH entry, prepends to it. Otherwise
754/// inherits from agentd's environment and prepends.
755/// Returns whether a host message should refresh the sandbox idle timer.
756///
757/// Maintenance traffic such as clock synchronization and reachability checks
758/// must not count as user activity, otherwise periodic host tasks would keep an
759/// idle sandbox alive. `core.touch` is excluded here too because it refreshes
760/// idleness explicitly in its handler, after its payload has been validated.
761fn message_refreshes_idle_timer(t: &MessageType) -> bool {
762    !matches!(
763        t,
764        MessageType::ClockSync | MessageType::Ping | MessageType::Touch
765    )
766}
767
768/// Returns whether an agent reply should refresh the sandbox idle timer.
769///
770/// Most guest output still represents useful sandbox activity. Maintenance
771/// replies to `core.ping` and `core.touch` are excluded so `ping` is a pure
772/// health check and `touch` advances activity exactly once. `core.error` is
773/// also excluded because valid work already records activity on the incoming
774/// request, while malformed maintenance traffic should not become a keepalive.
775fn guest_message_refreshes_idle_timer(t: &MessageType) -> bool {
776    !matches!(
777        t,
778        MessageType::Pong | MessageType::Touched | MessageType::CoreError
779    )
780}
781
782/// Spawns the heartbeat pulse on a dedicated OS thread.
783///
784/// This thread is intentionally outside the Tokio runtime: it reads the latest
785/// [`HeartbeatSnapshot`] (a lock-free `watch` borrow) and writes the heartbeat
786/// file with blocking `std::fs` once per [`HEARTBEAT_INTERVAL_SECS`]. Because it
787/// is an ordinary kernel-scheduled thread, a CPU-bound or I/O-saturated async
788/// runtime cannot delay the pulse — which is exactly the starvation that made
789/// the host kill busy-but-healthy sandboxes. The sleep is chunked so the thread
790/// observes the shutdown flag promptly when the agent loop exits.
791fn spawn_heartbeat_thread(
792    snapshot_rx: watch::Receiver<HeartbeatSnapshot>,
793    shutdown: Arc<AtomicBool>,
794) -> std::thread::JoinHandle<()> {
795    std::thread::Builder::new()
796        .name("agentd-heartbeat".to_string())
797        .spawn(move || {
798            let mut heartbeat_seq = 0u64;
799            let mut last_activity_seq = snapshot_rx.borrow().activity_seq;
800            let mut last_activity = Utc::now();
801
802            let interval = Duration::from_secs(HEARTBEAT_INTERVAL_SECS);
803            let step = Duration::from_millis(100);
804
805            while !shutdown.load(Ordering::Relaxed) {
806                let mut slept = Duration::ZERO;
807                while slept < interval {
808                    if shutdown.load(Ordering::Relaxed) {
809                        return;
810                    }
811                    std::thread::sleep(step);
812                    slept += step;
813                }
814
815                if !heartbeat::heartbeat_dir_exists() {
816                    continue;
817                }
818
819                heartbeat_seq = heartbeat_seq.saturating_add(1);
820                let snapshot = snapshot_rx.borrow().clone();
821                let timestamp = Utc::now();
822                if snapshot.activity_seq != last_activity_seq {
823                    last_activity_seq = snapshot.activity_seq;
824                    last_activity = timestamp;
825                }
826                let heartbeat = Heartbeat {
827                    heartbeat_seq,
828                    activity_seq: snapshot.activity_seq,
829                    timestamp,
830                    last_activity,
831                    active_exec_sessions: snapshot.active_exec_sessions,
832                    active_fs_streams: snapshot.active_fs_streams,
833                    active_tcp_streams: snapshot.active_tcp_streams,
834                    activity_counters: snapshot.counters,
835                };
836                let _ = heartbeat::write_heartbeat(&heartbeat);
837            }
838        })
839        .expect("failed to spawn agentd heartbeat thread")
840}
841
842fn heartbeat_snapshot(state: &AgentState, activity: &ActivityTracker) -> HeartbeatSnapshot {
843    HeartbeatSnapshot {
844        activity_seq: activity.activity_seq,
845        active_exec_sessions: state.sessions.len() as u32,
846        active_fs_streams: state
847            .read_sessions
848            .len()
849            .saturating_add(state.write_sessions.len()) as u32,
850        active_tcp_streams: state.tcp_sessions.len() as u32,
851        counters: activity.counters,
852    }
853}
854
855fn publish_heartbeat_snapshot(
856    heartbeat_tx: &watch::Sender<HeartbeatSnapshot>,
857    state: &AgentState,
858    activity: &ActivityTracker,
859) {
860    let _ = heartbeat_tx.send(heartbeat_snapshot(state, activity));
861}
862
863fn record_encoded_guest_messages(out_buf: &[u8], start: usize, activity: &mut ActivityTracker) {
864    let mut offset = start;
865    while offset + 4 <= out_buf.len() {
866        let frame_len = u32::from_be_bytes([
867            out_buf[offset],
868            out_buf[offset + 1],
869            out_buf[offset + 2],
870            out_buf[offset + 3],
871        ]) as usize;
872        let total = 4usize.saturating_add(frame_len);
873        if offset.saturating_add(total) > out_buf.len() {
874            break;
875        }
876
877        if encoded_guest_message_refreshes_idle_timer(out_buf, offset, frame_len) {
878            activity.record_guest_message();
879        }
880        offset += total;
881    }
882}
883
884fn encoded_guest_message_refreshes_idle_timer(
885    out_buf: &[u8],
886    offset: usize,
887    frame_len: usize,
888) -> bool {
889    if frame_len < microsandbox_protocol::message::FRAME_HEADER_SIZE {
890        return true;
891    }
892
893    let id_start = offset + 4;
894    let flags_index = id_start + 4;
895    let body_start = flags_index + 1;
896    let body_end = offset + 4 + frame_len;
897    if body_end > out_buf.len() || body_start > body_end {
898        return true;
899    }
900
901    let id = u32::from_be_bytes([
902        out_buf[id_start],
903        out_buf[id_start + 1],
904        out_buf[id_start + 2],
905        out_buf[id_start + 3],
906    ]);
907    let frame = codec::RawFrame {
908        id,
909        flags: out_buf[flags_index],
910        body: out_buf[body_start..body_end].to_vec(),
911    };
912
913    codec::raw_frame_to_message(frame)
914        .map(|msg| guest_message_refreshes_idle_timer(&msg.t))
915        .unwrap_or(true)
916}
917
918fn apply_raw_activity(raw: RawActivity, activity: &mut ActivityTracker) {
919    if raw.guest_message {
920        activity.record_guest_message();
921    }
922    if raw.fs_bytes > 0 {
923        activity.add_fs_bytes(raw.fs_bytes);
924    }
925    if raw.tcp_bytes > 0 {
926        activity.add_tcp_bytes(raw.tcp_bytes);
927    }
928}
929
930fn complete_raw_session(
931    id: u32,
932    completion: Option<RawSessionCompletion>,
933    read_sessions: &mut HashMap<u32, FsReadSession>,
934    tcp_sessions: &mut HashMap<u32, TcpSession>,
935) {
936    match completion {
937        Some(RawSessionCompletion::FsRead) => {
938            read_sessions.remove(&id);
939        }
940        Some(RawSessionCompletion::Tcp) => {
941            tcp_sessions.remove(&id);
942        }
943        None => {}
944    }
945}
946
947fn abort_read_sessions_in_owner_range(
948    read_sessions: &mut HashMap<u32, FsReadSession>,
949    id_start: u32,
950    id_end_exclusive: u32,
951) {
952    let mut retained = HashMap::new();
953    for (id, session) in read_sessions.drain() {
954        let owner_id = session.owner_id();
955        if owner_id >= id_start && owner_id < id_end_exclusive {
956            session.abort();
957        } else {
958            retained.insert(id, session);
959        }
960    }
961    *read_sessions = retained;
962}
963
964fn close_tcp_sessions_in_owner_range(
965    tcp_sessions: &mut HashMap<u32, TcpSession>,
966    id_start: u32,
967    id_end_exclusive: u32,
968) {
969    let mut retained = HashMap::new();
970    for (id, session) in tcp_sessions.drain() {
971        let owner_id = session.owner_id();
972        if owner_id >= id_start && owner_id < id_end_exclusive {
973            session.close();
974        } else {
975            retained.insert(id, session);
976        }
977    }
978    *tcp_sessions = retained;
979}
980
981fn encode_tcp_failed(id: u32, error: String, out_buf: &mut Vec<u8>) -> AgentdResult<()> {
982    let reply = Message::with_payload(MessageType::TcpFailed, id, &TcpFailed { error })
983        .map_err(|e| AgentdError::ExecSession(format!("encode tcp failed: {e}")))?;
984    codec::encode_to_buf(&reply, out_buf)
985        .map_err(|e| AgentdError::ExecSession(format!("encode tcp failed frame: {e}")))?;
986    Ok(())
987}
988
989fn encode_core_error_if_supported(
990    source: &Message,
991    id: u32,
992    kind: CoreErrorKind,
993    message: String,
994    offending_type: Option<String>,
995    out_buf: &mut Vec<u8>,
996) -> AgentdResult<()> {
997    if !MessageType::CoreError.is_available_at(source.v) {
998        return Err(AgentdError::ExecSession(format!(
999            "cannot send core.error to protocol generation {}",
1000            source.v
1001        )));
1002    }
1003
1004    encode_core_error(id, kind, message, offending_type, out_buf)
1005}
1006
1007fn encode_core_error(
1008    id: u32,
1009    kind: CoreErrorKind,
1010    message: String,
1011    offending_type: Option<String>,
1012    out_buf: &mut Vec<u8>,
1013) -> AgentdResult<()> {
1014    let reply = Message::with_payload(
1015        MessageType::CoreError,
1016        id,
1017        &CoreError {
1018            kind,
1019            message,
1020            offending_type,
1021        },
1022    )
1023    .map_err(|e| AgentdError::ExecSession(format!("encode core error: {e}")))?;
1024    codec::encode_to_buf(&reply, out_buf)
1025        .map_err(|e| AgentdError::ExecSession(format!("encode core error frame: {e}")))?;
1026    Ok(())
1027}
1028
1029fn decode_payload_or_core_error<T>(msg: &Message, out_buf: &mut Vec<u8>) -> AgentdResult<Option<T>>
1030where
1031    T: serde::de::DeserializeOwned,
1032{
1033    match msg.payload::<T>() {
1034        Ok(payload) => Ok(Some(payload)),
1035        Err(error) => {
1036            encode_core_error_if_supported(
1037                msg,
1038                msg.id,
1039                CoreErrorKind::InvalidPayload,
1040                format!("decode payload for {}: {error}", msg.t.as_str()),
1041                Some(msg.t.as_str().to_string()),
1042                out_buf,
1043            )?;
1044            Ok(None)
1045        }
1046    }
1047}
1048
1049/// Build an `ExecStdinError` payload from a failed `write_stdin` result.
1050fn stdin_error_payload(err: &AgentdError) -> ExecStdinError {
1051    let io_err = match err {
1052        AgentdError::Io(e) => Some(e),
1053        _ => None,
1054    };
1055    let errno = io_err.and_then(|e| e.raw_os_error());
1056    ExecStdinError {
1057        errno,
1058        errno_name: errno.and_then(errno_name),
1059        message: err.to_string(),
1060    }
1061}
1062
1063/// Map common errno values to their standard names. Returns `None` for
1064/// codes we don't recognize; callers fall back to the numeric `errno`.
1065fn errno_name(code: i32) -> Option<String> {
1066    let name = match code {
1067        libc::EPIPE => "EPIPE",
1068        libc::EBADF => "EBADF",
1069        libc::EINVAL => "EINVAL",
1070        libc::EIO => "EIO",
1071        libc::ENOSPC => "ENOSPC",
1072        libc::EFBIG => "EFBIG",
1073        _ => return None,
1074    };
1075    Some(name.to_string())
1076}
1077
1078fn prepend_scripts_to_path(req: &mut microsandbox_protocol::exec::ExecRequest) {
1079    // Check if the request already specifies PATH.
1080    if let Some(entry) = req.env.iter_mut().find(|e| e.starts_with("PATH=")) {
1081        let existing = &entry["PATH=".len()..];
1082        *entry = format!("PATH={}", scripts_path(Some(existing)));
1083    } else {
1084        let inherited = env::var("PATH").ok();
1085        req.env
1086            .push(format!("PATH={}", scripts_path(inherited.as_deref())));
1087    }
1088}
1089
1090/// Sets a file descriptor to non-blocking mode.
1091fn set_nonblocking(fd: i32) -> AgentdResult<()> {
1092    let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
1093    if flags < 0 {
1094        return Err(std::io::Error::last_os_error().into());
1095    }
1096    let ret = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
1097    if ret < 0 {
1098        return Err(std::io::Error::last_os_error().into());
1099    }
1100    Ok(())
1101}
1102
1103fn init_ack_deadline() -> Instant {
1104    Instant::now() + std::time::Duration::from_secs(INIT_ACK_TIMEOUT_SECS)
1105}
1106
1107fn init_ack_timeout() -> AgentdError {
1108    AgentdError::ExecSession("timed out waiting for init ack".into())
1109}
1110
1111fn wait_for_init_ack(
1112    fd: i32,
1113    boot_console: &mut BootConsoleState,
1114    deadline: Instant,
1115) -> AgentdResult<()> {
1116    let msg = read_boot_message(fd, boot_console, deadline, "init ack")?;
1117    if msg.t == MessageType::InitAck {
1118        let _: InitAck = msg
1119            .payload()
1120            .map_err(|e| AgentdError::ExecSession(format!("decode init ack payload: {e}")))?;
1121        return Ok(());
1122    }
1123
1124    Err(AgentdError::ExecSession(format!(
1125        "expected core.init.ack, got {}",
1126        msg.t.as_str()
1127    )))
1128}
1129
1130fn read_boot_message(
1131    fd: i32,
1132    state: &mut BootConsoleState,
1133    deadline: Instant,
1134    context: &str,
1135) -> AgentdResult<Message> {
1136    let mut read_buf = [0u8; 4096];
1137    loop {
1138        if let Some(msg) = codec::try_decode_from_buf(&mut state.input)
1139            .map_err(|e| AgentdError::ExecSession(format!("decode {context}: {e}")))?
1140        {
1141            return Ok(msg);
1142        }
1143        if state.input.len() > MAX_INPUT_BUF_SIZE {
1144            return Err(AgentdError::ExecSession(format!(
1145                "serial input buffer exceeded maximum size while waiting for {context}"
1146            )));
1147        }
1148        if !poll_fd_until(fd, libc::POLLIN, deadline)? {
1149            return Err(if context == "init ack" {
1150                init_ack_timeout()
1151            } else {
1152                AgentdError::ExecSession(format!("timed out waiting for {context}"))
1153            });
1154        }
1155        let n = match read_from_fd(fd, &mut read_buf) {
1156            Ok(n) => n,
1157            Err(error)
1158                if matches!(
1159                    error.kind(),
1160                    std::io::ErrorKind::Interrupted | std::io::ErrorKind::WouldBlock
1161                ) =>
1162            {
1163                continue;
1164            }
1165            Err(error) => return Err(error.into()),
1166        };
1167        if n == 0 {
1168            return Err(AgentdError::ExecSession(format!(
1169                "serial port closed while waiting for {context}"
1170            )));
1171        }
1172        state.input.extend_from_slice(&read_buf[..n]);
1173    }
1174}
1175
1176fn poll_fd_until(fd: i32, events: i16, deadline: Instant) -> AgentdResult<bool> {
1177    loop {
1178        let remaining = deadline.saturating_duration_since(Instant::now());
1179        if remaining.is_zero() {
1180            return Ok(false);
1181        }
1182
1183        let timeout_ms = remaining.as_millis().min(i32::MAX as u128) as i32;
1184        let timeout_ms = if timeout_ms == 0 { 1 } else { timeout_ms };
1185        let mut pfd = libc::pollfd {
1186            fd,
1187            events,
1188            revents: 0,
1189        };
1190        let ret = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
1191        if ret > 0 {
1192            return Ok(true);
1193        }
1194        if ret == 0 {
1195            return Ok(false);
1196        }
1197        let err = std::io::Error::last_os_error();
1198        if err.raw_os_error() == Some(libc::EINTR) {
1199            continue;
1200        }
1201        return Err(err.into());
1202    }
1203}
1204
1205/// Reads from a raw fd (non-blocking).
1206fn read_from_fd(fd: i32, buf: &mut [u8]) -> std::io::Result<usize> {
1207    let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
1208    if n < 0 {
1209        Err(std::io::Error::last_os_error())
1210    } else {
1211        Ok(n as usize)
1212    }
1213}
1214
1215fn write_all_to_fd(fd: i32, mut buf: &[u8], deadline: Instant) -> AgentdResult<()> {
1216    while !buf.is_empty() {
1217        match write_to_fd(fd, buf) {
1218            Ok(0) => return Err(std::io::Error::from(std::io::ErrorKind::WriteZero).into()),
1219            Ok(n) => buf = &buf[n..],
1220            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1221            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
1222                if !poll_fd_until(fd, libc::POLLOUT, deadline)? {
1223                    return Err(init_ack_timeout());
1224                }
1225            }
1226            Err(e) => return Err(e.into()),
1227        }
1228    }
1229
1230    Ok(())
1231}
1232
1233/// Flushes the write buffer to the async fd.
1234async fn flush_write_buf(fd: &AsyncFd<std::fs::File>, buf: &mut Vec<u8>) -> AgentdResult<()> {
1235    while !buf.is_empty() {
1236        let mut guard = fd.writable().await?;
1237        match guard.try_io(|inner| write_to_fd(inner.get_ref().as_raw_fd(), buf)) {
1238            Ok(Ok(n)) => {
1239                buf.drain(..n);
1240            }
1241            Ok(Err(e)) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1242            Ok(Err(e)) => return Err(e.into()),
1243            Err(_would_block) => continue,
1244        }
1245    }
1246    Ok(())
1247}
1248
1249/// Writes to a raw fd (non-blocking).
1250fn write_to_fd(fd: i32, buf: &[u8]) -> std::io::Result<usize> {
1251    let n = unsafe { libc::write(fd, buf.as_ptr() as *const libc::c_void, buf.len()) };
1252    if n < 0 {
1253        Err(std::io::Error::last_os_error())
1254    } else {
1255        Ok(n as usize)
1256    }
1257}
1258
1259fn request_guest_poweroff() -> AgentdResult<()> {
1260    if crate::handoff::is_pid_1() {
1261        // PID 1 mode (no handoff): tear down filesystems so block-backed
1262        // mounts reach a clean terminal state, then power the kernel off.
1263        crate::teardown::teardown_filesystems(true);
1264        let ret = unsafe { libc::reboot(libc::RB_POWER_OFF) };
1265        if ret != 0 {
1266            return Err(std::io::Error::last_os_error().into());
1267        }
1268        return Ok(());
1269    }
1270
1271    unsafe {
1272        libc::sync();
1273    }
1274
1275    // Handoff mode: ask the new init (PID 1) to shut down.
1276    // SIGRTMIN+4 is systemd's poweroff signal; sysvinit-derived inits
1277    // typically default-handle it as a clean exit. Either way, PID 1
1278    // exiting causes the kernel to panic the guest, which the VMM
1279    // observes as a clean shutdown.
1280    if crate::handoff::signal_init_shutdown().is_ok() {
1281        std::thread::sleep(HANDOFF_POWEROFF_TIMEOUT);
1282    }
1283
1284    // Reaching this point means the init ignored the poweroff request, so
1285    // the guest is going down hard (SIGTERM fallback, then the host's
1286    // VMM-process kill as backstop). Force filesystems toward a clean
1287    // terminal state first — without the process sweep, since the foreign
1288    // init's services are not ours to kill.
1289    crate::teardown::teardown_filesystems(false);
1290
1291    let _ = crate::handoff::signal_init_term();
1292    Ok(())
1293}
1294
1295//--------------------------------------------------------------------------------------------------
1296// Tests
1297//--------------------------------------------------------------------------------------------------
1298
1299#[cfg(test)]
1300mod tests {
1301    use super::*;
1302    use microsandbox_protocol::message::PROTOCOL_VERSION;
1303
1304    #[test]
1305    fn coalesced_bootstrap_and_init_ack_retain_the_second_frame() {
1306        let bootstrap = GuestBootstrap::default();
1307        let bootstrap_message =
1308            Message::with_payload(MessageType::Bootstrap, 0, &bootstrap).unwrap();
1309        let ack_message = Message::with_payload(MessageType::InitAck, 0, &InitAck {}).unwrap();
1310        let mut state = BootConsoleState::default();
1311        codec::encode_to_buf(&bootstrap_message, &mut state.input).unwrap();
1312        codec::encode_to_buf(&ack_message, &mut state.input).unwrap();
1313
1314        let decoded = read_boot_message(
1315            -1,
1316            &mut state,
1317            Instant::now() + std::time::Duration::from_secs(1),
1318            "guest bootstrap",
1319        )
1320        .unwrap();
1321        assert_eq!(decode_bootstrap_message(decoded).unwrap(), bootstrap);
1322        assert!(
1323            !state.input.is_empty(),
1324            "init ack frame should remain buffered"
1325        );
1326
1327        wait_for_init_ack(
1328            -1,
1329            &mut state,
1330            Instant::now() + std::time::Duration::from_secs(1),
1331        )
1332        .unwrap();
1333        assert!(state.input.is_empty());
1334    }
1335
1336    #[test]
1337    fn bootstrap_rejects_non_control_correlation_fields() {
1338        let mut message =
1339            Message::with_payload(MessageType::Bootstrap, 1, &GuestBootstrap::default()).unwrap();
1340        message.flags = 1;
1341
1342        let error = decode_bootstrap_message(message).unwrap_err();
1343        assert!(error.to_string().contains("requires id=0 and flags=0"));
1344    }
1345
1346    #[test]
1347    fn bootstrap_rejects_wrong_first_message_type() {
1348        let message = Message::with_payload(MessageType::Ping, 0, &Ping {}).unwrap();
1349
1350        let error = decode_bootstrap_message(message).unwrap_err();
1351        assert!(error.to_string().contains("expected core.bootstrap"));
1352    }
1353
1354    #[test]
1355    fn bootstrap_rejects_older_protocol_generation() {
1356        let mut message =
1357            Message::with_payload(MessageType::Bootstrap, 0, &GuestBootstrap::default()).unwrap();
1358        message.v = PROTOCOL_VERSION - 1;
1359
1360        let error = decode_bootstrap_message(message).unwrap_err();
1361        assert!(error.to_string().contains("or newer"));
1362    }
1363
1364    #[test]
1365    fn bootstrap_accepts_newer_additive_protocol_generation() {
1366        let mut message =
1367            Message::with_payload(MessageType::Bootstrap, 0, &GuestBootstrap::default()).unwrap();
1368        message.v = PROTOCOL_VERSION + 1;
1369
1370        assert_eq!(
1371            decode_bootstrap_message(message).unwrap(),
1372            GuestBootstrap::default()
1373        );
1374    }
1375
1376    #[test]
1377    fn bootstrap_rejects_malformed_payload() {
1378        let message = Message::new(MessageType::Bootstrap, 0, vec![0xff]);
1379
1380        let error = decode_bootstrap_message(message).unwrap_err();
1381        assert!(error.to_string().contains("decode guest bootstrap payload"));
1382    }
1383
1384    #[test]
1385    fn record_encoded_guest_messages_counts_only_appended_frames() {
1386        let mut out_buf = Vec::new();
1387        let existing =
1388            Message::with_payload(MessageType::ExecStarted, 1, &ExecStarted { pid: 123 }).unwrap();
1389        codec::encode_to_buf(&existing, &mut out_buf).unwrap();
1390        let start = out_buf.len();
1391
1392        let appended =
1393            Message::with_payload(MessageType::ExecStarted, 2, &ExecStarted { pid: 456 }).unwrap();
1394        codec::encode_to_buf(&appended, &mut out_buf).unwrap();
1395
1396        let mut activity = ActivityTracker::new();
1397        record_encoded_guest_messages(&out_buf, start, &mut activity);
1398
1399        assert_eq!(activity.activity_seq, 1);
1400        assert_eq!(activity.counters.guest_messages, 1);
1401    }
1402
1403    #[test]
1404    fn apply_raw_activity_updates_guest_and_byte_counters() {
1405        let mut activity = ActivityTracker::new();
1406
1407        apply_raw_activity(RawActivity::fs_bytes(42), &mut activity);
1408        apply_raw_activity(RawActivity::tcp_bytes(7), &mut activity);
1409
1410        assert_eq!(activity.activity_seq, 2);
1411        assert_eq!(activity.counters.guest_messages, 2);
1412        assert_eq!(activity.counters.fs_bytes, 42);
1413        assert_eq!(activity.counters.tcp_bytes, 7);
1414    }
1415
1416    #[test]
1417    fn maintenance_messages_do_not_implicitly_refresh_idle_timer() {
1418        assert!(!message_refreshes_idle_timer(&MessageType::ClockSync));
1419        assert!(!message_refreshes_idle_timer(&MessageType::Ping));
1420        assert!(!message_refreshes_idle_timer(&MessageType::Touch));
1421        assert!(message_refreshes_idle_timer(&MessageType::ExecRequest));
1422    }
1423
1424    #[test]
1425    fn maintenance_replies_do_not_refresh_idle_timer() {
1426        assert!(!guest_message_refreshes_idle_timer(&MessageType::Pong));
1427        assert!(!guest_message_refreshes_idle_timer(&MessageType::Touched));
1428        assert!(!guest_message_refreshes_idle_timer(&MessageType::CoreError));
1429        assert!(guest_message_refreshes_idle_timer(&MessageType::ExecStdout));
1430    }
1431
1432    #[test]
1433    fn record_encoded_guest_messages_ignores_pong_and_touched() {
1434        let mut out_buf = Vec::new();
1435        let pong = Message::with_payload(MessageType::Pong, 1, &Pong {}).unwrap();
1436        codec::encode_to_buf(&pong, &mut out_buf).unwrap();
1437
1438        let touched =
1439            Message::with_payload(MessageType::Touched, 2, &Touched { activity_seq: 42 }).unwrap();
1440        codec::encode_to_buf(&touched, &mut out_buf).unwrap();
1441
1442        let mut activity = ActivityTracker::new();
1443        record_encoded_guest_messages(&out_buf, 0, &mut activity);
1444
1445        assert_eq!(activity.activity_seq, 0);
1446        assert_eq!(activity.counters.guest_messages, 0);
1447    }
1448}