Skip to main content

unifier/daemon/
client.rs

1//! Connect to a running hot daemon.
2
3use std::io::{BufRead, BufReader, Write};
4use std::os::unix::net::UnixStream;
5use std::path::Path;
6use std::time::Duration;
7
8use crate::daemon::paths::{pid_path, socket_path};
9use crate::daemon::protocol::{decode_response, Request, Response};
10use crate::error::{Error, Result};
11use crate::home::UnifierHome;
12use crate::postbox::Message;
13
14pub struct Client {
15    stream: UnixStream,
16}
17
18impl Client {
19    pub fn connect(home: &UnifierHome) -> Result<Self> {
20        let path = socket_path(home);
21        let stream = UnixStream::connect(&path)
22            .map_err(|e| Error::msg(format!("daemon not reachable at {}: {e}", path.display())))?;
23        stream.set_read_timeout(Some(Duration::from_secs(30)))?;
24        stream.set_write_timeout(Some(Duration::from_secs(30)))?;
25        Ok(Self { stream })
26    }
27
28    pub fn is_running(home: &UnifierHome) -> bool {
29        read_pid(home)
30            .ok()
31            .flatten()
32            .is_some_and(process_alive)
33            && socket_path(home).exists()
34    }
35
36    pub fn request(&mut self, req: Request) -> Result<Response> {
37        let line = format!("{}\n", serde_json::to_string(&req)?);
38        self.stream.write_all(line.as_bytes())?;
39        self.stream.flush()?;
40
41        let mut reader = BufReader::new(&self.stream);
42        let mut buf = String::new();
43        reader.read_line(&mut buf)?;
44        decode_response(&buf).map_err(|e| Error::msg(format!("invalid daemon response: {e}")))
45    }
46}
47
48pub fn read_pid(home: &UnifierHome) -> Result<Option<u32>> {
49    let path = pid_path(home);
50    if !path.is_file() {
51        return Ok(None);
52    }
53    let text = std::fs::read_to_string(path)?.trim().to_string();
54    if text.is_empty() {
55        return Ok(None);
56    }
57    text.parse::<u32>()
58        .map(Some)
59        .map_err(|_| Error::msg("invalid pid file"))
60}
61
62pub fn process_alive(pid: u32) -> bool {
63    Path::new(&format!("/proc/{pid}")).exists()
64}
65
66pub fn response_messages(resp: Response) -> Result<Vec<Message>> {
67    match resp {
68        Response::Ok { messages, .. } => Ok(messages.into_iter().map(Message::from).collect()),
69        Response::Err { error } => Err(Error::msg(error)),
70    }
71}
72
73pub fn response_value(resp: Response) -> Result<Option<String>> {
74    match resp {
75        Response::Ok { value, .. } => Ok(value),
76        Response::Err { error } => Err(Error::msg(error)),
77    }
78}
79
80pub fn response_uuid(resp: Response) -> Result<uuid::Uuid> {
81    match resp {
82        Response::Ok { uuid: Some(id), .. } => Ok(id),
83        Response::Ok { .. } => Err(Error::msg("daemon response missing uuid")),
84        Response::Err { error } => Err(Error::msg(error)),
85    }
86}
87
88pub fn response_found(resp: Response) -> Result<bool> {
89    match resp {
90        Response::Ok { found: Some(v), .. } => Ok(v),
91        Response::Ok { .. } => Ok(true),
92        Response::Err { error } => Err(Error::msg(error)),
93    }
94}
95
96pub fn response_ok(resp: Response) -> Result<()> {
97    match resp {
98        Response::Ok { .. } => Ok(()),
99        Response::Err { error } => Err(Error::msg(error)),
100    }
101}
102
103pub fn response_dirty(resp: Response) -> Result<bool> {
104    match resp {
105        Response::Ok { dirty: Some(v), .. } => Ok(v),
106        Response::Ok { .. } => Ok(false),
107        Response::Err { error } => Err(Error::msg(error)),
108    }
109}
110
111pub fn ping(home: &UnifierHome) -> Result<()> {
112    let mut client = Client::connect(home)?;
113    response_ok(client.request(Request::Ping)?)
114}
115
116pub fn shutdown(home: &UnifierHome) -> Result<()> {
117    if !Client::is_running(home) {
118        return Err(Error::msg("daemon is not running"));
119    }
120    let mut client = Client::connect(home)?;
121    response_ok(client.request(Request::Shutdown)?)
122}
123
124pub fn flush(home: &UnifierHome) -> Result<bool> {
125    let mut client = Client::connect(home)?;
126    response_dirty(client.request(Request::Flush)?)
127}