1use std::collections::VecDeque;
12
13use tokio::sync::{mpsc, watch};
14
15use super::workload::{WorkloadClaim, WorkloadSlot};
16use super::*;
17use crate::lifecycle::actor::WorkloadOutcome;
18
19const CHANNEL_RETENTION: usize = 8 * 1024 * 1024;
23
24const EXITED_RETENTION: Duration = Duration::from_secs(5 * 60);
27
28const ATTACH_CHUNK: usize = 64 * 1024;
30
31const ATTACH_QUEUE: usize = 64;
33
34const MAX_START_RESERVE_ROUNDS: usize = 8;
37
38#[derive(Debug, Clone, Default)]
40pub struct ExecutionSpec {
41 pub id: Option<String>,
43 pub cmd: Vec<String>,
45 pub env: HashMap<String, String>,
47 pub working_dir: String,
49 pub user: String,
51 pub tty: bool,
53 pub tty_size: Option<(u16, u16)>,
55 pub timeout_seconds: u32,
57 pub stdin: bool,
60}
61
62#[derive(Debug, Clone)]
64pub struct ExecutionSnapshot {
65 pub id: String,
67 pub sandbox_id: SandboxId,
69 pub tty: bool,
71 pub started_at: DateTime<Utc>,
73 pub exited_at: Option<DateTime<Utc>>,
75 pub exit_status: Option<ExitStatus>,
78 pub error: Option<String>,
80 pub stdout_len: u64,
82 pub stderr_len: u64,
84 pub stdin: StdinState,
86}
87
88impl ExecutionSnapshot {
89 #[must_use]
91 pub const fn is_running(&self) -> bool {
92 self.exited_at.is_none()
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum ExecutionChannel {
100 Stdout,
102 Stderr,
104}
105
106#[derive(Debug, Clone)]
108pub struct ExecutionOutput {
109 pub channel: ExecutionChannel,
111 pub offset: u64,
114 pub data: Vec<u8>,
116}
117
118#[derive(Debug, Clone, Copy, Default)]
120pub struct StdinState {
121 pub bytes_written: u64,
124 pub closed: bool,
126}
127
128#[derive(Default)]
130struct ChannelBuffer {
131 base: u64,
133 data: VecDeque<u8>,
134}
135
136impl ChannelBuffer {
137 fn end(&self) -> u64 {
139 self.base + self.data.len() as u64
140 }
141
142 fn append(&mut self, bytes: &[u8]) {
143 self.data.extend(bytes);
144 if self.data.len() > CHANNEL_RETENTION {
145 let drop = self.data.len() - CHANNEL_RETENTION;
146 self.data.drain(..drop);
147 self.base += drop as u64;
148 }
149 }
150
151 fn read_from(&self, offset: u64, max: usize) -> (u64, Vec<u8>) {
154 let start = offset.max(self.base);
155 if start >= self.end() {
156 return (start, Vec::new());
157 }
158 let skip = usize::try_from(start - self.base).expect("offset within retained buffer");
160 let take = (self.data.len() - skip).min(max);
161 let (front, back) = self.data.as_slices();
162 let mut out = Vec::with_capacity(take);
163 if skip < front.len() {
164 let n = (front.len() - skip).min(take);
165 out.extend_from_slice(&front[skip..skip + n]);
166 out.extend_from_slice(&back[..take - n]);
167 } else {
168 let off = skip - front.len();
169 out.extend_from_slice(&back[off..off + take]);
170 }
171 (start, out)
172 }
173}
174
175#[derive(Default)]
178struct ExecState {
179 stdout: ChannelBuffer,
180 stderr: ChannelBuffer,
181 stdin_written: u64,
182 stdin_closed: bool,
183 exited_at: Option<DateTime<Utc>>,
184 exit_status: Option<ExitStatus>,
185 error: Option<String>,
186}
187
188pub(super) struct Execution {
190 id: String,
191 sandbox_id: SandboxId,
192 tty: bool,
193 started_at: DateTime<Utc>,
194 cmd: Vec<String>,
197 state: Mutex<ExecState>,
198 version: watch::Sender<u64>,
201 input_tx: mpsc::Sender<ExecInputMsg>,
203 stdin_gate: tokio::sync::Mutex<()>,
205}
206
207impl Execution {
208 fn new(
209 id: String,
210 sandbox_id: SandboxId,
211 spec: &ExecutionSpec,
212 input_tx: mpsc::Sender<ExecInputMsg>,
213 ) -> Self {
214 Self {
215 id,
216 sandbox_id,
217 tty: spec.tty,
218 started_at: Utc::now(),
219 cmd: spec.cmd.clone(),
220 state: Mutex::new(ExecState {
221 stdin_closed: !spec.stdin,
222 ..ExecState::default()
223 }),
224 version: watch::channel(0).0,
225 input_tx,
226 stdin_gate: tokio::sync::Mutex::new(()),
227 }
228 }
229
230 fn snapshot(&self) -> ExecutionSnapshot {
231 let st = self.state.lock().unwrap();
232 ExecutionSnapshot {
233 id: self.id.clone(),
234 sandbox_id: self.sandbox_id.clone(),
235 tty: self.tty,
236 started_at: self.started_at,
237 exited_at: st.exited_at,
238 exit_status: st.exit_status,
239 error: st.error.clone(),
240 stdout_len: st.stdout.end(),
241 stderr_len: st.stderr.end(),
242 stdin: StdinState {
243 bytes_written: st.stdin_written,
244 closed: st.stdin_closed,
245 },
246 }
247 }
248
249 fn bump(&self) {
250 self.version.send_modify(|v| *v += 1);
251 }
252
253 fn mark_exited(&self, outcome: &std::result::Result<ExitStatus, String>) {
257 let mut st = self.state.lock().unwrap();
258 if st.exited_at.is_some() {
259 return;
260 }
261 st.exited_at = Some(Utc::now());
262 match outcome {
263 Ok(status) => st.exit_status = Some(*status),
264 Err(e) => st.error = Some(e.clone()),
265 }
266 drop(st);
267 self.bump();
268 }
269
270 fn has_exited(&self) -> bool {
271 self.state.lock().unwrap().exited_at.is_some()
272 }
273
274 fn exited_error(&self) -> VmmError {
276 VmmError::WrongState {
277 id: format!("execution '{}'", self.id),
278 expected: "running".into(),
279 actual: "exited".into(),
280 }
281 }
282
283 fn attach(
286 self: &Arc<Self>,
287 stdout_offset: u64,
288 stderr_offset: u64,
289 ) -> (ExecutionSnapshot, mpsc::Receiver<ExecutionOutput>) {
290 let snapshot = self.snapshot();
291 let (tx, rx) = mpsc::channel(ATTACH_QUEUE);
292 tokio::spawn(pump_attach(
293 Arc::clone(self),
294 stdout_offset,
295 stderr_offset,
296 tx,
297 ));
298 (snapshot, rx)
299 }
300
301 async fn write_stdin(&self, offset: u64, data: &[u8], eof: bool) -> Result<StdinState> {
303 if eof && self.tty {
304 return Err(VmmError::Config(
305 "cannot close stdin of a TTY execution; send Ctrl-D (0x04) instead".into(),
306 ));
307 }
308 let _gate = self.stdin_gate.lock().await;
311
312 let fresh = {
313 let st = self.state.lock().unwrap();
314 if st.exited_at.is_some() {
315 return Err(self.exited_error());
316 }
317 if offset > st.stdin_written {
318 return Err(VmmError::StdinGap {
319 accepted: st.stdin_written,
320 offset,
321 });
322 }
323 let overlap = st.stdin_written - offset;
324 if overlap >= data.len() as u64 {
325 Vec::new()
327 } else {
328 if st.stdin_closed {
329 return Err(VmmError::Config("stdin is already closed".into()));
330 }
331 #[allow(
332 clippy::cast_possible_truncation,
333 reason = "overlap < data.len() which is usize"
334 )]
335 let skip = overlap as usize;
336 data[skip..].to_vec()
337 }
338 };
339
340 if !fresh.is_empty() {
341 let accepted = fresh.len() as u64;
342 self.input_tx
343 .send(ExecInputMsg::Stdin(fresh))
344 .await
345 .map_err(|_| self.exited_error())?;
346 self.state.lock().unwrap().stdin_written += accepted;
347 self.bump();
348 }
349 if eof && !self.state.lock().unwrap().stdin_closed {
350 let _ = self.input_tx.send(ExecInputMsg::Eof).await;
353 self.state.lock().unwrap().stdin_closed = true;
354 self.bump();
355 }
356
357 Ok(self.snapshot().stdin)
358 }
359
360 async fn signal(&self, signal: i32) -> Result<()> {
362 if !(1..=64).contains(&signal) {
363 return Err(VmmError::Config(format!("invalid signal {signal}")));
364 }
365 if self.has_exited() {
366 return Err(self.exited_error());
367 }
368 self.input_tx
369 .send(ExecInputMsg::Signal(signal))
370 .await
371 .map_err(|_| self.exited_error())
372 }
373
374 async fn resize(&self, width: u16, height: u16) -> Result<()> {
376 if !self.tty {
377 return Err(VmmError::Config("execution has no TTY to resize".into()));
378 }
379 if self.has_exited() {
380 return Err(self.exited_error());
381 }
382 self.input_tx
383 .send(ExecInputMsg::Resize { width, height })
384 .await
385 .map_err(|_| self.exited_error())
386 }
387
388 async fn wait(&self, timeout: Duration) -> ExecutionSnapshot {
390 let mut version = self.version.subscribe();
391 let deadline = tokio::time::Instant::now() + timeout;
392 loop {
393 version.borrow_and_update();
396 if self.has_exited() || timeout.is_zero() {
397 return self.snapshot();
398 }
399 if tokio::time::timeout_at(deadline, version.changed())
400 .await
401 .is_err()
402 {
403 return self.snapshot();
404 }
405 }
406 }
407}
408
409type ExecKey = (SandboxId, String);
410
411#[derive(Default)]
413pub(super) struct ExecutionRegistry {
414 inner: Mutex<RegistryInner>,
415}
416
417#[derive(Default)]
418struct RegistryInner {
419 live: HashMap<ExecKey, Arc<Execution>>,
420 pending: HashMap<ExecKey, PendingStart>,
423}
424
425struct PendingStart {
427 cmd: Vec<String>,
430 done: watch::Sender<bool>,
432}
433
434enum Reserve {
436 Existing(ExecutionSnapshot),
438 Slot(SlotGuard),
440 AwaitPending(watch::Receiver<bool>),
442}
443
444struct SlotGuard {
446 registry: Arc<ExecutionRegistry>,
447 key: ExecKey,
448 committed: bool,
449}
450
451impl SlotGuard {
452 fn commit(mut self, exec: &Arc<Execution>) {
453 let mut inner = self.registry.inner.lock().unwrap();
454 let pending = inner.pending.remove(&self.key);
455 inner.live.insert(self.key.clone(), Arc::clone(exec));
456 drop(inner);
457 if let Some(pending) = pending {
460 let _ = pending.done.send(true);
461 }
462 self.committed = true;
463 }
464}
465
466impl Drop for SlotGuard {
467 fn drop(&mut self) {
468 if !self.committed {
469 let pending = self
470 .registry
471 .inner
472 .lock()
473 .unwrap()
474 .pending
475 .remove(&self.key);
476 if let Some(pending) = pending {
477 let _ = pending.done.send(true);
478 }
479 }
480 }
481}
482
483impl ExecutionRegistry {
484 fn reserve(self: &Arc<Self>, key: ExecKey, cmd: &[String]) -> Result<Reserve> {
485 let mut inner = self.inner.lock().unwrap();
486 if let Some(existing) = inner.live.get(&key) {
487 if existing.cmd == cmd {
488 return Ok(Reserve::Existing(existing.snapshot()));
489 }
490 return Err(VmmError::AlreadyExists(format!(
491 "execution '{}' (id reused for a different command)",
492 key.1
493 )));
494 }
495 if let Some(pending) = inner.pending.get(&key) {
496 if pending.cmd == cmd {
497 return Ok(Reserve::AwaitPending(pending.done.subscribe()));
501 }
502 return Err(VmmError::AlreadyExists(format!(
503 "execution '{}' (id reused for a different command)",
504 key.1
505 )));
506 }
507 inner.pending.insert(
508 key.clone(),
509 PendingStart {
510 cmd: cmd.to_vec(),
511 done: watch::channel(false).0,
512 },
513 );
514 Ok(Reserve::Slot(SlotGuard {
515 registry: Arc::clone(self),
516 key,
517 committed: false,
518 }))
519 }
520
521 fn get(&self, sandbox_id: &str, execution_id: &str) -> Result<Arc<Execution>> {
522 self.inner
523 .lock()
524 .unwrap()
525 .live
526 .get(&(sandbox_id.to_owned(), execution_id.to_owned()))
527 .cloned()
528 .ok_or_else(|| {
529 VmmError::NotFound(format!(
530 "execution '{execution_id}' in sandbox '{sandbox_id}'"
531 ))
532 })
533 }
534
535 fn list(&self, sandbox_id: &str) -> Vec<ExecutionSnapshot> {
538 self.inner
539 .lock()
540 .unwrap()
541 .live
542 .iter()
543 .filter(|((sid, _), _)| sid == sandbox_id)
544 .map(|(_, exec)| exec.snapshot())
545 .collect()
546 }
547
548 fn remove_generation(&self, exec: &Arc<Execution>) {
550 let key = (exec.sandbox_id.clone(), exec.id.clone());
551 let mut inner = self.inner.lock().unwrap();
552 if inner.live.get(&key).is_some_and(|e| Arc::ptr_eq(e, exec)) {
553 inner.live.remove(&key);
554 }
555 }
556
557 fn interrupt_sandbox(&self, sandbox_id: &str) {
562 let executions: Vec<Arc<Execution>> = {
563 let inner = self.inner.lock().unwrap();
564 inner
565 .live
566 .iter()
567 .filter(|((sid, _), _)| sid == sandbox_id)
568 .map(|(_, exec)| Arc::clone(exec))
569 .collect()
570 };
571 for exec in executions {
572 exec.mark_exited(&Err("sandbox stopped".to_owned()));
573 }
574 }
575}
576
577pub(super) fn spawn_teardown_purge(
581 registry: Arc<ExecutionRegistry>,
582 mut events: broadcast::Receiver<SandboxEvent>,
583) {
584 tokio::spawn(async move {
585 loop {
586 match events.recv().await {
587 Ok(ev) if ev.is_terminal() => {
588 registry.interrupt_sandbox(&ev.sandbox_id);
589 }
590 Ok(_) | Err(broadcast::error::RecvError::Lagged(_)) => {}
591 Err(broadcast::error::RecvError::Closed) => break,
592 }
593 }
594 });
595}
596
597async fn run_session(
600 exec: Arc<Execution>,
601 mut output_rx: mpsc::Receiver<Result<OutputChunk>>,
602 slot: Arc<dyn WorkloadSlot>,
603 registry: Arc<ExecutionRegistry>,
604) {
605 let outcome: std::result::Result<ExitStatus, String> = loop {
606 match output_rx.recv().await {
607 Some(Ok(OutputChunk::Stdout(data))) => {
608 exec.state.lock().unwrap().stdout.append(&data);
609 exec.bump();
610 }
611 Some(Ok(OutputChunk::Stderr(data))) => {
612 exec.state.lock().unwrap().stderr.append(&data);
613 exec.bump();
614 }
615 Some(Ok(OutputChunk::Exit(status))) => break Ok(status),
616 Some(Err(e)) => break Err(e.to_string()),
617 None => break Err("execution session closed before exit".to_owned()),
618 }
619 };
620
621 match &outcome {
622 Ok(status) => slot.exited(WorkloadOutcome::Exited(*status)),
623 Err(e) => {
627 warn!(sandbox_id = %exec.sandbox_id, execution_id = %exec.id, error = %e,
628 "execution session broke before exit");
629 slot.exited(WorkloadOutcome::Broke(e.clone()));
630 }
631 }
632 exec.mark_exited(&outcome);
633
634 tokio::time::sleep(EXITED_RETENTION).await;
635 registry.remove_generation(&exec);
636}
637
638impl SandboxManager {
639 pub async fn start_execution(
646 &self,
647 sandbox_id: &SandboxId,
648 spec: ExecutionSpec,
649 ) -> Result<ExecutionSnapshot> {
650 let id = match &spec.id {
651 Some(id) if !id.is_empty() => {
652 super::validate_id("execution id", id)?;
653 id.clone()
654 }
655 _ => Uuid::new_v4().to_string(),
656 };
657 if spec.cmd.is_empty() {
658 return Err(VmmError::Config(
659 "execution command must not be empty".into(),
660 ));
661 }
662
663 let mut reservation = None;
668 for _ in 0..MAX_START_RESERVE_ROUNDS {
669 match self
670 .executions
671 .reserve((sandbox_id.clone(), id.clone()), &spec.cmd)?
672 {
673 Reserve::Existing(snapshot) => return Ok(snapshot),
674 Reserve::Slot(guard) => {
675 reservation = Some(guard);
676 break;
677 }
678 Reserve::AwaitPending(mut done) => {
679 let _ = done.changed().await;
680 }
681 }
682 }
683 let Some(reservation) = reservation else {
684 return Err(VmmError::AlreadyExists(format!(
685 "execution '{id}' (concurrent starts did not settle)"
686 )));
687 };
688
689 let agent = self.require_ready_agent(sandbox_id)?;
690 let start = StartCommand {
691 cmd: spec.cmd.clone(),
692 env: spec.env.clone(),
693 working_dir: spec.working_dir.clone(),
694 user: spec.user.clone(),
695 tty: spec.tty,
696 tty_width: spec.tty_size.map_or(80, |(w, _)| w),
697 tty_height: spec.tty_size.map_or(24, |(_, h)| h),
698 timeout_seconds: spec.timeout_seconds,
699 };
700
701 let slot = self.workload_slot(sandbox_id)?;
705 slot.claim(WorkloadClaim::Api).await?;
706
707 let (input_tx, output_rx) = match agent.exec(start).await {
708 Ok(pair) => pair,
709 Err(e) => {
710 slot.release();
711 return Err(e);
712 }
713 };
714 if !spec.stdin && !spec.tty {
715 let _ = input_tx.send(ExecInputMsg::Eof).await;
720 }
721
722 let exec = Arc::new(Execution::new(id, sandbox_id.clone(), &spec, input_tx));
723 reservation.commit(&exec);
724
725 tokio::spawn(run_session(
726 Arc::clone(&exec),
727 output_rx,
728 slot,
729 Arc::clone(&self.executions),
730 ));
731
732 Ok(exec.snapshot())
733 }
734
735 pub fn attach_execution(
743 &self,
744 sandbox_id: &str,
745 execution_id: &str,
746 stdout_offset: u64,
747 stderr_offset: u64,
748 ) -> Result<(ExecutionSnapshot, mpsc::Receiver<ExecutionOutput>)> {
749 let exec = self.executions.get(sandbox_id, execution_id)?;
750 Ok(exec.attach(stdout_offset, stderr_offset))
751 }
752
753 pub async fn write_stdin(
761 &self,
762 sandbox_id: &str,
763 execution_id: &str,
764 offset: u64,
765 data: &[u8],
766 eof: bool,
767 ) -> Result<StdinState> {
768 self.executions
769 .get(sandbox_id, execution_id)?
770 .write_stdin(offset, data, eof)
771 .await
772 }
773
774 pub fn stdin_status(&self, sandbox_id: &str, execution_id: &str) -> Result<StdinState> {
776 Ok(self
777 .executions
778 .get(sandbox_id, execution_id)?
779 .snapshot()
780 .stdin)
781 }
782
783 pub async fn signal_execution(
785 &self,
786 sandbox_id: &str,
787 execution_id: &str,
788 signal: i32,
789 ) -> Result<()> {
790 self.executions
791 .get(sandbox_id, execution_id)?
792 .signal(signal)
793 .await
794 }
795
796 pub async fn resize_execution(
798 &self,
799 sandbox_id: &str,
800 execution_id: &str,
801 width: u16,
802 height: u16,
803 ) -> Result<()> {
804 self.executions
805 .get(sandbox_id, execution_id)?
806 .resize(width, height)
807 .await
808 }
809
810 pub async fn wait_execution(
813 &self,
814 sandbox_id: &str,
815 execution_id: &str,
816 timeout: Duration,
817 ) -> Result<ExecutionSnapshot> {
818 Ok(self
819 .executions
820 .get(sandbox_id, execution_id)?
821 .wait(timeout)
822 .await)
823 }
824
825 pub fn list_executions(&self, sandbox_id: &SandboxId) -> Result<Vec<ExecutionSnapshot>> {
831 self.computer(sandbox_id)?;
832 let mut snapshots = self.executions.list(sandbox_id);
833 snapshots.sort_by(|a, b| {
834 a.started_at
835 .cmp(&b.started_at)
836 .then_with(|| a.id.cmp(&b.id))
837 });
838 Ok(snapshots)
839 }
840
841 pub async fn wait_sandbox_port(
848 &self,
849 id: &SandboxId,
850 port: u16,
851 timeout: Duration,
852 ) -> Result<()> {
853 let agent = self.require_alive_agent(id)?;
854 match agent.wait_for_port(port, timeout).await? {
855 crate::agent::PortWait::Listening => Ok(()),
856 crate::agent::PortWait::Deadline => Err(VmmError::DeadlineExceeded(format!(
857 "no listener on port {port} in sandbox '{id}' within {}s",
858 timeout.as_secs()
859 ))),
860 }
861 }
862}
863
864async fn pump_attach(
866 exec: Arc<Execution>,
867 mut stdout_cursor: u64,
868 mut stderr_cursor: u64,
869 tx: mpsc::Sender<ExecutionOutput>,
870) {
871 let mut version = exec.version.subscribe();
872 loop {
873 version.borrow_and_update();
876 loop {
877 let (chunk, exited) = {
878 let st = exec.state.lock().unwrap();
879 let (start, data) = st.stdout.read_from(stdout_cursor, ATTACH_CHUNK);
880 if data.is_empty() {
881 let (start, data) = st.stderr.read_from(stderr_cursor, ATTACH_CHUNK);
882 if data.is_empty() {
883 (None, st.exited_at.is_some())
884 } else {
885 stderr_cursor = start + data.len() as u64;
886 (
887 Some(ExecutionOutput {
888 channel: ExecutionChannel::Stderr,
889 offset: start,
890 data,
891 }),
892 false,
893 )
894 }
895 } else {
896 stdout_cursor = start + data.len() as u64;
897 (
898 Some(ExecutionOutput {
899 channel: ExecutionChannel::Stdout,
900 offset: start,
901 data,
902 }),
903 false,
904 )
905 }
906 };
907 match chunk {
908 Some(chunk) => {
909 if tx.send(chunk).await.is_err() {
910 return; }
912 }
913 None if exited => return, None => break, }
916 }
917 let _ = version.changed().await;
919 }
920}
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925
926 fn stdin_spec() -> ExecutionSpec {
927 ExecutionSpec {
928 cmd: vec!["cat".into()],
929 stdin: true,
930 ..ExecutionSpec::default()
931 }
932 }
933
934 fn test_execution(spec: &ExecutionSpec) -> (Arc<Execution>, mpsc::Receiver<ExecInputMsg>) {
935 let (tx, rx) = mpsc::channel(8);
936 let exec = Arc::new(Execution::new(
937 "exec-1".into(),
938 "sandbox-1".into(),
939 spec,
940 tx,
941 ));
942 (exec, rx)
943 }
944
945 #[test]
946 fn channel_buffer_addresses_bytes_absolutely() {
947 let mut buf = ChannelBuffer::default();
948 buf.append(b"hello ");
949 buf.append(b"world");
950 assert_eq!(buf.end(), 11);
951
952 let (start, data) = buf.read_from(0, 1024);
953 assert_eq!((start, data.as_slice()), (0, b"hello world".as_slice()));
954
955 let (start, data) = buf.read_from(6, 3);
956 assert_eq!((start, data.as_slice()), (6, b"wor".as_slice()));
957
958 let (start, data) = buf.read_from(11, 1024);
960 assert_eq!((start, data.len()), (11, 0));
961 }
962
963 #[test]
964 fn channel_buffer_trims_to_retention_and_advances_base() {
965 let mut buf = ChannelBuffer::default();
966 let chunk = vec![7u8; CHANNEL_RETENTION];
967 buf.append(&chunk);
968 buf.append(b"tail");
969
970 assert_eq!(buf.base, 4);
971 assert_eq!(buf.end(), CHANNEL_RETENTION as u64 + 4);
972
973 let (start, data) = buf.read_from(0, 8);
976 assert_eq!(start, 4);
977 assert_eq!(data.len(), 8);
978 }
979
980 #[tokio::test]
981 async fn write_stdin_dedupes_retries_and_rejects_gaps() {
982 let (exec, mut rx) = test_execution(&stdin_spec());
983
984 let st = exec.write_stdin(0, b"hello", false).await.unwrap();
985 assert_eq!(st.bytes_written, 5);
986
987 let st = exec.write_stdin(0, b"hello", false).await.unwrap();
989 assert_eq!(st.bytes_written, 5);
990
991 let st = exec.write_stdin(3, b"lo world", false).await.unwrap();
993 assert_eq!(st.bytes_written, 11);
994
995 let err = exec.write_stdin(99, b"x", false).await.unwrap_err();
997 assert!(matches!(err, VmmError::StdinGap { accepted: 11, .. }));
998
999 let mut forwarded = Vec::new();
1001 while let Ok(msg) = rx.try_recv() {
1002 match msg {
1003 ExecInputMsg::Stdin(data) => forwarded.extend_from_slice(&data),
1004 other => panic!("unexpected input message: {other:?}"),
1005 }
1006 }
1007 assert_eq!(forwarded, b"hello world");
1008 }
1009
1010 #[tokio::test]
1011 async fn write_stdin_eof_closes_and_tty_rejects_eof() {
1012 let (exec, mut rx) = test_execution(&stdin_spec());
1013
1014 let st = exec.write_stdin(0, b"in", true).await.unwrap();
1015 assert!(st.closed);
1016 assert!(matches!(rx.recv().await, Some(ExecInputMsg::Stdin(_))));
1017 assert!(matches!(rx.recv().await, Some(ExecInputMsg::Eof)));
1018
1019 let err = exec.write_stdin(2, b"more", false).await.unwrap_err();
1021 assert!(matches!(err, VmmError::Config(_)));
1022 let st = exec.write_stdin(0, b"in", false).await.unwrap();
1023 assert_eq!(st.bytes_written, 2);
1024
1025 let tty_spec = ExecutionSpec {
1027 tty: true,
1028 ..stdin_spec()
1029 };
1030 let (tty_exec, _tty_rx) = test_execution(&tty_spec);
1031 let err = tty_exec.write_stdin(0, b"", true).await.unwrap_err();
1032 assert!(matches!(err, VmmError::Config(_)));
1033 }
1034
1035 #[tokio::test]
1036 async fn attach_replays_from_offsets_and_closes_after_exit() {
1037 let (exec, _rx) = test_execution(&stdin_spec());
1038
1039 {
1040 let mut st = exec.state.lock().unwrap();
1041 st.stdout.append(b"out-data");
1042 st.stderr.append(b"err");
1043 }
1044 exec.bump();
1045
1046 let (snapshot, mut out) = exec.attach(2, 0);
1047 assert!(snapshot.is_running());
1048
1049 let first = out.recv().await.unwrap();
1050 assert_eq!(first.channel, ExecutionChannel::Stdout);
1051 assert_eq!(first.offset, 2);
1052 assert_eq!(first.data, b"t-data");
1053 let second = out.recv().await.unwrap();
1054 assert_eq!(second.channel, ExecutionChannel::Stderr);
1055 assert_eq!(second.offset, 0);
1056 assert_eq!(second.data, b"err");
1057
1058 exec.state.lock().unwrap().stdout.append(b"+live");
1060 exec.bump();
1061 let live = out.recv().await.unwrap();
1062 assert_eq!(live.offset, 8);
1063 assert_eq!(live.data, b"+live");
1064
1065 exec.mark_exited(&Ok(ExitStatus::Code(0)));
1067 assert!(out.recv().await.is_none());
1068
1069 let done = exec.wait(Duration::ZERO).await;
1070 assert_eq!(done.exit_status, Some(ExitStatus::Code(0)));
1071 }
1072
1073 #[tokio::test]
1074 async fn wait_times_out_then_resolves_on_exit() {
1075 let (exec, _rx) = test_execution(&stdin_spec());
1076
1077 assert!(exec.wait(Duration::ZERO).await.is_running());
1079 assert!(exec.wait(Duration::from_millis(20)).await.is_running());
1081
1082 let exiter = {
1084 let exec = Arc::clone(&exec);
1085 tokio::spawn(async move {
1086 tokio::time::sleep(Duration::from_millis(20)).await;
1087 exec.mark_exited(&Ok(ExitStatus::Signaled(9)));
1088 })
1089 };
1090 let snap = exec.wait(Duration::from_secs(5)).await;
1091 assert_eq!(snap.exit_status, Some(ExitStatus::Signaled(9)));
1092 exiter.await.unwrap();
1093 }
1094
1095 #[tokio::test]
1096 async fn signal_and_resize_validate_state() {
1097 let (exec, mut rx) = test_execution(&stdin_spec());
1098
1099 exec.signal(15).await.unwrap();
1100 assert!(matches!(rx.recv().await, Some(ExecInputMsg::Signal(15))));
1101 assert!(matches!(
1102 exec.signal(0).await.unwrap_err(),
1103 VmmError::Config(_)
1104 ));
1105 assert!(matches!(
1107 exec.resize(80, 24).await.unwrap_err(),
1108 VmmError::Config(_)
1109 ));
1110
1111 exec.mark_exited(&Ok(ExitStatus::Code(0)));
1112 assert!(matches!(
1113 exec.signal(9).await.unwrap_err(),
1114 VmmError::WrongState { .. }
1115 ));
1116 }
1117
1118 #[tokio::test]
1119 async fn interrupt_sandbox_marks_running_executions_but_keeps_them_readable() {
1120 let registry = Arc::new(ExecutionRegistry::default());
1121 let (exec, _rx) = test_execution(&stdin_spec());
1122 registry
1123 .inner
1124 .lock()
1125 .unwrap()
1126 .live
1127 .insert(("sandbox-1".into(), "exec-1".into()), Arc::clone(&exec));
1128
1129 registry.interrupt_sandbox("sandbox-1");
1130 assert!(registry.get("sandbox-1", "exec-1").is_ok());
1133 let snap = exec.snapshot();
1134 assert!(!snap.is_running());
1135 assert_eq!(snap.error.as_deref(), Some("sandbox stopped"));
1136
1137 let (done, _rx2) = test_execution(&stdin_spec());
1139 done.mark_exited(&Ok(ExitStatus::Code(3)));
1140 registry
1141 .inner
1142 .lock()
1143 .unwrap()
1144 .live
1145 .insert(("sandbox-1".into(), "exec-2".into()), Arc::clone(&done));
1146 registry.interrupt_sandbox("sandbox-1");
1147 assert_eq!(done.snapshot().exit_status, Some(ExitStatus::Code(3)));
1148 }
1149
1150 #[tokio::test]
1151 async fn start_reservation_is_idempotent_per_command() {
1152 let registry = Arc::new(ExecutionRegistry::default());
1153 let key = ("s".to_owned(), "e".to_owned());
1154 let cmd = vec!["echo".to_owned()];
1155
1156 let Reserve::Slot(slot) = registry.reserve(key.clone(), &cmd).unwrap() else {
1158 panic!("expected a fresh slot");
1159 };
1160 let (tx, _rx) = mpsc::channel(1);
1161 let exec = Arc::new(Execution::new(
1162 "e".into(),
1163 "s".into(),
1164 &ExecutionSpec {
1165 cmd: cmd.clone(),
1166 ..ExecutionSpec::default()
1167 },
1168 tx,
1169 ));
1170 slot.commit(&exec);
1171
1172 assert!(matches!(
1174 registry.reserve(key.clone(), &cmd).unwrap(),
1175 Reserve::Existing(_)
1176 ));
1177 assert!(matches!(
1179 registry.reserve(key, &["other".to_owned()]),
1180 Err(VmmError::AlreadyExists(_))
1181 ));
1182
1183 let key2 = ("s".to_owned(), "e2".to_owned());
1188 {
1189 let Reserve::Slot(_slot) = registry.reserve(key2.clone(), &cmd).unwrap() else {
1190 panic!("expected a fresh slot");
1191 };
1192 assert!(matches!(
1193 registry.reserve(key2.clone(), &cmd).unwrap(),
1194 Reserve::AwaitPending(_)
1195 ));
1196 assert!(matches!(
1197 registry.reserve(key2.clone(), &["other".to_owned()]),
1198 Err(VmmError::AlreadyExists(_))
1199 ));
1200 }
1201 assert!(matches!(
1203 registry.reserve(key2, &cmd).unwrap(),
1204 Reserve::Slot(_)
1205 ));
1206 }
1207
1208 #[tokio::test]
1209 async fn a_pending_start_wakes_matching_retries_with_the_committed_execution() {
1210 let registry = Arc::new(ExecutionRegistry::default());
1211 let key = ("s".to_owned(), "e".to_owned());
1212 let cmd = vec!["sleep".to_owned()];
1213
1214 let Reserve::Slot(slot) = registry.reserve(key.clone(), &cmd).unwrap() else {
1215 panic!("expected a fresh slot");
1216 };
1217 let Reserve::AwaitPending(mut waiter) = registry.reserve(key.clone(), &cmd).unwrap() else {
1218 panic!("a matching retry must await the in-flight start");
1219 };
1220
1221 let (tx, _rx) = mpsc::channel(1);
1223 let exec = Arc::new(Execution::new(
1224 "e".into(),
1225 "s".into(),
1226 &ExecutionSpec {
1227 cmd: cmd.clone(),
1228 ..ExecutionSpec::default()
1229 },
1230 tx,
1231 ));
1232 slot.commit(&exec);
1233
1234 waiter.changed().await.expect("pending start signals");
1235 assert!(matches!(
1236 registry.reserve(key.clone(), &cmd).unwrap(),
1237 Reserve::Existing(_)
1238 ));
1239
1240 let key2 = ("s".to_owned(), "e2".to_owned());
1243 let Reserve::Slot(failed) = registry.reserve(key2.clone(), &cmd).unwrap() else {
1244 panic!("expected a fresh slot");
1245 };
1246 let Reserve::AwaitPending(mut waiter) = registry.reserve(key2.clone(), &cmd).unwrap()
1247 else {
1248 panic!("a matching retry must await the in-flight start");
1249 };
1250 drop(failed);
1251 waiter.changed().await.expect("unwound start signals");
1252 assert!(matches!(
1253 registry.reserve(key2, &cmd).unwrap(),
1254 Reserve::Slot(_)
1255 ));
1256 }
1257}