1use 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}