1use std::path::PathBuf;
4use std::process::{Command, Stdio};
5
6use a3s_box_core::error::{BoxError, Result};
7use async_trait::async_trait;
8
9use super::handler::ShimHandler;
10use super::provider::VmmProvider;
11use super::spec::InstanceSpec;
12use super::VmHandler;
13
14#[cfg(unix)]
15fn clear_fd_close_on_exec(descriptor: i32) -> std::io::Result<()> {
16 let flags = unsafe { libc::fcntl(descriptor, libc::F_GETFD) };
17 if flags == -1
18 || unsafe { libc::fcntl(descriptor, libc::F_SETFD, flags & !libc::FD_CLOEXEC) } == -1
19 {
20 return Err(std::io::Error::last_os_error());
21 }
22 Ok(())
23}
24
25#[cfg(target_os = "windows")]
26struct StandardHandleInheritanceGuard {
27 _spawn_lock: std::sync::MutexGuard<'static, ()>,
28 changed_handles: Vec<windows_sys::Win32::Foundation::HANDLE>,
29}
30
31#[cfg(target_os = "windows")]
32impl StandardHandleInheritanceGuard {
33 fn acquire() -> std::io::Result<Self> {
34 use windows_sys::Win32::System::Console::{
35 GetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
36 };
37
38 static SPAWN_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
39 let spawn_lock = SPAWN_LOCK
40 .lock()
41 .unwrap_or_else(|poisoned| poisoned.into_inner());
42 let mut guard = Self {
43 _spawn_lock: spawn_lock,
44 changed_handles: Vec::new(),
45 };
46 for standard_handle in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
47 let handle = unsafe { GetStdHandle(standard_handle) };
48 match clear_handle_inherit_flag(handle) {
49 Ok(true) => guard.changed_handles.push(handle),
50 Ok(false) => {}
51 Err(error) => {
52 let error = std::io::Error::new(
53 error.kind(),
54 format!(
55 "failed to clear inheritance for standard handle {standard_handle}: {error}"
56 ),
57 );
58 if let Err(rollback_error) = guard.restore() {
59 return Err(std::io::Error::new(
60 error.kind(),
61 format!("{error}; rollback also failed: {rollback_error}"),
62 ));
63 }
64 return Err(error);
65 }
66 }
67 }
68
69 Ok(guard)
70 }
71
72 fn restore(&mut self) -> std::io::Result<()> {
73 use windows_sys::Win32::Foundation::{SetHandleInformation, HANDLE_FLAG_INHERIT};
74
75 let mut first_error = None;
76 let mut failed_handles = Vec::new();
77 for handle in std::mem::take(&mut self.changed_handles) {
78 if unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) }
79 == 0
80 {
81 failed_handles.push(handle);
82 if first_error.is_none() {
83 first_error = Some(std::io::Error::last_os_error());
84 }
85 }
86 }
87 self.changed_handles = failed_handles;
88 match first_error {
89 Some(error) => Err(error),
90 None => Ok(()),
91 }
92 }
93}
94
95#[cfg(target_os = "windows")]
96impl Drop for StandardHandleInheritanceGuard {
97 fn drop(&mut self) {
98 if let Err(error) = self.restore() {
99 tracing::warn!(
100 %error,
101 "Failed to restore standard-handle inheritance after shim spawn"
102 );
103 }
104 }
105}
106
107#[cfg(target_os = "windows")]
108fn clear_handle_inherit_flag(
109 handle: windows_sys::Win32::Foundation::HANDLE,
110) -> std::io::Result<bool> {
111 use windows_sys::Win32::Foundation::{
112 GetHandleInformation, SetHandleInformation, HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE,
113 };
114
115 if handle == 0 || handle == INVALID_HANDLE_VALUE {
116 return Ok(false);
117 }
118
119 let mut flags = 0;
120 if unsafe { GetHandleInformation(handle, &mut flags) } == 0 {
121 return Err(std::io::Error::last_os_error());
122 }
123 if flags & HANDLE_FLAG_INHERIT == 0 {
124 return Ok(false);
125 }
126 if unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0) } == 0 {
127 return Err(std::io::Error::last_os_error());
128 }
129 Ok(true)
130}
131
132pub struct VmController {
138 shim_path: PathBuf,
140}
141
142impl VmController {
143 fn configure_shim_stdio(&self, cmd: &mut Command, spec: &InstanceSpec) {
144 use std::fs::OpenOptions;
145
146 let Some(console_output) = spec.console_output.as_ref() else {
147 cmd.stdout(Stdio::null()).stderr(Stdio::null());
148 return;
149 };
150 let Some(log_dir) = console_output.parent() else {
151 cmd.stdout(Stdio::null()).stderr(Stdio::null());
152 return;
153 };
154 if let Err(error) = std::fs::create_dir_all(log_dir) {
155 tracing::warn!(
156 box_id = %spec.box_id,
157 path = %log_dir.display(),
158 error = %error,
159 "Failed to create shim log directory"
160 );
161 cmd.stdout(Stdio::null()).stderr(Stdio::null());
162 return;
163 }
164
165 let stdout_path = log_dir.join("shim.stdout.log");
166 let stderr_path = log_dir.join("shim.stderr.log");
167
168 let stdout_file = OpenOptions::new()
169 .create(true)
170 .truncate(true)
171 .write(true)
172 .open(&stdout_path);
173 let stderr_file = OpenOptions::new()
174 .create(true)
175 .truncate(true)
176 .write(true)
177 .open(&stderr_path);
178
179 match (stdout_file, stderr_file) {
180 (Ok(stdout_file), Ok(stderr_file)) => {
181 tracing::debug!(
182 box_id = %spec.box_id,
183 stdout = %stdout_path.display(),
184 stderr = %stderr_path.display(),
185 "Redirecting shim stdio to per-box files"
186 );
187 cmd.stdout(Stdio::from(stdout_file))
188 .stderr(Stdio::from(stderr_file));
189 }
190 (stdout_result, stderr_result) => {
191 if let Err(error) = stdout_result {
192 tracing::warn!(
193 box_id = %spec.box_id,
194 path = %stdout_path.display(),
195 error = %error,
196 "Failed to open shim stdout log file"
197 );
198 }
199 if let Err(error) = stderr_result {
200 tracing::warn!(
201 box_id = %spec.box_id,
202 path = %stderr_path.display(),
203 error = %error,
204 "Failed to open shim stderr log file"
205 );
206 }
207 cmd.stdout(Stdio::null()).stderr(Stdio::null());
208 }
209 }
210 }
211
212 pub fn new(shim_path: PathBuf) -> Result<Self> {
221 if !shim_path.exists() {
223 return Err(BoxError::BoxBootError {
224 message: format!("Shim binary not found: {}", shim_path.display()),
225 hint: Some("Build the shim with: cargo build -p a3s-box-shim".to_string()),
226 });
227 }
228
229 #[cfg(target_os = "macos")]
231 Self::ensure_entitlement(&shim_path)?;
232
233 Ok(Self { shim_path })
234 }
235
236 #[cfg(target_os = "macos")]
245 fn ensure_entitlement(shim_path: &std::path::Path) -> Result<()> {
246 use std::fs::File;
247
248 if Self::has_hypervisor_entitlement(shim_path)? {
250 return Ok(());
251 }
252
253 let lock_path = std::env::temp_dir().join("a3s-box-shim-codesign.lock");
255 let lock_file = File::create(&lock_path).map_err(|e| BoxError::BoxBootError {
256 message: format!("Failed to create codesign lock file: {}", e),
257 hint: None,
258 })?;
259
260 let fd = std::os::unix::io::AsRawFd::as_raw_fd(&lock_file);
262 let ret = unsafe { libc::flock(fd, libc::LOCK_EX) };
263 if ret != 0 {
264 return Err(BoxError::BoxBootError {
265 message: format!(
266 "Failed to acquire codesign lock: {}",
267 std::io::Error::last_os_error()
268 ),
269 hint: None,
270 });
271 }
272
273 if Self::has_hypervisor_entitlement(shim_path)? {
275 return Ok(());
277 }
278
279 tracing::info!("Signing shim with Hypervisor.framework entitlement");
280
281 let entitlements_path = Self::find_entitlements_plist(shim_path)?;
282
283 let status = Command::new("codesign")
284 .args(["--entitlements"])
285 .arg(&entitlements_path)
286 .args(["--force", "-s", "-"])
287 .arg(shim_path)
288 .status()
289 .map_err(|e| BoxError::BoxBootError {
290 message: format!("Failed to codesign shim: {}", e),
291 hint: None,
292 })?;
293
294 if !status.success() {
295 return Err(BoxError::BoxBootError {
296 message: "Failed to sign shim with Hypervisor entitlement".to_string(),
297 hint: Some(format!(
298 "Try manually: codesign --entitlements {} --force -s - {}",
299 entitlements_path.display(),
300 shim_path.display()
301 )),
302 });
303 }
304
305 Ok(())
307 }
308
309 #[cfg(target_os = "macos")]
311 fn has_hypervisor_entitlement(shim_path: &std::path::Path) -> Result<bool> {
312 let output = Command::new("codesign")
313 .args(["-d", "--entitlements", "-", "--xml"])
314 .arg(shim_path)
315 .output()
316 .map_err(|e| BoxError::BoxBootError {
317 message: format!("Failed to check entitlements: {}", e),
318 hint: None,
319 })?;
320
321 let stdout = String::from_utf8_lossy(&output.stdout);
322 Ok(stdout.contains("com.apple.security.hypervisor"))
323 }
324
325 #[cfg(target_os = "macos")]
327 fn find_entitlements_plist(shim_path: &std::path::Path) -> Result<PathBuf> {
328 if let Some(dir) = shim_path.parent() {
330 let plist = dir.join("entitlements.plist");
331 if plist.exists() {
332 return Ok(plist);
333 }
334 }
335
336 if let Some(dir) = shim_path.parent() {
339 for ancestor in dir.ancestors().take(5) {
340 let plist = ancestor.join("shim").join("entitlements.plist");
341 if plist.exists() {
342 return Ok(plist);
343 }
344 }
345 }
346
347 let tmp_plist = std::env::temp_dir().join("a3s-box-entitlements.plist");
349 std::fs::write(
350 &tmp_plist,
351 r#"<?xml version="1.0" encoding="UTF-8"?>
352<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
353<plist version="1.0">
354<dict>
355 <key>com.apple.security.hypervisor</key>
356 <true/>
357</dict>
358</plist>
359"#,
360 )
361 .map_err(|e| BoxError::BoxBootError {
362 message: format!("Failed to write temporary entitlements plist: {}", e),
363 hint: None,
364 })?;
365
366 Ok(tmp_plist)
367 }
368
369 pub fn find_shim() -> Result<PathBuf> {
377 #[cfg(target_os = "windows")]
379 let shim_name = "a3s-box-shim.exe";
380 #[cfg(not(target_os = "windows"))]
381 let shim_name = "a3s-box-shim";
382
383 if let Ok(exe_path) = std::env::current_exe() {
385 if let Some(exe_dir) = exe_path.parent() {
386 let shim_path = exe_dir.join(shim_name);
387 if shim_path.exists() {
388 return Ok(shim_path);
389 }
390 }
391 }
392
393 {
395 let shim_path = a3s_box_core::dirs_home().join("bin").join(shim_name);
396 if shim_path.exists() {
397 return Ok(shim_path);
398 }
399 }
400
401 let target_dirs = ["target/debug", "target/release"];
403 for dir in target_dirs {
404 let shim_path = PathBuf::from(dir).join(shim_name);
405 if shim_path.exists() {
406 return Ok(shim_path);
407 }
408 }
409
410 #[cfg(target_os = "windows")]
412 let which_cmd = "where";
413 #[cfg(not(target_os = "windows"))]
414 let which_cmd = "which";
415
416 if let Ok(output) = Command::new(which_cmd).arg(shim_name).output() {
417 if output.status.success() {
418 let path = String::from_utf8_lossy(&output.stdout)
419 .lines()
420 .next()
421 .unwrap_or("")
422 .trim()
423 .to_string();
424 if !path.is_empty() {
425 return Ok(PathBuf::from(path));
426 }
427 }
428 }
429
430 Err(BoxError::BoxBootError {
431 message: "Could not find a3s-box-shim binary".to_string(),
432 hint: Some("Build the shim with: cargo build -p a3s-box-shim".to_string()),
433 })
434 }
435
436 #[cfg(target_os = "windows")]
437 fn windows_shim_path_env(shim_path: &std::path::Path) -> Option<std::ffi::OsString> {
438 use std::collections::HashSet;
439
440 let mut dirs = Vec::<PathBuf>::new();
441 if let Ok(dir) = std::env::var("LIBKRUN_DIR") {
442 dirs.push(PathBuf::from(dir));
443 }
444 if let Some(dir) = option_env!("LIBKRUN_DIR") {
445 dirs.push(PathBuf::from(dir));
446 }
447 if let Some(dir) = shim_path.parent() {
448 dirs.push(dir.to_path_buf());
449 dirs.push(dir.join("lib"));
450 }
451
452 let mut seen = HashSet::new();
453 let mut path_entries = Vec::new();
454 for dir in dirs {
455 if !seen.insert(dir.clone()) {
456 continue;
457 }
458 if dir.join("krun.dll").exists() {
459 path_entries.push(dir);
460 }
461 }
462
463 if path_entries.is_empty() {
464 return None;
465 }
466
467 let mut merged = std::ffi::OsString::new();
468 for entry in path_entries {
469 if !merged.is_empty() {
470 merged.push(";");
471 }
472 merged.push(entry);
473 }
474 if let Some(existing) = std::env::var_os("PATH") {
475 if !merged.is_empty() {
476 merged.push(";");
477 }
478 merged.push(existing);
479 }
480 Some(merged)
481 }
482}
483
484#[async_trait]
485impl VmmProvider for VmController {
486 async fn start(&self, spec: &InstanceSpec) -> Result<Box<dyn VmHandler>> {
487 tracing::debug!(
488 box_id = %spec.box_id,
489 vcpus = spec.vcpus,
490 memory_mib = spec.memory_mib,
491 "Starting VM subprocess"
492 );
493
494 let config_json = serde_json::to_string(spec).map_err(|e| BoxError::BoxBootError {
496 message: format!("Failed to serialize config: {}", e),
497 hint: None,
498 })?;
499
500 tracing::trace!(config = %config_json, "VM configuration");
501
502 if let Some(socket_dir) = spec.exec_socket_path.parent() {
504 std::fs::create_dir_all(socket_dir).map_err(|e| BoxError::BoxBootError {
505 message: format!(
506 "Failed to create socket directory {}: {}",
507 socket_dir.display(),
508 e
509 ),
510 hint: None,
511 })?;
512 }
513
514 #[cfg(unix)]
516 tracing::info!(
517 shim = %self.shim_path.display(),
518 box_id = %spec.box_id,
519 net_socket_fd = spec.network.as_ref().and_then(|net| net.net_socket_fd),
520 net_proxy_fd = spec.network.as_ref().and_then(|net| net.net_proxy_fd),
521 "Spawning shim subprocess"
522 );
523 #[cfg(not(unix))]
524 tracing::info!(
525 shim = %self.shim_path.display(),
526 box_id = %spec.box_id,
527 "Spawning shim subprocess"
528 );
529
530 let mut cmd = Command::new(&self.shim_path);
531 cmd.arg("--config").arg(&config_json).stdin(Stdio::null());
532 self.configure_shim_stdio(&mut cmd, spec);
533
534 if spec.ksm {
537 cmd.env("A3S_BOX_KSM", "1");
538 }
539
540 let snap_env: [(&str, Option<&str>); 3] = [
546 ("KRUN_SNAPSHOT_MEM_FILE", spec.snapshot_mem_file.as_deref()),
547 ("KRUN_SNAPSHOT_SOCK", spec.snapshot_sock.as_deref()),
548 ("KRUN_RESTORE_FROM", spec.restore_from.as_deref()),
549 ];
550 for (var, spec_val) in snap_env {
551 match spec_val {
552 Some(val) if !val.is_empty() => {
553 cmd.env(var, val);
554 }
555 _ => {
556 if let Ok(val) = std::env::var(var) {
557 if !val.is_empty() {
558 cmd.env(var, val);
559 }
560 }
561 }
562 }
563 }
564
565 #[cfg(target_os = "macos")]
567 {
568 let mut dylib_paths = Vec::new();
569 let bundled_lib_dir = self
570 .shim_path
571 .parent()
572 .and_then(|dir| dir.parent())
573 .map(|dir| dir.join("lib"));
574 if let Some(path) = bundled_lib_dir.filter(|path| path.exists()) {
575 dylib_paths.push(path);
576 }
577 let home_lib_dir = a3s_box_core::dirs_home().join("lib");
578 if home_lib_dir.exists() {
579 dylib_paths.push(home_lib_dir);
580 }
581 if let Some(existing) = std::env::var_os("DYLD_LIBRARY_PATH") {
582 dylib_paths.extend(std::env::split_paths(&existing));
583 } else {
584 dylib_paths.push(std::path::PathBuf::from("/opt/homebrew/lib"));
585 }
586 if let Ok(joined) = std::env::join_paths(dylib_paths) {
587 cmd.env("DYLD_LIBRARY_PATH", joined);
588 }
589 }
590
591 #[cfg(target_os = "windows")]
592 if let Some(path) = Self::windows_shim_path_env(&self.shim_path) {
593 cmd.env("PATH", path);
594 }
595
596 #[cfg(unix)]
602 {
603 use std::os::unix::process::CommandExt;
604 let net_socket_fd = spec.network.as_ref().and_then(|net| net.net_socket_fd);
605 let net_proxy_fd = spec.network.as_ref().and_then(|net| net.net_proxy_fd);
606 unsafe {
607 cmd.pre_exec(move || {
608 libc::setsid();
610 for descriptor in [net_socket_fd, net_proxy_fd].into_iter().flatten() {
614 clear_fd_close_on_exec(descriptor)?;
615 }
616 Ok(())
617 });
618 }
619 }
620
621 #[cfg(target_os = "windows")]
622 let mut standard_handle_guard =
623 StandardHandleInheritanceGuard::acquire().map_err(|e| BoxError::BoxBootError {
624 message: format!("Failed to isolate shim standard handles: {e}"),
625 hint: Some("Retry the command from a fresh terminal".to_string()),
626 })?;
627
628 let child = cmd.spawn().map_err(|e| BoxError::BoxBootError {
629 message: format!("Failed to spawn shim: {}", e),
630 hint: Some(format!("Shim path: {}", self.shim_path.display())),
631 })?;
632
633 #[cfg(target_os = "windows")]
634 if let Err(error) = standard_handle_guard.restore() {
635 let mut failed_child = child;
636 let _ = failed_child.kill();
637 let _ = failed_child.wait();
638 return Err(BoxError::BoxBootError {
639 message: format!(
640 "Failed to restore standard-handle inheritance after shim spawn: {error}"
641 ),
642 hint: Some("Retry the command from a fresh terminal".to_string()),
643 });
644 }
645
646 let pid = child.id();
647 tracing::info!(
648 box_id = %spec.box_id,
649 pid = pid,
650 "Shim subprocess spawned"
651 );
652
653 let handler = ShimHandler::from_child(child, spec.box_id.clone());
655
656 Ok(Box::new(handler))
657 }
658}
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663
664 #[cfg(unix)]
665 fn make_fake_shim(dir: &std::path::Path) -> PathBuf {
666 use std::os::unix::fs::PermissionsExt;
667
668 let shim_path = dir.join("fake-a3s-box-shim");
669 std::fs::write(
670 &shim_path,
671 r#"#!/bin/sh
672printf '%s\n' "$@" > "$A3S_TEST_ARGS_FILE"
673printf '%s\n' "$A3S_BOX_KSM" > "$A3S_TEST_KSM_FILE"
674printf '%s\n' "$KRUN_SNAPSHOT_MEM_FILE" > "$A3S_TEST_SNAPSHOT_MEM_FILE"
675printf '%s\n' "$KRUN_SNAPSHOT_SOCK" > "$A3S_TEST_SNAPSHOT_SOCK_FILE"
676printf '%s\n' "$KRUN_RESTORE_FROM" > "$A3S_TEST_RESTORE_FILE"
677printf shim-stdout
678printf shim-stderr >&2
679exec /bin/sleep 30
680"#,
681 )
682 .unwrap();
683 std::fs::set_permissions(&shim_path, std::fs::Permissions::from_mode(0o755)).unwrap();
684 shim_path
685 }
686
687 #[cfg(unix)]
688 fn wait_for_file(path: &std::path::Path) {
689 for _ in 0..250 {
690 if path.metadata().is_ok_and(|metadata| metadata.len() > 0) {
694 return;
695 }
696 std::thread::sleep(std::time::Duration::from_millis(20));
697 }
698 panic!("expected file to appear: {}", path.display());
699 }
700
701 #[test]
702 fn new_reports_missing_shim_with_build_hint() {
703 let missing = tempfile::tempdir()
704 .unwrap()
705 .path()
706 .join("missing-a3s-box-shim");
707
708 let error = match VmController::new(missing.clone()) {
709 Ok(_) => panic!("missing shim should be rejected"),
710 Err(error) => error,
711 };
712 let message = error.to_string();
713
714 assert!(message.contains("Shim binary not found"));
715 assert!(message.contains(&missing.display().to_string()));
716 assert!(message.contains("cargo build -p a3s-box-shim"));
717 }
718
719 #[cfg(unix)]
720 #[test]
721 fn inherited_network_fd_can_be_claimed_for_the_shim_exec() {
722 use std::os::fd::AsRawFd;
723
724 let (left, _right) = std::os::unix::net::UnixStream::pair().unwrap();
725 let descriptor = left.as_raw_fd();
726 assert_ne!(
727 unsafe { libc::fcntl(descriptor, libc::F_SETFD, libc::FD_CLOEXEC) },
728 -1
729 );
730
731 clear_fd_close_on_exec(descriptor).unwrap();
732
733 let flags = unsafe { libc::fcntl(descriptor, libc::F_GETFD) };
734 assert_ne!(flags, -1);
735 assert_eq!(flags & libc::FD_CLOEXEC, 0);
736 }
737
738 #[cfg(target_os = "windows")]
739 #[test]
740 fn clear_handle_inherit_flag_clears_an_inheritable_file_handle() {
741 use std::os::windows::io::AsRawHandle;
742 use windows_sys::Win32::Foundation::{
743 GetHandleInformation, SetHandleInformation, HANDLE, HANDLE_FLAG_INHERIT,
744 };
745
746 static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
747
748 let file = tempfile::tempfile().unwrap();
749 let handle = file.as_raw_handle() as HANDLE;
750 assert_ne!(
751 unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) },
752 0
753 );
754
755 assert!(clear_handle_inherit_flag(handle).unwrap());
756 let mut flags = 0;
757 assert_ne!(unsafe { GetHandleInformation(handle, &mut flags) }, 0);
758 assert_eq!(flags & HANDLE_FLAG_INHERIT, 0);
759 assert!(!clear_handle_inherit_flag(handle).unwrap());
760
761 let mut guard = StandardHandleInheritanceGuard {
762 _spawn_lock: TEST_LOCK.lock().unwrap(),
763 changed_handles: vec![handle],
764 };
765 guard.restore().unwrap();
766 assert_ne!(unsafe { GetHandleInformation(handle, &mut flags) }, 0);
767 assert_ne!(flags & HANDLE_FLAG_INHERIT, 0);
768 }
769
770 #[cfg(target_os = "windows")]
771 #[test]
772 fn configure_shim_stdio_keeps_explicit_windows_log_handles() {
773 let temp = tempfile::tempdir().unwrap();
774 let controller = VmController {
775 shim_path: PathBuf::from("unused"),
776 };
777 let spec = InstanceSpec {
778 box_id: "box-windows-stdio".to_string(),
779 console_output: Some(temp.path().join("logs").join("console.log")),
780 ..Default::default()
781 };
782 let mut cmd = Command::new("cmd.exe");
783 cmd.arg("/C")
784 .arg("echo fresh-stdout & echo fresh-stderr 1>&2");
785
786 controller.configure_shim_stdio(&mut cmd, &spec);
787 let status = cmd.status().unwrap();
788
789 assert!(status.success());
790 assert_eq!(
791 std::fs::read_to_string(temp.path().join("logs").join("shim.stdout.log"))
792 .unwrap()
793 .trim(),
794 "fresh-stdout"
795 );
796 assert_eq!(
797 std::fs::read_to_string(temp.path().join("logs").join("shim.stderr.log"))
798 .unwrap()
799 .trim(),
800 "fresh-stderr"
801 );
802 }
803
804 #[cfg(unix)]
805 #[tokio::test]
806 async fn start_spawns_shim_with_config_env_and_stdio() {
807 let temp = tempfile::tempdir().unwrap();
808 let fake_shim = make_fake_shim(temp.path());
809 let controller = VmController {
810 shim_path: fake_shim,
811 };
812
813 let args_file = temp.path().join("shim.args");
814 let ksm_file = temp.path().join("shim.ksm");
815 let snapshot_mem_file = temp.path().join("shim.snapshot_mem");
816 let snapshot_sock_file = temp.path().join("shim.snapshot_sock");
817 let restore_file = temp.path().join("shim.restore");
818
819 std::env::set_var("A3S_TEST_ARGS_FILE", &args_file);
820 std::env::set_var("A3S_TEST_KSM_FILE", &ksm_file);
821 std::env::set_var("A3S_TEST_SNAPSHOT_MEM_FILE", &snapshot_mem_file);
822 std::env::set_var("A3S_TEST_SNAPSHOT_SOCK_FILE", &snapshot_sock_file);
823 std::env::set_var("A3S_TEST_RESTORE_FILE", &restore_file);
824
825 let socket_dir = temp.path().join("runtime").join("sockets");
826 let spec = InstanceSpec {
827 box_id: "box-start".to_string(),
828 exec_socket_path: socket_dir.join("exec.sock"),
829 console_output: Some(temp.path().join("logs").join("console.log")),
830 ksm: true,
831 snapshot_mem_file: Some("/tmp/a3s-mem".to_string()),
832 snapshot_sock: Some("/tmp/a3s-snapshot.sock".to_string()),
833 restore_from: Some("/tmp/a3s-restore".to_string()),
834 ..Default::default()
835 };
836
837 let mut handler = controller.start(&spec).await.unwrap();
838 wait_for_file(&args_file);
839 wait_for_file(&restore_file);
840 wait_for_file(&temp.path().join("logs").join("shim.stderr.log"));
841
842 assert!(socket_dir.exists());
843 let args = std::fs::read_to_string(&args_file).unwrap();
844 assert!(args.contains("--config"));
845 assert!(args.contains("\"box_id\":\"box-start\""));
846 assert_eq!(std::fs::read_to_string(&ksm_file).unwrap().trim(), "1");
847 assert_eq!(
848 std::fs::read_to_string(&snapshot_mem_file).unwrap().trim(),
849 "/tmp/a3s-mem"
850 );
851 assert_eq!(
852 std::fs::read_to_string(&snapshot_sock_file).unwrap().trim(),
853 "/tmp/a3s-snapshot.sock"
854 );
855 assert_eq!(
856 std::fs::read_to_string(&restore_file).unwrap().trim(),
857 "/tmp/a3s-restore"
858 );
859 assert_eq!(
860 std::fs::read_to_string(temp.path().join("logs").join("shim.stdout.log")).unwrap(),
861 "shim-stdout"
862 );
863 assert_eq!(
864 std::fs::read_to_string(temp.path().join("logs").join("shim.stderr.log")).unwrap(),
865 "shim-stderr"
866 );
867
868 handler.stop(libc::SIGTERM, 1_000).unwrap();
869
870 std::env::remove_var("A3S_TEST_ARGS_FILE");
871 std::env::remove_var("A3S_TEST_KSM_FILE");
872 std::env::remove_var("A3S_TEST_SNAPSHOT_MEM_FILE");
873 std::env::remove_var("A3S_TEST_SNAPSHOT_SOCK_FILE");
874 std::env::remove_var("A3S_TEST_RESTORE_FILE");
875 }
876
877 #[cfg(unix)]
878 #[tokio::test]
879 async fn start_reports_socket_directory_creation_failure() {
880 let temp = tempfile::tempdir().unwrap();
881 let controller = VmController {
882 shim_path: PathBuf::from("/bin/sh"),
883 };
884 let socket_dir = temp.path().join("socket-dir-is-file");
885 std::fs::write(&socket_dir, "not a directory").unwrap();
886 let spec = InstanceSpec {
887 box_id: "box-start-error".to_string(),
888 exec_socket_path: socket_dir.join("exec.sock"),
889 ..Default::default()
890 };
891
892 let err = match controller.start(&spec).await {
893 Ok(_) => panic!("socket directory creation should fail before spawning the shim"),
894 Err(err) => err,
895 };
896
897 assert!(err
898 .to_string()
899 .contains("Failed to create socket directory"));
900 }
901
902 #[cfg(unix)]
903 #[test]
904 fn configure_shim_stdio_writes_per_box_stdout_and_stderr_logs() {
905 let temp = tempfile::tempdir().unwrap();
906 let controller = VmController {
907 shim_path: PathBuf::from("/bin/sh"),
908 };
909 let spec = InstanceSpec {
910 box_id: "box-stdio".to_string(),
911 console_output: Some(temp.path().join("logs").join("console.log")),
912 ..Default::default()
913 };
914
915 let mut cmd = Command::new("sh");
916 cmd.arg("-c")
917 .arg("printf fresh-stdout; printf fresh-stderr >&2");
918
919 std::fs::create_dir_all(temp.path().join("logs")).unwrap();
920 std::fs::write(temp.path().join("logs").join("shim.stdout.log"), "stale").unwrap();
921 std::fs::write(temp.path().join("logs").join("shim.stderr.log"), "stale").unwrap();
922
923 controller.configure_shim_stdio(&mut cmd, &spec);
924 let status = cmd.status().unwrap();
925
926 assert!(status.success());
927 assert_eq!(
928 std::fs::read_to_string(temp.path().join("logs").join("shim.stdout.log")).unwrap(),
929 "fresh-stdout"
930 );
931 assert_eq!(
932 std::fs::read_to_string(temp.path().join("logs").join("shim.stderr.log")).unwrap(),
933 "fresh-stderr"
934 );
935 }
936
937 #[cfg(unix)]
938 #[test]
939 fn configure_shim_stdio_creates_missing_log_directory() {
940 let temp = tempfile::tempdir().unwrap();
941 let controller = VmController {
942 shim_path: PathBuf::from("/bin/sh"),
943 };
944 let spec = InstanceSpec {
945 box_id: "box-stdio-dir".to_string(),
946 console_output: Some(temp.path().join("missing").join("console.log")),
947 ..Default::default()
948 };
949
950 let mut cmd = Command::new("sh");
951 cmd.arg("-c").arg("printf out; printf err >&2");
952
953 controller.configure_shim_stdio(&mut cmd, &spec);
954 let status = cmd.status().unwrap();
955
956 assert!(status.success());
957 assert_eq!(
958 std::fs::read_to_string(temp.path().join("missing").join("shim.stdout.log")).unwrap(),
959 "out"
960 );
961 assert_eq!(
962 std::fs::read_to_string(temp.path().join("missing").join("shim.stderr.log")).unwrap(),
963 "err"
964 );
965 }
966}