//! QMP (QEMU Machine Protocol) client. //! //! Implements just enough of the QMP JSON protocol to negotiate //! capabilities and issue `system_powerdown` / `quit` commands for //! graceful or forced VM shutdown. use std::io::{BufRead, BufReader, Write}; use std::os::unix::net::UnixStream; use std::path::Path; use std::time::Duration; use crate::error::LaunchError; /// A minimal QMP client connected via a Unix socket. pub struct QmpClient { stream: UnixStream, } impl QmpClient { /// Connect to the QMP Unix socket and perform the capability /// negotiation handshake. pub fn connect(socket_path: &Path, timeout: Duration) -> Result { let stream = UnixStream::connect(socket_path).map_err(LaunchError::QmpIo)?; stream .set_read_timeout(Some(timeout)) .map_err(LaunchError::QmpIo)?; stream .set_write_timeout(Some(timeout)) .map_err(LaunchError::QmpIo)?; let mut client = Self { stream }; // Read the server greeting (QMP banner). let greeting = client.read_line()?; if !greeting.contains("\"QMP\"") { return Err(LaunchError::Qmp(format!( "unexpected QMP greeting: {greeting}" ))); } // Negotiate capabilities. client.send_command(r#"{"execute":"qmp_capabilities"}"#)?; let resp = client.read_line()?; if !resp.contains("\"return\"") { return Err(LaunchError::Qmp(format!("qmp_capabilities failed: {resp}"))); } Ok(client) } /// Send `system_powerdown` for a graceful ACPI shutdown. pub fn system_powerdown(&mut self) -> Result<(), LaunchError> { self.send_command(r#"{"execute":"system_powerdown"}"#)?; let resp = self.read_line()?; if resp.contains("\"error\"") { return Err(LaunchError::Qmp(format!("system_powerdown failed: {resp}"))); } Ok(()) } /// Send `quit` to force QEMU to exit immediately. pub fn quit(&mut self) -> Result<(), LaunchError> { self.send_command(r#"{"execute":"quit"}"#)?; // QEMU may close the socket before we can read the response. let _ = self.read_line(); Ok(()) } /// Send `query-status` to check the VM's run state. pub fn query_status(&mut self) -> Result { self.send_command(r#"{"execute":"query-status"}"#)?; self.read_line() } fn send_command(&mut self, cmd: &str) -> Result<(), LaunchError> { self.stream .write_all(cmd.as_bytes()) .map_err(LaunchError::QmpIo)?; self.stream.write_all(b"\n").map_err(LaunchError::QmpIo)?; self.stream.flush().map_err(LaunchError::QmpIo)?; Ok(()) } fn read_line(&mut self) -> Result { let mut reader = BufReader::new(&self.stream); let mut line = String::new(); reader.read_line(&mut line).map_err(LaunchError::QmpIo)?; Ok(line) } } #[cfg(test)] mod tests { // QMP tests require a running QEMU instance, so we only test // construction logic here. Full integration tests belong in // tests/rvf-integration. #[test] fn connect_to_nonexistent_socket_fails() { use super::*; let result = QmpClient::connect( Path::new("/tmp/nonexistent_qmp.sock"), Duration::from_secs(1), ); assert!(result.is_err()); } }