Files
wifi-densepose/vendor/ruvector/crates/rvf/rvf-launch/src/qmp.rs

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());
}
}