Skip to main content

a3s_box_runtime/vmm/
handler.rs

1//! ShimHandler — concrete VmHandler for a libkrun shim subprocess.
2
3pub 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/// Handler for a running VM subprocess (shim process).
11///
12/// Provides lifecycle operations (stop, metrics, status) for a VM identified by PID.
13pub struct ShimHandler {
14    pid: u32,
15    box_id: String,
16    /// Child process handle for proper lifecycle management.
17    /// When we spawn the process, we keep the Child to properly wait() on stop.
18    /// When we attach to an existing process, this is None.
19    process: Option<Child>,
20    /// Shared System instance for CPU metrics calculation across calls.
21    /// CPU usage requires comparing snapshots over time, so we must reuse the same System.
22    metrics_sys: Mutex<System>,
23    /// Exit code of the shim process, set when stop() collects the exit status.
24    exit_code: Option<i32>,
25}
26
27impl ShimHandler {
28    /// Create a handler for a spawned VM with process ownership.
29    ///
30    /// This constructor takes ownership of the Child process handle for proper
31    /// lifecycle management (clean shutdown with wait()).
32    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    /// Create a handler for an existing VM (attach mode).
44    ///
45    /// Used when reconnecting to a running box. We don't have a Child handle,
46    /// so we manage the process by PID only.
47    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    /// Get the box ID.
58    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        // Graceful shutdown: send configured signal first, wait, then SIGKILL if needed.
71        // This gives libkrun time to flush its virtio-blk buffers to disk.
72
73        if let Some(mut process) = self.process.take() {
74            // Step 1: Send configured stop signal for graceful shutdown
75            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            // Step 2: Wait with timeout for process to exit
82            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                        // Still running, check timeout
92                        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                        // Brief sleep before checking again
105                        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            // Attached mode: use configured signal then SIGKILL with polling
117            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            // Poll for exit with timeout
123            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                    // Error - process may not be our child (common in attached mode)
134                    let exists = unsafe { libc::kill(self.pid as i32, 0) } == 0;
135                    if !exists {
136                        return Ok(()); // Already dead
137                    }
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        // Windows version: use Child::kill() or terminate process by PID
160        if let Some(mut process) = self.process.take() {
161            tracing::debug!(pid = self.pid, box_id = %self.box_id, "Terminating VM process");
162
163            // Try graceful wait first
164            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            // Attached mode: just check if process exists
193            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        // Use the shared System instance for stateful CPU tracking
213        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        // Refresh process info - this updates the internal state for delta calculation
222        sys.refresh_process(pid);
223
224        // Try to get process information
225        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        // Process not found or not running - return empty metrics
233        VmMetrics::default()
234    }
235
236    #[cfg(unix)]
237    fn is_running(&self) -> bool {
238        // Check if process exists by sending signal 0
239        unsafe { libc::kill(self.pid as i32, 0) == 0 }
240    }
241
242    #[cfg(windows)]
243    fn is_running(&self) -> bool {
244        // Use sysinfo to check if process exists
245        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        // PID 999999999 should not exist
310        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        // Non-existent process should return default metrics
319        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        // Current process PID should be running
325        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}