Skip to main content

rvf_launch/
qmp.rs

1//! QMP (QEMU Machine Protocol) client.
2//!
3//! Implements just enough of the QMP JSON protocol to negotiate
4//! capabilities and issue `system_powerdown` / `quit` commands for
5//! graceful or forced VM shutdown.
6
7use std::io::{BufRead, BufReader, Write};
8use std::os::unix::net::UnixStream;
9use std::path::Path;
10use std::time::Duration;
11
12use crate::error::LaunchError;
13
14/// A minimal QMP client connected via a Unix socket.
15pub struct QmpClient {
16    stream: UnixStream,
17}
18
19impl QmpClient {
20    /// Connect to the QMP Unix socket and perform the capability
21    /// negotiation handshake.
22    pub fn connect(socket_path: &Path, timeout: Duration) -> Result<Self, LaunchError> {
23        let stream = UnixStream::connect(socket_path).map_err(LaunchError::QmpIo)?;
24        stream
25            .set_read_timeout(Some(timeout))
26            .map_err(LaunchError::QmpIo)?;
27        stream
28            .set_write_timeout(Some(timeout))
29            .map_err(LaunchError::QmpIo)?;
30
31        let mut client = Self { stream };
32
33        // Read the server greeting (QMP banner).
34        let greeting = client.read_line()?;
35        if !greeting.contains("\"QMP\"") {
36            return Err(LaunchError::Qmp(format!(
37                "unexpected QMP greeting: {greeting}"
38            )));
39        }
40
41        // Negotiate capabilities.
42        client.send_command(r#"{"execute":"qmp_capabilities"}"#)?;
43        let resp = client.read_line()?;
44        if !resp.contains("\"return\"") {
45            return Err(LaunchError::Qmp(format!(
46                "qmp_capabilities failed: {resp}"
47            )));
48        }
49
50        Ok(client)
51    }
52
53    /// Send `system_powerdown` for a graceful ACPI shutdown.
54    pub fn system_powerdown(&mut self) -> Result<(), LaunchError> {
55        self.send_command(r#"{"execute":"system_powerdown"}"#)?;
56        let resp = self.read_line()?;
57        if resp.contains("\"error\"") {
58            return Err(LaunchError::Qmp(format!("system_powerdown failed: {resp}")));
59        }
60        Ok(())
61    }
62
63    /// Send `quit` to force QEMU to exit immediately.
64    pub fn quit(&mut self) -> Result<(), LaunchError> {
65        self.send_command(r#"{"execute":"quit"}"#)?;
66        // QEMU may close the socket before we can read the response.
67        let _ = self.read_line();
68        Ok(())
69    }
70
71    /// Send `query-status` to check the VM's run state.
72    pub fn query_status(&mut self) -> Result<String, LaunchError> {
73        self.send_command(r#"{"execute":"query-status"}"#)?;
74        self.read_line()
75    }
76
77    fn send_command(&mut self, cmd: &str) -> Result<(), LaunchError> {
78        self.stream
79            .write_all(cmd.as_bytes())
80            .map_err(LaunchError::QmpIo)?;
81        self.stream
82            .write_all(b"\n")
83            .map_err(LaunchError::QmpIo)?;
84        self.stream.flush().map_err(LaunchError::QmpIo)?;
85        Ok(())
86    }
87
88    fn read_line(&mut self) -> Result<String, LaunchError> {
89        let mut reader = BufReader::new(&self.stream);
90        let mut line = String::new();
91        reader.read_line(&mut line).map_err(LaunchError::QmpIo)?;
92        Ok(line)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    // QMP tests require a running QEMU instance, so we only test
99    // construction logic here. Full integration tests belong in
100    // tests/rvf-integration.
101    #[test]
102    fn connect_to_nonexistent_socket_fails() {
103        use super::*;
104        let result =
105            QmpClient::connect(Path::new("/tmp/nonexistent_qmp.sock"), Duration::from_secs(1));
106        assert!(result.is_err());
107    }
108}