107 lines
3.4 KiB
Rust
107 lines
3.4 KiB
Rust
//! 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<Self, LaunchError> {
|
|
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<String, LaunchError> {
|
|
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<String, LaunchError> {
|
|
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());
|
|
}
|
|
}
|