1use 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
14pub struct QmpClient {
16 stream: UnixStream,
17}
18
19impl QmpClient {
20 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 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 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 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 pub fn quit(&mut self) -> Result<(), LaunchError> {
65 self.send_command(r#"{"execute":"quit"}"#)?;
66 let _ = self.read_line();
68 Ok(())
69 }
70
71 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 #[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}