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
10pub struct ShimHandler {
14 pid: u32,
15 pid_start_time: Option<u64>,
17 box_id: String,
18 process: Option<Child>,
22 metrics_sys: Mutex<System>,
25 exit_code: Option<i32>,
27}
28
29impl ShimHandler {
30 pub fn from_child(process: Child, box_id: String) -> Self {
35 let pid = process.id();
36 Self {
37 pid,
38 pid_start_time: crate::process::pid_start_time(pid),
39 box_id,
40 process: Some(process),
41 metrics_sys: Mutex::new(System::new()),
42 exit_code: None,
43 }
44 }
45
46 pub fn from_pid(pid: u32, box_id: String) -> Self {
51 Self {
52 pid,
53 pid_start_time: crate::process::pid_start_time(pid),
54 box_id,
55 process: None,
56 metrics_sys: Mutex::new(System::new()),
57 exit_code: None,
58 }
59 }
60
61 pub fn box_id(&self) -> &str {
63 &self.box_id
64 }
65}
66
67impl VmHandler for ShimHandler {
68 fn pid(&self) -> u32 {
69 self.pid
70 }
71
72 #[cfg(unix)]
73 fn stop(&mut self, signal: i32, timeout_ms: u64) -> Result<()> {
74 if self.exit_code.is_some() {
80 self.process.take();
81 return Ok(());
82 }
83 if !self.is_running() {
84 return Ok(());
85 }
86
87 if let Some(mut process) = self.process.take() {
88 let pid = process.id();
90 tracing::debug!(pid, box_id = %self.box_id, signal, "Sending stop signal to VM process");
91 unsafe {
92 libc::kill(pid as i32, signal);
93 }
94
95 let start = std::time::Instant::now();
97 loop {
98 match process.try_wait() {
99 Ok(Some(status)) => {
100 tracing::debug!(pid, ?status, "VM process exited gracefully");
101 self.exit_code = status.code();
102 return Ok(());
103 }
104 Ok(None) => {
105 if start.elapsed().as_millis() > timeout_ms as u128 {
107 tracing::warn!(
108 pid,
109 timeout_ms,
110 "VM process did not exit gracefully, sending SIGKILL"
111 );
112 let _ = process.kill();
113 if let Ok(status) = process.wait() {
114 self.exit_code = status.code();
115 }
116 return Ok(());
117 }
118 std::thread::sleep(std::time::Duration::from_millis(50));
120 }
121 Err(e) => {
122 tracing::warn!(pid, error = %e, "Error checking process status, forcing kill");
123 let _ = process.kill();
124 let _ = process.wait();
125 return Ok(());
126 }
127 }
128 }
129 } else {
130 tracing::debug!(pid = self.pid, box_id = %self.box_id, signal, "Sending stop signal to attached VM process");
132 unsafe {
133 libc::kill(self.pid as i32, signal);
134 }
135
136 let start = std::time::Instant::now();
138 loop {
139 let mut status: i32 = 0;
140 let result = unsafe { libc::waitpid(self.pid as i32, &mut status, libc::WNOHANG) };
141
142 if result > 0 {
143 tracing::debug!(pid = self.pid, "VM process exited gracefully");
144 return Ok(());
145 }
146 if result < 0 {
147 if !self.is_running() {
149 return Ok(()); }
151 }
152
153 if start.elapsed().as_millis() > timeout_ms as u128 {
154 if !self.is_running() {
155 return Ok(());
156 }
157 tracing::warn!(
158 pid = self.pid,
159 timeout_ms,
160 "VM process did not exit gracefully, sending SIGKILL"
161 );
162 unsafe {
163 libc::kill(self.pid as i32, libc::SIGKILL);
164 }
165 return Ok(());
166 }
167
168 std::thread::sleep(std::time::Duration::from_millis(50));
169 }
170 }
171 }
172
173 #[cfg(windows)]
174 fn stop(&mut self, _signal: i32, timeout_ms: u64) -> Result<()> {
175 if let Some(mut process) = self.process.take() {
177 tracing::debug!(pid = self.pid, box_id = %self.box_id, "Terminating VM process");
178
179 let start = std::time::Instant::now();
181 loop {
182 match process.try_wait() {
183 Ok(Some(status)) => {
184 tracing::debug!(pid = self.pid, ?status, "VM process exited");
185 self.exit_code = status.code();
186 return Ok(());
187 }
188 Ok(None) => {
189 if start.elapsed().as_millis() > timeout_ms as u128 {
190 tracing::warn!(pid = self.pid, "VM process did not exit, forcing kill");
191 let _ = process.kill();
192 if let Ok(status) = process.wait() {
193 self.exit_code = status.code();
194 }
195 return Ok(());
196 }
197 std::thread::sleep(std::time::Duration::from_millis(50));
198 }
199 Err(e) => {
200 tracing::warn!(pid = self.pid, error = %e, "Error checking process, forcing kill");
201 let _ = process.kill();
202 let _ = process.wait();
203 return Ok(());
204 }
205 }
206 }
207 } else {
208 tracing::debug!(pid = self.pid, "Checking attached VM process");
210 let start = std::time::Instant::now();
211 while start.elapsed().as_millis() <= timeout_ms as u128 {
212 if !self.is_running() {
213 return Ok(());
214 }
215 std::thread::sleep(std::time::Duration::from_millis(50));
216 }
217 tracing::warn!(
218 pid = self.pid,
219 "Attached VM process still running after timeout"
220 );
221 Ok(())
222 }
223 }
224
225 fn metrics(&self) -> VmMetrics {
226 if !self.is_running() {
227 return VmMetrics::default();
228 }
229
230 let pid = Pid::from_u32(self.pid);
231
232 let mut sys = match self.metrics_sys.lock() {
234 Ok(guard) => guard,
235 Err(e) => {
236 tracing::warn!(error = %e, "metrics_sys lock poisoned");
237 return VmMetrics::default();
238 }
239 };
240
241 sys.refresh_process(pid);
243
244 if let Some(proc_info) = sys.process(pid) {
246 return VmMetrics {
247 cpu_percent: Some(proc_info.cpu_usage()),
248 memory_bytes: Some(proc_info.memory()),
249 };
250 }
251
252 VmMetrics::default()
254 }
255
256 #[cfg(unix)]
257 fn is_running(&self) -> bool {
258 crate::process::is_process_alive_with_identity(self.pid, self.pid_start_time)
259 }
260
261 #[cfg(windows)]
262 fn is_running(&self) -> bool {
263 crate::process::is_process_alive_with_identity(self.pid, self.pid_start_time)
264 }
265
266 fn exit_code(&self) -> Option<i32> {
267 self.exit_code
268 }
269
270 fn try_wait_exit(&mut self) -> Result<Option<i32>> {
271 if self.exit_code.is_some() {
272 return Ok(self.exit_code);
273 }
274
275 let Some(process) = self.process.as_mut() else {
276 return Ok(None);
277 };
278
279 match process.try_wait() {
280 Ok(Some(status)) => {
281 self.exit_code = status.code();
282 Ok(self.exit_code)
283 }
284 Ok(None) => Ok(None),
285 Err(e) => Err(a3s_box_core::error::BoxError::ExecError(format!(
286 "Failed to poll VM process {}: {}",
287 self.pid, e
288 ))),
289 }
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn test_vm_metrics_default() {
299 let m = VmMetrics::default();
300 assert!(m.cpu_percent.is_none());
301 assert!(m.memory_bytes.is_none());
302 }
303
304 #[test]
305 fn test_vm_metrics_clone() {
306 let m = VmMetrics {
307 cpu_percent: Some(50.0),
308 memory_bytes: Some(1024 * 1024),
309 };
310 let cloned = m.clone();
311 assert_eq!(cloned.cpu_percent, Some(50.0));
312 assert_eq!(cloned.memory_bytes, Some(1024 * 1024));
313 }
314
315 #[test]
316 fn test_shim_handler_from_pid() {
317 let handler = ShimHandler::from_pid(12345, "box-abc".to_string());
318 assert_eq!(handler.pid(), 12345);
319 assert_eq!(handler.box_id(), "box-abc");
320 assert_eq!(handler.exit_code(), None);
321 }
322
323 #[cfg(target_os = "linux")]
324 #[test]
325 fn attached_handler_rejects_a_reused_pid_identity() {
326 let mut child = std::process::Command::new("sleep")
327 .arg("30")
328 .spawn()
329 .unwrap();
330 let mut handler = ShimHandler::from_pid(child.id(), "box-stale-pid".to_string());
331 handler.pid_start_time = Some(u64::MAX);
332
333 assert!(!handler.is_running());
334 let metrics = handler.metrics();
335 assert!(metrics.cpu_percent.is_none());
336 assert!(metrics.memory_bytes.is_none());
337 handler.stop(libc::SIGTERM, 0).unwrap();
338 assert!(child.try_wait().unwrap().is_none());
339
340 let _ = child.kill();
341 let _ = child.wait();
342 }
343
344 #[test]
345 fn test_shim_handler_try_wait_exit_captures_child_exit_code() {
346 let child = std::process::Command::new("sh")
347 .arg("-c")
348 .arg("exit 7")
349 .spawn()
350 .unwrap();
351 let mut handler = ShimHandler::from_child(child, "box-child-exit".to_string());
352
353 let exit_code = loop {
354 if let Some(code) = handler.try_wait_exit().unwrap() {
355 break code;
356 }
357 std::thread::sleep(std::time::Duration::from_millis(10));
358 };
359
360 assert_eq!(exit_code, 7);
361 assert_eq!(handler.exit_code(), Some(7));
362 assert_eq!(handler.try_wait_exit().unwrap(), Some(7));
363 }
364
365 #[cfg(unix)]
366 #[test]
367 fn test_shim_handler_stop_terminates_owned_child() {
368 let child = std::process::Command::new("sh")
369 .arg("-c")
370 .arg("trap 'exit 0' TERM; while :; do sleep 0.05; done")
371 .spawn()
372 .unwrap();
373 let mut handler = ShimHandler::from_child(child, "box-child-stop".to_string());
374
375 assert!(handler.is_running());
376 handler.stop(libc::SIGTERM, 2_000).unwrap();
377
378 assert!(!handler.is_running());
379 let exit_code = handler.exit_code();
380 assert!(matches!(exit_code, None | Some(0)));
381 assert_eq!(handler.try_wait_exit().unwrap(), exit_code);
382 }
383
384 #[cfg(unix)]
385 #[test]
386 fn test_shim_handler_stop_attached_missing_pid_is_ok() {
387 let mut handler = ShimHandler::from_pid(999_999_999, "missing".to_string());
388
389 handler.stop(libc::SIGTERM, 10).unwrap();
390
391 assert!(!handler.is_running());
392 assert_eq!(handler.exit_code(), None);
393 }
394
395 #[test]
396 fn test_shim_handler_is_running_nonexistent_pid() {
397 let handler = ShimHandler::from_pid(999_999_999, "test".to_string());
399 assert!(!handler.is_running());
400 }
401
402 #[test]
403 fn test_shim_handler_metrics_nonexistent_pid() {
404 let handler = ShimHandler::from_pid(999_999_999, "test".to_string());
405 let m = handler.metrics();
406 assert!(m.cpu_percent.is_none() || m.cpu_percent == Some(0.0));
408 }
409
410 #[test]
411 fn test_shim_handler_is_running_current_process() {
412 let pid = std::process::id();
414 let handler = ShimHandler::from_pid(pid, "self".to_string());
415 assert!(handler.is_running());
416 }
417
418 #[test]
419 fn test_default_shutdown_timeout() {
420 assert_eq!(DEFAULT_SHUTDOWN_TIMEOUT_MS, 10_000);
421 }
422
423 #[test]
424 fn test_vm_metrics_debug() {
425 let m = VmMetrics {
426 cpu_percent: Some(25.5),
427 memory_bytes: Some(512),
428 };
429 let debug = format!("{:?}", m);
430 assert!(debug.contains("25.5"));
431 assert!(debug.contains("512"));
432 }
433}