a3s_box_runtime/vmm/
handler.rs1pub 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 box_id: String,
16 process: Option<Child>,
20 metrics_sys: Mutex<System>,
23 exit_code: Option<i32>,
25}
26
27impl ShimHandler {
28 pub fn from_child(process: Child, box_id: String) -> Self {
33 let pid = process.id();
34 Self {
35 pid,
36 box_id,
37 process: Some(process),
38 metrics_sys: Mutex::new(System::new()),
39 exit_code: None,
40 }
41 }
42
43 pub fn from_pid(pid: u32, box_id: String) -> Self {
48 Self {
49 pid,
50 box_id,
51 process: None,
52 metrics_sys: Mutex::new(System::new()),
53 exit_code: None,
54 }
55 }
56
57 pub fn box_id(&self) -> &str {
59 &self.box_id
60 }
61}
62
63impl VmHandler for ShimHandler {
64 fn pid(&self) -> u32 {
65 self.pid
66 }
67
68 #[cfg(unix)]
69 fn stop(&mut self, signal: i32, timeout_ms: u64) -> Result<()> {
70 if let Some(mut process) = self.process.take() {
74 let pid = process.id();
76 tracing::debug!(pid, box_id = %self.box_id, signal, "Sending stop signal to VM process");
77 unsafe {
78 libc::kill(pid as i32, signal);
79 }
80
81 let start = std::time::Instant::now();
83 loop {
84 match process.try_wait() {
85 Ok(Some(status)) => {
86 tracing::debug!(pid, ?status, "VM process exited gracefully");
87 self.exit_code = status.code();
88 return Ok(());
89 }
90 Ok(None) => {
91 if start.elapsed().as_millis() > timeout_ms as u128 {
93 tracing::warn!(
94 pid,
95 timeout_ms,
96 "VM process did not exit gracefully, sending SIGKILL"
97 );
98 let _ = process.kill();
99 if let Ok(status) = process.wait() {
100 self.exit_code = status.code();
101 }
102 return Ok(());
103 }
104 std::thread::sleep(std::time::Duration::from_millis(50));
106 }
107 Err(e) => {
108 tracing::warn!(pid, error = %e, "Error checking process status, forcing kill");
109 let _ = process.kill();
110 let _ = process.wait();
111 return Ok(());
112 }
113 }
114 }
115 } else {
116 tracing::debug!(pid = self.pid, box_id = %self.box_id, signal, "Sending stop signal to attached VM process");
118 unsafe {
119 libc::kill(self.pid as i32, signal);
120 }
121
122 let start = std::time::Instant::now();
124 loop {
125 let mut status: i32 = 0;
126 let result = unsafe { libc::waitpid(self.pid as i32, &mut status, libc::WNOHANG) };
127
128 if result > 0 {
129 tracing::debug!(pid = self.pid, "VM process exited gracefully");
130 return Ok(());
131 }
132 if result < 0 {
133 let exists = unsafe { libc::kill(self.pid as i32, 0) } == 0;
135 if !exists {
136 return Ok(()); }
138 }
139
140 if start.elapsed().as_millis() > timeout_ms as u128 {
141 tracing::warn!(
142 pid = self.pid,
143 timeout_ms,
144 "VM process did not exit gracefully, sending SIGKILL"
145 );
146 unsafe {
147 libc::kill(self.pid as i32, libc::SIGKILL);
148 }
149 return Ok(());
150 }
151
152 std::thread::sleep(std::time::Duration::from_millis(50));
153 }
154 }
155 }
156
157 #[cfg(windows)]
158 fn stop(&mut self, _signal: i32, timeout_ms: u64) -> Result<()> {
159 if let Some(mut process) = self.process.take() {
161 tracing::debug!(pid = self.pid, box_id = %self.box_id, "Terminating VM process");
162
163 let start = std::time::Instant::now();
165 loop {
166 match process.try_wait() {
167 Ok(Some(status)) => {
168 tracing::debug!(pid = self.pid, ?status, "VM process exited");
169 self.exit_code = status.code();
170 return Ok(());
171 }
172 Ok(None) => {
173 if start.elapsed().as_millis() > timeout_ms as u128 {
174 tracing::warn!(pid = self.pid, "VM process did not exit, forcing kill");
175 let _ = process.kill();
176 if let Ok(status) = process.wait() {
177 self.exit_code = status.code();
178 }
179 return Ok(());
180 }
181 std::thread::sleep(std::time::Duration::from_millis(50));
182 }
183 Err(e) => {
184 tracing::warn!(pid = self.pid, error = %e, "Error checking process, forcing kill");
185 let _ = process.kill();
186 let _ = process.wait();
187 return Ok(());
188 }
189 }
190 }
191 } else {
192 tracing::debug!(pid = self.pid, "Checking attached VM process");
194 let start = std::time::Instant::now();
195 while start.elapsed().as_millis() <= timeout_ms as u128 {
196 if !self.is_running() {
197 return Ok(());
198 }
199 std::thread::sleep(std::time::Duration::from_millis(50));
200 }
201 tracing::warn!(
202 pid = self.pid,
203 "Attached VM process still running after timeout"
204 );
205 Ok(())
206 }
207 }
208
209 fn metrics(&self) -> VmMetrics {
210 let pid = Pid::from_u32(self.pid);
211
212 let mut sys = match self.metrics_sys.lock() {
214 Ok(guard) => guard,
215 Err(e) => {
216 tracing::warn!(error = %e, "metrics_sys lock poisoned");
217 return VmMetrics::default();
218 }
219 };
220
221 sys.refresh_process(pid);
223
224 if let Some(proc_info) = sys.process(pid) {
226 return VmMetrics {
227 cpu_percent: Some(proc_info.cpu_usage()),
228 memory_bytes: Some(proc_info.memory()),
229 };
230 }
231
232 VmMetrics::default()
234 }
235
236 #[cfg(unix)]
237 fn is_running(&self) -> bool {
238 unsafe { libc::kill(self.pid as i32, 0) == 0 }
240 }
241
242 #[cfg(windows)]
243 fn is_running(&self) -> bool {
244 let mut sys = System::new();
246 sys.refresh_process(Pid::from_u32(self.pid));
247 sys.process(Pid::from_u32(self.pid)).is_some()
248 }
249
250 fn exit_code(&self) -> Option<i32> {
251 self.exit_code
252 }
253
254 fn try_wait_exit(&mut self) -> Result<Option<i32>> {
255 if self.exit_code.is_some() {
256 return Ok(self.exit_code);
257 }
258
259 let Some(process) = self.process.as_mut() else {
260 return Ok(None);
261 };
262
263 match process.try_wait() {
264 Ok(Some(status)) => {
265 self.exit_code = status.code();
266 Ok(self.exit_code)
267 }
268 Ok(None) => Ok(None),
269 Err(e) => Err(a3s_box_core::error::BoxError::ExecError(format!(
270 "Failed to poll VM process {}: {}",
271 self.pid, e
272 ))),
273 }
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_vm_metrics_default() {
283 let m = VmMetrics::default();
284 assert!(m.cpu_percent.is_none());
285 assert!(m.memory_bytes.is_none());
286 }
287
288 #[test]
289 fn test_vm_metrics_clone() {
290 let m = VmMetrics {
291 cpu_percent: Some(50.0),
292 memory_bytes: Some(1024 * 1024),
293 };
294 let cloned = m.clone();
295 assert_eq!(cloned.cpu_percent, Some(50.0));
296 assert_eq!(cloned.memory_bytes, Some(1024 * 1024));
297 }
298
299 #[test]
300 fn test_shim_handler_from_pid() {
301 let handler = ShimHandler::from_pid(12345, "box-abc".to_string());
302 assert_eq!(handler.pid(), 12345);
303 assert_eq!(handler.box_id(), "box-abc");
304 assert_eq!(handler.exit_code(), None);
305 }
306
307 #[test]
308 fn test_shim_handler_is_running_nonexistent_pid() {
309 let handler = ShimHandler::from_pid(999_999_999, "test".to_string());
311 assert!(!handler.is_running());
312 }
313
314 #[test]
315 fn test_shim_handler_metrics_nonexistent_pid() {
316 let handler = ShimHandler::from_pid(999_999_999, "test".to_string());
317 let m = handler.metrics();
318 assert!(m.cpu_percent.is_none() || m.cpu_percent == Some(0.0));
320 }
321
322 #[test]
323 fn test_shim_handler_is_running_current_process() {
324 let pid = std::process::id();
326 let handler = ShimHandler::from_pid(pid, "self".to_string());
327 assert!(handler.is_running());
328 }
329
330 #[test]
331 fn test_default_shutdown_timeout() {
332 assert_eq!(DEFAULT_SHUTDOWN_TIMEOUT_MS, 10_000);
333 }
334
335 #[test]
336 fn test_vm_metrics_debug() {
337 let m = VmMetrics {
338 cpu_percent: Some(25.5),
339 memory_bytes: Some(512),
340 };
341 let debug = format!("{:?}", m);
342 assert!(debug.contains("25.5"));
343 assert!(debug.contains("512"));
344 }
345}