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
14pub struct VmController {
20 shim_path: PathBuf,
22}
23
24impl VmController {
25 fn configure_shim_stdio(&self, cmd: &mut Command, spec: &InstanceSpec) {
26 use std::fs::OpenOptions;
27
28 let Some(console_output) = spec.console_output.as_ref() else {
29 cmd.stdout(Stdio::null()).stderr(Stdio::null());
30 return;
31 };
32 let Some(log_dir) = console_output.parent() else {
33 cmd.stdout(Stdio::null()).stderr(Stdio::null());
34 return;
35 };
36 if let Err(error) = std::fs::create_dir_all(log_dir) {
37 tracing::warn!(
38 box_id = %spec.box_id,
39 path = %log_dir.display(),
40 error = %error,
41 "Failed to create shim log directory"
42 );
43 cmd.stdout(Stdio::null()).stderr(Stdio::null());
44 return;
45 }
46
47 let stdout_path = log_dir.join("shim.stdout.log");
48 let stderr_path = log_dir.join("shim.stderr.log");
49
50 let stdout_file = OpenOptions::new()
51 .create(true)
52 .truncate(true)
53 .write(true)
54 .open(&stdout_path);
55 let stderr_file = OpenOptions::new()
56 .create(true)
57 .truncate(true)
58 .write(true)
59 .open(&stderr_path);
60
61 match (stdout_file, stderr_file) {
62 (Ok(stdout_file), Ok(stderr_file)) => {
63 tracing::debug!(
64 box_id = %spec.box_id,
65 stdout = %stdout_path.display(),
66 stderr = %stderr_path.display(),
67 "Redirecting shim stdio to per-box files"
68 );
69 cmd.stdout(Stdio::from(stdout_file))
70 .stderr(Stdio::from(stderr_file));
71 }
72 (stdout_result, stderr_result) => {
73 if let Err(error) = stdout_result {
74 tracing::warn!(
75 box_id = %spec.box_id,
76 path = %stdout_path.display(),
77 error = %error,
78 "Failed to open shim stdout log file"
79 );
80 }
81 if let Err(error) = stderr_result {
82 tracing::warn!(
83 box_id = %spec.box_id,
84 path = %stderr_path.display(),
85 error = %error,
86 "Failed to open shim stderr log file"
87 );
88 }
89 cmd.stdout(Stdio::null()).stderr(Stdio::null());
90 }
91 }
92 }
93
94 pub fn new(shim_path: PathBuf) -> Result<Self> {
103 if !shim_path.exists() {
105 return Err(BoxError::BoxBootError {
106 message: format!("Shim binary not found: {}", shim_path.display()),
107 hint: Some("Build the shim with: cargo build -p a3s-box-shim".to_string()),
108 });
109 }
110
111 #[cfg(target_os = "macos")]
113 Self::ensure_entitlement(&shim_path)?;
114
115 Ok(Self { shim_path })
116 }
117
118 #[cfg(target_os = "macos")]
127 fn ensure_entitlement(shim_path: &std::path::Path) -> Result<()> {
128 use std::fs::File;
129
130 if Self::has_hypervisor_entitlement(shim_path)? {
132 return Ok(());
133 }
134
135 let lock_path = std::env::temp_dir().join("a3s-box-shim-codesign.lock");
137 let lock_file = File::create(&lock_path).map_err(|e| BoxError::BoxBootError {
138 message: format!("Failed to create codesign lock file: {}", e),
139 hint: None,
140 })?;
141
142 let fd = std::os::unix::io::AsRawFd::as_raw_fd(&lock_file);
144 let ret = unsafe { libc::flock(fd, libc::LOCK_EX) };
145 if ret != 0 {
146 return Err(BoxError::BoxBootError {
147 message: format!(
148 "Failed to acquire codesign lock: {}",
149 std::io::Error::last_os_error()
150 ),
151 hint: None,
152 });
153 }
154
155 if Self::has_hypervisor_entitlement(shim_path)? {
157 return Ok(());
159 }
160
161 tracing::info!("Signing shim with Hypervisor.framework entitlement");
162
163 let entitlements_path = Self::find_entitlements_plist(shim_path)?;
164
165 let status = Command::new("codesign")
166 .args(["--entitlements"])
167 .arg(&entitlements_path)
168 .args(["--force", "-s", "-"])
169 .arg(shim_path)
170 .status()
171 .map_err(|e| BoxError::BoxBootError {
172 message: format!("Failed to codesign shim: {}", e),
173 hint: None,
174 })?;
175
176 if !status.success() {
177 return Err(BoxError::BoxBootError {
178 message: "Failed to sign shim with Hypervisor entitlement".to_string(),
179 hint: Some(format!(
180 "Try manually: codesign --entitlements {} --force -s - {}",
181 entitlements_path.display(),
182 shim_path.display()
183 )),
184 });
185 }
186
187 Ok(())
189 }
190
191 #[cfg(target_os = "macos")]
193 fn has_hypervisor_entitlement(shim_path: &std::path::Path) -> Result<bool> {
194 let output = Command::new("codesign")
195 .args(["-d", "--entitlements", "-", "--xml"])
196 .arg(shim_path)
197 .output()
198 .map_err(|e| BoxError::BoxBootError {
199 message: format!("Failed to check entitlements: {}", e),
200 hint: None,
201 })?;
202
203 let stdout = String::from_utf8_lossy(&output.stdout);
204 Ok(stdout.contains("com.apple.security.hypervisor"))
205 }
206
207 #[cfg(target_os = "macos")]
209 fn find_entitlements_plist(shim_path: &std::path::Path) -> Result<PathBuf> {
210 if let Some(dir) = shim_path.parent() {
212 let plist = dir.join("entitlements.plist");
213 if plist.exists() {
214 return Ok(plist);
215 }
216 }
217
218 if let Some(dir) = shim_path.parent() {
221 for ancestor in dir.ancestors().take(5) {
222 let plist = ancestor.join("shim").join("entitlements.plist");
223 if plist.exists() {
224 return Ok(plist);
225 }
226 }
227 }
228
229 let tmp_plist = std::env::temp_dir().join("a3s-box-entitlements.plist");
231 std::fs::write(
232 &tmp_plist,
233 r#"<?xml version="1.0" encoding="UTF-8"?>
234<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
235<plist version="1.0">
236<dict>
237 <key>com.apple.security.hypervisor</key>
238 <true/>
239</dict>
240</plist>
241"#,
242 )
243 .map_err(|e| BoxError::BoxBootError {
244 message: format!("Failed to write temporary entitlements plist: {}", e),
245 hint: None,
246 })?;
247
248 Ok(tmp_plist)
249 }
250
251 pub fn find_shim() -> Result<PathBuf> {
259 #[cfg(target_os = "windows")]
261 let shim_name = "a3s-box-shim.exe";
262 #[cfg(not(target_os = "windows"))]
263 let shim_name = "a3s-box-shim";
264
265 if let Ok(exe_path) = std::env::current_exe() {
267 if let Some(exe_dir) = exe_path.parent() {
268 let shim_path = exe_dir.join(shim_name);
269 if shim_path.exists() {
270 return Ok(shim_path);
271 }
272 }
273 }
274
275 {
277 let shim_path = a3s_box_core::dirs_home().join("bin").join(shim_name);
278 if shim_path.exists() {
279 return Ok(shim_path);
280 }
281 }
282
283 let target_dirs = ["target/debug", "target/release"];
285 for dir in target_dirs {
286 let shim_path = PathBuf::from(dir).join(shim_name);
287 if shim_path.exists() {
288 return Ok(shim_path);
289 }
290 }
291
292 #[cfg(target_os = "windows")]
294 let which_cmd = "where";
295 #[cfg(not(target_os = "windows"))]
296 let which_cmd = "which";
297
298 if let Ok(output) = Command::new(which_cmd).arg(shim_name).output() {
299 if output.status.success() {
300 let path = String::from_utf8_lossy(&output.stdout)
301 .lines()
302 .next()
303 .unwrap_or("")
304 .trim()
305 .to_string();
306 if !path.is_empty() {
307 return Ok(PathBuf::from(path));
308 }
309 }
310 }
311
312 Err(BoxError::BoxBootError {
313 message: "Could not find a3s-box-shim binary".to_string(),
314 hint: Some("Build the shim with: cargo build -p a3s-box-shim".to_string()),
315 })
316 }
317
318 #[cfg(target_os = "windows")]
319 fn windows_shim_path_env(shim_path: &std::path::Path) -> Option<std::ffi::OsString> {
320 use std::collections::HashSet;
321
322 let mut dirs = Vec::<PathBuf>::new();
323 if let Ok(dir) = std::env::var("LIBKRUN_DIR") {
324 dirs.push(PathBuf::from(dir));
325 }
326 if let Some(dir) = option_env!("LIBKRUN_DIR") {
327 dirs.push(PathBuf::from(dir));
328 }
329 if let Some(dir) = shim_path.parent() {
330 dirs.push(dir.to_path_buf());
331 dirs.push(dir.join("lib"));
332 }
333
334 let mut seen = HashSet::new();
335 let mut path_entries = Vec::new();
336 for dir in dirs {
337 if !seen.insert(dir.clone()) {
338 continue;
339 }
340 if dir.join("krun.dll").exists() {
341 path_entries.push(dir);
342 }
343 }
344
345 if path_entries.is_empty() {
346 return None;
347 }
348
349 let mut merged = std::ffi::OsString::new();
350 for entry in path_entries {
351 if !merged.is_empty() {
352 merged.push(";");
353 }
354 merged.push(entry);
355 }
356 if let Some(existing) = std::env::var_os("PATH") {
357 if !merged.is_empty() {
358 merged.push(";");
359 }
360 merged.push(existing);
361 }
362 Some(merged)
363 }
364}
365
366#[async_trait]
367impl VmmProvider for VmController {
368 async fn start(&self, spec: &InstanceSpec) -> Result<Box<dyn VmHandler>> {
369 tracing::debug!(
370 box_id = %spec.box_id,
371 vcpus = spec.vcpus,
372 memory_mib = spec.memory_mib,
373 "Starting VM subprocess"
374 );
375
376 let config_json = serde_json::to_string(spec).map_err(|e| BoxError::BoxBootError {
378 message: format!("Failed to serialize config: {}", e),
379 hint: None,
380 })?;
381
382 tracing::trace!(config = %config_json, "VM configuration");
383
384 if let Some(socket_dir) = spec.exec_socket_path.parent() {
386 std::fs::create_dir_all(socket_dir).map_err(|e| BoxError::BoxBootError {
387 message: format!(
388 "Failed to create socket directory {}: {}",
389 socket_dir.display(),
390 e
391 ),
392 hint: None,
393 })?;
394 }
395
396 #[cfg(target_os = "macos")]
398 tracing::info!(
399 shim = %self.shim_path.display(),
400 box_id = %spec.box_id,
401 net_socket_fd = spec.network.as_ref().and_then(|net| net.net_socket_fd),
402 net_proxy_fd = spec.network.as_ref().and_then(|net| net.net_proxy_fd),
403 "Spawning shim subprocess"
404 );
405 #[cfg(not(target_os = "macos"))]
406 tracing::info!(
407 shim = %self.shim_path.display(),
408 box_id = %spec.box_id,
409 "Spawning shim subprocess"
410 );
411
412 let mut cmd = Command::new(&self.shim_path);
413 cmd.arg("--config").arg(&config_json).stdin(Stdio::null());
414 self.configure_shim_stdio(&mut cmd, spec);
415
416 if spec.ksm {
419 cmd.env("A3S_BOX_KSM", "1");
420 }
421
422 let snap_env: [(&str, Option<&str>); 3] = [
428 ("KRUN_SNAPSHOT_MEM_FILE", spec.snapshot_mem_file.as_deref()),
429 ("KRUN_SNAPSHOT_SOCK", spec.snapshot_sock.as_deref()),
430 ("KRUN_RESTORE_FROM", spec.restore_from.as_deref()),
431 ];
432 for (var, spec_val) in snap_env {
433 match spec_val {
434 Some(val) if !val.is_empty() => {
435 cmd.env(var, val);
436 }
437 _ => {
438 if let Ok(val) = std::env::var(var) {
439 if !val.is_empty() {
440 cmd.env(var, val);
441 }
442 }
443 }
444 }
445 }
446
447 #[cfg(target_os = "macos")]
449 {
450 let mut dylib_paths = Vec::new();
451 let bundled_lib_dir = self
452 .shim_path
453 .parent()
454 .and_then(|dir| dir.parent())
455 .map(|dir| dir.join("lib"));
456 if let Some(path) = bundled_lib_dir.filter(|path| path.exists()) {
457 dylib_paths.push(path);
458 }
459 let home_lib_dir = a3s_box_core::dirs_home().join("lib");
460 if home_lib_dir.exists() {
461 dylib_paths.push(home_lib_dir);
462 }
463 if let Some(existing) = std::env::var_os("DYLD_LIBRARY_PATH") {
464 dylib_paths.extend(std::env::split_paths(&existing));
465 } else {
466 dylib_paths.push(std::path::PathBuf::from("/opt/homebrew/lib"));
467 }
468 if let Ok(joined) = std::env::join_paths(dylib_paths) {
469 cmd.env("DYLD_LIBRARY_PATH", joined);
470 }
471 }
472
473 #[cfg(target_os = "windows")]
474 if let Some(path) = Self::windows_shim_path_env(&self.shim_path) {
475 cmd.env("PATH", path);
476 }
477
478 #[cfg(unix)]
484 {
485 use std::os::unix::process::CommandExt;
486 unsafe {
487 cmd.pre_exec(|| {
488 libc::setsid();
490 Ok(())
491 });
492 }
493 }
494
495 let child = cmd.spawn().map_err(|e| BoxError::BoxBootError {
496 message: format!("Failed to spawn shim: {}", e),
497 hint: Some(format!("Shim path: {}", self.shim_path.display())),
498 })?;
499
500 let pid = child.id();
501 tracing::info!(
502 box_id = %spec.box_id,
503 pid = pid,
504 "Shim subprocess spawned"
505 );
506
507 let handler = ShimHandler::from_child(child, spec.box_id.clone());
509
510 Ok(Box::new(handler))
511 }
512}