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