1pub use a3s_box_core::vmm::{VmHandler, VmMetrics, DEFAULT_SHUTDOWN_TIMEOUT_MS};
4
5use a3s_box_core::error::Result;
6use std::process::Child;
7use std::sync::Mutex;
8use sysinfo::{Pid, System};
9
10#[cfg(windows)]
11fn wait_then_terminate_attached_process(pid: u32, box_id: &str, timeout_ms: u64) -> Result<()> {
12 use a3s_box_core::error::BoxError;
13 use windows_sys::Win32::Foundation::{CloseHandle, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT};
14 use windows_sys::Win32::System::Threading::{
15 OpenProcess, TerminateProcess, WaitForSingleObject, PROCESS_SYNCHRONIZE, PROCESS_TERMINATE,
16 };
17
18 const TERMINATION_WAIT_MS: u32 = 5_000;
19 const MAX_FINITE_WAIT_MS: u64 = (u32::MAX - 1) as u64;
20
21 let handle = unsafe { OpenProcess(PROCESS_TERMINATE | PROCESS_SYNCHRONIZE, 0, pid) };
22 if handle == 0 {
23 let error = std::io::Error::last_os_error();
24 if !crate::process::is_process_alive(pid) {
25 return Ok(());
26 }
27 return Err(BoxError::ExecError(format!(
28 "Failed to open attached VM process {pid} for box {box_id}: {error}"
29 )));
30 }
31
32 let result = (|| {
33 let graceful_wait_ms = timeout_ms.min(MAX_FINITE_WAIT_MS) as u32;
34 match unsafe { WaitForSingleObject(handle, graceful_wait_ms) } {
35 WAIT_OBJECT_0 => return Ok(()),
36 WAIT_TIMEOUT => tracing::warn!(
37 pid,
38 box_id,
39 timeout_ms,
40 "Attached VM process did not exit after the guest stop request; forcing termination"
41 ),
42 WAIT_FAILED => tracing::warn!(
43 pid,
44 box_id,
45 error = %std::io::Error::last_os_error(),
46 "Failed while waiting for attached VM process; forcing termination"
47 ),
48 status => tracing::warn!(
49 pid,
50 box_id,
51 status,
52 "Unexpected attached VM wait status; forcing termination"
53 ),
54 }
55
56 if unsafe { TerminateProcess(handle, 1) } == 0 {
57 let error = std::io::Error::last_os_error();
58 if unsafe { WaitForSingleObject(handle, 0) } == WAIT_OBJECT_0 {
59 return Ok(());
60 }
61 return Err(BoxError::ExecError(format!(
62 "Failed to terminate attached VM process {pid} for box {box_id}: {error}"
63 )));
64 }
65
66 match unsafe { WaitForSingleObject(handle, TERMINATION_WAIT_MS) } {
67 WAIT_OBJECT_0 => Ok(()),
68 WAIT_TIMEOUT => Err(BoxError::ExecError(format!(
69 "Attached VM process {pid} for box {box_id} did not exit after termination"
70 ))),
71 WAIT_FAILED => Err(BoxError::ExecError(format!(
72 "Failed to wait for attached VM process {pid} for box {box_id}: {}",
73 std::io::Error::last_os_error()
74 ))),
75 status => Err(BoxError::ExecError(format!(
76 "Unexpected wait status {status} for attached VM process {pid} of box {box_id}"
77 ))),
78 }
79 })();
80 unsafe {
81 CloseHandle(handle);
82 }
83 result
84}
85
86pub struct ShimHandler {
90 pid: u32,
91 pid_start_time: Option<u64>,
93 box_id: String,
94 process: Option<Child>,
98 metrics_sys: Mutex<System>,
101 exit_code: Option<i32>,
103}
104
105impl ShimHandler {
106 pub fn from_child(process: Child, box_id: String) -> Self {
111 let pid = process.id();
112 Self {
113 pid,
114 pid_start_time: crate::process::pid_start_time(pid),
115 box_id,
116 process: Some(process),
117 metrics_sys: Mutex::new(System::new()),
118 exit_code: None,
119 }
120 }
121
122 pub fn from_pid(pid: u32, box_id: String) -> Self {
127 Self {
128 pid,
129 pid_start_time: crate::process::pid_start_time(pid),
130 box_id,
131 process: None,
132 metrics_sys: Mutex::new(System::new()),
133 exit_code: None,
134 }
135 }
136
137 pub fn box_id(&self) -> &str {
139 &self.box_id
140 }
141}
142
143impl VmHandler for ShimHandler {
144 fn pid(&self) -> u32 {
145 self.pid
146 }
147
148 #[cfg(unix)]
149 fn stop(&mut self, signal: i32, timeout_ms: u64) -> Result<()> {
150 if self.exit_code.is_some() {
156 self.process.take();
157 return Ok(());
158 }
159 if !self.is_running() {
160 return Ok(());
161 }
162
163 if let Some(mut process) = self.process.take() {
164 let pid = process.id();
166 tracing::debug!(pid, box_id = %self.box_id, signal, "Sending stop signal to VM process");
167 unsafe {
168 libc::kill(pid as i32, signal);
169 }
170
171 let start = std::time::Instant::now();
173 loop {
174 match process.try_wait() {
175 Ok(Some(status)) => {
176 tracing::debug!(pid, ?status, "VM process exited gracefully");
177 self.exit_code = status.code();
178 return Ok(());
179 }
180 Ok(None) => {
181 if start.elapsed().as_millis() > timeout_ms as u128 {
183 tracing::warn!(
184 pid,
185 timeout_ms,
186 "VM process did not exit gracefully, sending SIGKILL"
187 );
188 let _ = process.kill();
189 if let Ok(status) = process.wait() {
190 self.exit_code = status.code();
191 }
192 return Ok(());
193 }
194 std::thread::sleep(std::time::Duration::from_millis(50));
196 }
197 Err(e) => {
198 tracing::warn!(pid, error = %e, "Error checking process status, forcing kill");
199 let _ = process.kill();
200 let _ = process.wait();
201 return Ok(());
202 }
203 }
204 }
205 } else {
206 tracing::debug!(pid = self.pid, box_id = %self.box_id, signal, "Sending stop signal to attached VM process");
208 unsafe {
209 libc::kill(self.pid as i32, signal);
210 }
211
212 let start = std::time::Instant::now();
214 loop {
215 let mut status: i32 = 0;
216 let result = unsafe { libc::waitpid(self.pid as i32, &mut status, libc::WNOHANG) };
217
218 if result > 0 {
219 tracing::debug!(pid = self.pid, "VM process exited gracefully");
220 return Ok(());
221 }
222 if result < 0 {
223 if !self.is_running() {
225 return Ok(()); }
227 }
228
229 if start.elapsed().as_millis() > timeout_ms as u128 {
230 if !self.is_running() {
231 return Ok(());
232 }
233 tracing::warn!(
234 pid = self.pid,
235 timeout_ms,
236 "VM process did not exit gracefully, sending SIGKILL"
237 );
238 unsafe {
239 libc::kill(self.pid as i32, libc::SIGKILL);
240 }
241 return Ok(());
242 }
243
244 std::thread::sleep(std::time::Duration::from_millis(50));
245 }
246 }
247 }
248
249 #[cfg(windows)]
250 fn stop(&mut self, _signal: i32, timeout_ms: u64) -> Result<()> {
251 if let Some(mut process) = self.process.take() {
253 tracing::debug!(pid = self.pid, box_id = %self.box_id, "Terminating VM process");
254
255 let start = std::time::Instant::now();
257 loop {
258 match process.try_wait() {
259 Ok(Some(status)) => {
260 tracing::debug!(pid = self.pid, ?status, "VM process exited");
261 self.exit_code = status.code();
262 return Ok(());
263 }
264 Ok(None) => {
265 if start.elapsed().as_millis() > timeout_ms as u128 {
266 tracing::warn!(pid = self.pid, "VM process did not exit, forcing kill");
267 let _ = process.kill();
268 if let Ok(status) = process.wait() {
269 self.exit_code = status.code();
270 }
271 return Ok(());
272 }
273 std::thread::sleep(std::time::Duration::from_millis(50));
274 }
275 Err(e) => {
276 tracing::warn!(pid = self.pid, error = %e, "Error checking process, forcing kill");
277 let _ = process.kill();
278 let _ = process.wait();
279 return Ok(());
280 }
281 }
282 }
283 } else {
284 if !self.is_running() {
285 return Ok(());
286 }
287
288 tracing::debug!(pid = self.pid, box_id = %self.box_id, timeout_ms, "Waiting for attached VM process to stop");
294 wait_then_terminate_attached_process(self.pid, &self.box_id, timeout_ms)
295 }
296 }
297
298 fn metrics(&self) -> VmMetrics {
299 if !self.is_running() {
300 return VmMetrics::default();
301 }
302
303 let pid = Pid::from_u32(self.pid);
304
305 let mut sys = match self.metrics_sys.lock() {
307 Ok(guard) => guard,
308 Err(e) => {
309 tracing::warn!(error = %e, "metrics_sys lock poisoned");
310 return VmMetrics::default();
311 }
312 };
313
314 sys.refresh_process(pid);
316
317 if let Some(proc_info) = sys.process(pid) {
319 return VmMetrics {
320 cpu_percent: Some(proc_info.cpu_usage()),
321 memory_bytes: Some(proc_info.memory()),
322 };
323 }
324
325 VmMetrics::default()
327 }
328
329 #[cfg(unix)]
330 fn is_running(&self) -> bool {
331 crate::process::is_process_alive_with_identity(self.pid, self.pid_start_time)
332 }
333
334 #[cfg(windows)]
335 fn is_running(&self) -> bool {
336 crate::process::is_process_alive_with_identity(self.pid, self.pid_start_time)
337 }
338
339 fn exit_code(&self) -> Option<i32> {
340 self.exit_code
341 }
342
343 fn try_wait_exit(&mut self) -> Result<Option<i32>> {
344 if self.exit_code.is_some() {
345 return Ok(self.exit_code);
346 }
347
348 let Some(process) = self.process.as_mut() else {
349 return Ok(None);
350 };
351
352 match process.try_wait() {
353 Ok(Some(status)) => {
354 self.exit_code = status.code();
355 Ok(self.exit_code)
356 }
357 Ok(None) => Ok(None),
358 Err(e) => Err(a3s_box_core::error::BoxError::ExecError(format!(
359 "Failed to poll VM process {}: {}",
360 self.pid, e
361 ))),
362 }
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn test_vm_metrics_default() {
372 let m = VmMetrics::default();
373 assert!(m.cpu_percent.is_none());
374 assert!(m.memory_bytes.is_none());
375 }
376
377 #[test]
378 fn test_vm_metrics_clone() {
379 let m = VmMetrics {
380 cpu_percent: Some(50.0),
381 memory_bytes: Some(1024 * 1024),
382 };
383 let cloned = m.clone();
384 assert_eq!(cloned.cpu_percent, Some(50.0));
385 assert_eq!(cloned.memory_bytes, Some(1024 * 1024));
386 }
387
388 #[test]
389 fn test_shim_handler_from_pid() {
390 let handler = ShimHandler::from_pid(12345, "box-abc".to_string());
391 assert_eq!(handler.pid(), 12345);
392 assert_eq!(handler.box_id(), "box-abc");
393 assert_eq!(handler.exit_code(), None);
394 }
395
396 #[cfg(target_os = "linux")]
397 #[test]
398 fn attached_handler_rejects_a_reused_pid_identity() {
399 let mut child = std::process::Command::new("sleep")
400 .arg("30")
401 .spawn()
402 .unwrap();
403 let mut handler = ShimHandler::from_pid(child.id(), "box-stale-pid".to_string());
404 handler.pid_start_time = Some(u64::MAX);
405
406 assert!(!handler.is_running());
407 let metrics = handler.metrics();
408 assert!(metrics.cpu_percent.is_none());
409 assert!(metrics.memory_bytes.is_none());
410 handler.stop(libc::SIGTERM, 0).unwrap();
411 assert!(child.try_wait().unwrap().is_none());
412
413 let _ = child.kill();
414 let _ = child.wait();
415 }
416
417 #[test]
418 fn test_shim_handler_try_wait_exit_captures_child_exit_code() {
419 let child = std::process::Command::new("sh")
420 .arg("-c")
421 .arg("exit 7")
422 .spawn()
423 .unwrap();
424 let mut handler = ShimHandler::from_child(child, "box-child-exit".to_string());
425
426 let exit_code = loop {
427 if let Some(code) = handler.try_wait_exit().unwrap() {
428 break code;
429 }
430 std::thread::sleep(std::time::Duration::from_millis(10));
431 };
432
433 assert_eq!(exit_code, 7);
434 assert_eq!(handler.exit_code(), Some(7));
435 assert_eq!(handler.try_wait_exit().unwrap(), Some(7));
436 }
437
438 #[cfg(unix)]
439 #[test]
440 fn test_shim_handler_stop_terminates_owned_child() {
441 let child = std::process::Command::new("sh")
442 .arg("-c")
443 .arg("trap 'exit 0' TERM; while :; do sleep 0.05; done")
444 .spawn()
445 .unwrap();
446 let mut handler = ShimHandler::from_child(child, "box-child-stop".to_string());
447
448 assert!(handler.is_running());
449 handler.stop(libc::SIGTERM, 2_000).unwrap();
450
451 assert!(!handler.is_running());
452 let exit_code = handler.exit_code();
453 assert!(matches!(exit_code, None | Some(0)));
454 assert_eq!(handler.try_wait_exit().unwrap(), exit_code);
455 }
456
457 #[cfg(unix)]
458 #[test]
459 fn test_shim_handler_stop_attached_missing_pid_is_ok() {
460 let mut handler = ShimHandler::from_pid(999_999_999, "missing".to_string());
461
462 handler.stop(libc::SIGTERM, 10).unwrap();
463
464 assert!(!handler.is_running());
465 assert_eq!(handler.exit_code(), None);
466 }
467
468 #[cfg(windows)]
469 #[test]
470 fn test_shim_handler_stop_terminates_attached_child() {
471 let mut child = std::process::Command::new("powershell.exe")
472 .args(["-NoProfile", "-Command", "Start-Sleep -Seconds 30"])
473 .spawn()
474 .unwrap();
475 let mut handler = ShimHandler::from_pid(child.id(), "box-attached-stop".to_string());
476
477 assert!(handler.is_running());
478 let stop_result = handler.stop(15, 0);
479 let exited = child.try_wait().unwrap().is_some();
480 if !exited {
481 let _ = child.kill();
482 let _ = child.wait();
483 }
484
485 stop_result.unwrap();
486 assert!(exited, "attached Windows process survived handler.stop()");
487 assert!(!handler.is_running());
488 }
489
490 #[cfg(windows)]
491 #[test]
492 fn test_shim_handler_stop_allows_attached_child_to_exit_gracefully() {
493 let mut child = std::process::Command::new("powershell.exe")
494 .args([
495 "-NoProfile",
496 "-Command",
497 "Start-Sleep -Milliseconds 150; exit 0",
498 ])
499 .spawn()
500 .unwrap();
501 let mut handler = ShimHandler::from_pid(child.id(), "box-attached-graceful".to_string());
502
503 handler.stop(15, 2_000).unwrap();
504 let status = child.wait().unwrap();
505
506 assert!(status.success(), "graceful child was force-terminated");
507 assert!(!handler.is_running());
508 }
509
510 #[test]
511 fn test_shim_handler_is_running_nonexistent_pid() {
512 let handler = ShimHandler::from_pid(999_999_999, "test".to_string());
514 assert!(!handler.is_running());
515 }
516
517 #[test]
518 fn test_shim_handler_metrics_nonexistent_pid() {
519 let handler = ShimHandler::from_pid(999_999_999, "test".to_string());
520 let m = handler.metrics();
521 assert!(m.cpu_percent.is_none() || m.cpu_percent == Some(0.0));
523 }
524
525 #[test]
526 fn test_shim_handler_is_running_current_process() {
527 let pid = std::process::id();
529 let handler = ShimHandler::from_pid(pid, "self".to_string());
530 assert!(handler.is_running());
531 }
532
533 #[test]
534 fn test_default_shutdown_timeout() {
535 assert_eq!(DEFAULT_SHUTDOWN_TIMEOUT_MS, 10_000);
536 }
537
538 #[test]
539 fn test_vm_metrics_debug() {
540 let m = VmMetrics {
541 cpu_percent: Some(25.5),
542 memory_bytes: Some(512),
543 };
544 let debug = format!("{:?}", m);
545 assert!(debug.contains("25.5"));
546 assert!(debug.contains("512"));
547 }
548}