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, tick_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    /// Kept across requests so buffered bytes are not dropped between calls.
17    reader: BufReader<UnixStream>,
18}
19
20impl Client {
21    pub fn connect(home: &UnifierHome) -> Result<Self> {
22        Self::connect_path(socket_path(home))
23    }
24
25    /// Connect to `.daemon/tick.sock` (tick lifecycle control plane for Jan).
26    pub fn connect_tick(home: &UnifierHome) -> Result<Self> {
27        Self::connect_path(tick_socket_path(home))
28    }
29
30    fn connect_path(path: std::path::PathBuf) -> Result<Self> {
31        let stream = UnixStream::connect(&path)
32            .map_err(|e| Error::msg(format!("daemon not reachable at {}: {e}", path.display())))?;
33        stream.set_read_timeout(Some(Duration::from_secs(30)))?;
34        stream.set_write_timeout(Some(Duration::from_secs(30)))?;
35        let reader = BufReader::new(stream.try_clone()?);
36        Ok(Self { stream, reader })
37    }
38
39    pub fn is_running(home: &UnifierHome) -> bool {
40        read_pid(home).ok().flatten().is_some_and(process_alive) && socket_path(home).exists()
41    }
42
43    pub fn request(&mut self, req: Request) -> Result<Response> {
44        let line = format!("{}\n", serde_json::to_string(&req)?);
45        self.stream.write_all(line.as_bytes())?;
46        self.stream.flush()?;
47
48        let mut buf = String::new();
49        if self.reader.read_line(&mut buf)? == 0 {
50            return Err(Error::msg("daemon closed the connection"));
51        }
52        decode_response(&buf).map_err(|e| Error::msg(format!("invalid daemon response: {e}")))
53    }
54}
55
56pub fn read_pid(home: &UnifierHome) -> Result<Option<u32>> {
57    let path = pid_path(home);
58    if !path.is_file() {
59        return Ok(None);
60    }
61    let text = std::fs::read_to_string(path)?.trim().to_string();
62    if text.is_empty() {
63        return Ok(None);
64    }
65    text.parse::<u32>()
66        .map(Some)
67        .map_err(|_| Error::msg("invalid pid file"))
68}
69
70pub fn process_alive(pid: u32) -> bool {
71    Path::new(&format!("/proc/{pid}")).exists()
72}
73
74pub fn response_messages(resp: Response) -> Result<Vec<Message>> {
75    match resp {
76        Response::Ok { messages, .. } => Ok(messages.into_iter().map(Message::from).collect()),
77        Response::Err { error } => Err(Error::msg(error)),
78    }
79}
80
81pub fn response_value(resp: Response) -> Result<Option<String>> {
82    match resp {
83        Response::Ok { value, .. } => Ok(value),
84        Response::Err { error } => Err(Error::msg(error)),
85    }
86}
87
88pub fn response_uuid(resp: Response) -> Result<uuid::Uuid> {
89    match resp {
90        Response::Ok { uuid: Some(id), .. } => Ok(id),
91        Response::Ok { .. } => Err(Error::msg("daemon response missing uuid")),
92        Response::Err { error } => Err(Error::msg(error)),
93    }
94}
95
96pub fn response_found(resp: Response) -> Result<bool> {
97    match resp {
98        Response::Ok { found: Some(v), .. } => Ok(v),
99        Response::Ok { .. } => Ok(true),
100        Response::Err { error } => Err(Error::msg(error)),
101    }
102}
103
104pub fn response_ok(resp: Response) -> Result<()> {
105    match resp {
106        Response::Ok { .. } => Ok(()),
107        Response::Err { error } => Err(Error::msg(error)),
108    }
109}
110
111pub fn response_dirty(resp: Response) -> Result<bool> {
112    match resp {
113        Response::Ok { dirty: Some(v), .. } => Ok(v),
114        Response::Ok { .. } => Ok(false),
115        Response::Err { error } => Err(Error::msg(error)),
116    }
117}
118
119pub fn ping(home: &UnifierHome) -> Result<()> {
120    let mut client = Client::connect(home)?;
121    response_ok(client.request(Request::Ping)?)
122}
123
124pub fn shutdown(home: &UnifierHome) -> Result<()> {
125    if !Client::is_running(home) {
126        return Err(Error::msg("daemon is not running"));
127    }
128    let mut client = Client::connect(home)?;
129    response_ok(client.request(Request::Shutdown)?)
130}
131
132pub fn flush(home: &UnifierHome) -> Result<bool> {
133    let mut client = Client::connect(home)?;
134    response_dirty(client.request(Request::Flush)?)
135}