Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
106
vendor/ruvector/crates/rvf/rvf-launch/src/qmp.rs
vendored
Normal file
106
vendor/ruvector/crates/rvf/rvf-launch/src/qmp.rs
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user