1use rusty_time_api::{ControlEndpoint, ControlRequest, ControlResponse};
13use std::io::{BufRead, BufReader, Read, Write};
14use std::time::Duration;
15
16const MAX_RESPONSE_BYTES: u64 = 1024 * 1024;
19const TIMEOUT: Duration = Duration::from_secs(5);
20
21pub fn default_path() -> String {
24 rusty_time_api::default_control_spec()
25}
26
27pub fn request(spec: &str, req: &ControlRequest) -> Result<ControlResponse, String> {
29 let mut line = serde_json::to_string(req).map_err(|e| e.to_string())?;
30 line.push('\n');
31
32 match rusty_time_api::control_endpoint(spec) {
33 #[cfg(unix)]
34 ControlEndpoint::UnixPath(path) => {
35 use std::os::unix::net::UnixStream;
36 let stream =
37 UnixStream::connect(&path).map_err(|e| format!("connecting {path}: {e}"))?;
38 stream
39 .set_read_timeout(Some(TIMEOUT))
40 .map_err(|e| e.to_string())?;
41 stream
42 .set_write_timeout(Some(TIMEOUT))
43 .map_err(|e| e.to_string())?;
44 exchange(stream, &line)
45 }
46 #[cfg(not(unix))]
47 ControlEndpoint::UnixPath(path) => Err(format!(
48 "unix domain sockets are unavailable on this platform (asked for {path})"
49 )),
50 ControlEndpoint::Loopback(port) => {
51 let addr = format!("127.0.0.1:{port}");
52 let stream = std::net::TcpStream::connect(&addr)
53 .map_err(|e| format!("connecting {addr}: {e}"))?;
54 stream
55 .set_read_timeout(Some(TIMEOUT))
56 .map_err(|e| e.to_string())?;
57 stream
58 .set_write_timeout(Some(TIMEOUT))
59 .map_err(|e| e.to_string())?;
60 exchange(stream, &line)
61 }
62 }
63}
64
65fn exchange<S: Read + Write>(mut stream: S, line: &str) -> Result<ControlResponse, String> {
67 stream
68 .write_all(line.as_bytes())
69 .map_err(|e| format!("sending request: {e}"))?;
70 stream.flush().map_err(|e| e.to_string())?;
71
72 let mut reader = BufReader::new(Read::by_ref(&mut stream).take(MAX_RESPONSE_BYTES));
74 let mut response = String::new();
75 reader
76 .read_line(&mut response)
77 .map_err(|e| format!("reading response: {e}"))?;
78 if response.trim().is_empty() {
79 return Err("daemon closed the connection without answering".into());
80 }
81 serde_json::from_str(response.trim()).map_err(|e| format!("parsing response: {e}"))
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn a_missing_daemon_is_a_clear_error_not_a_hang() {
90 let err = request(
92 "rusty_time_definitely_not_running_xyz",
93 &ControlRequest::Ping,
94 )
95 .expect_err("should fail");
96 assert!(
97 err.contains("connecting"),
98 "error should name the connect step, got: {err}"
99 );
100 }
101
102 #[test]
103 fn the_default_control_name_resolves() {
104 let spec = default_path();
107 assert!(!spec.is_empty());
108 let _ = rusty_time_api::control_endpoint(&spec);
109 }
110}