use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::path::Path;
use std::time::Duration;
use crate::daemon::paths::{pid_path, socket_path};
use crate::daemon::protocol::{decode_response, Request, Response};
use crate::error::{Error, Result};
use crate::home::UnifierHome;
use crate::postbox::Message;
pub struct Client {
stream: UnixStream,
}
impl Client {
pub fn connect(home: &UnifierHome) -> Result<Self> {
let path = socket_path(home);
let stream = UnixStream::connect(&path)
.map_err(|e| Error::msg(format!("daemon not reachable at {}: {e}", path.display())))?;
stream.set_read_timeout(Some(Duration::from_secs(30)))?;
stream.set_write_timeout(Some(Duration::from_secs(30)))?;
Ok(Self { stream })
}
pub fn is_running(home: &UnifierHome) -> bool {
read_pid(home)
.ok()
.flatten()
.is_some_and(process_alive)
&& socket_path(home).exists()
}
pub fn request(&mut self, req: Request) -> Result<Response> {
let line = format!("{}\n", serde_json::to_string(&req)?);
self.stream.write_all(line.as_bytes())?;
self.stream.flush()?;
let mut reader = BufReader::new(&self.stream);
let mut buf = String::new();
reader.read_line(&mut buf)?;
decode_response(&buf).map_err(|e| Error::msg(format!("invalid daemon response: {e}")))
}
}
pub fn read_pid(home: &UnifierHome) -> Result<Option<u32>> {
let path = pid_path(home);
if !path.is_file() {
return Ok(None);
}
let text = std::fs::read_to_string(path)?.trim().to_string();
if text.is_empty() {
return Ok(None);
}
text.parse::<u32>()
.map(Some)
.map_err(|_| Error::msg("invalid pid file"))
}
pub fn process_alive(pid: u32) -> bool {
Path::new(&format!("/proc/{pid}")).exists()
}
pub fn response_messages(resp: Response) -> Result<Vec<Message>> {
match resp {
Response::Ok { messages, .. } => Ok(messages.into_iter().map(Message::from).collect()),
Response::Err { error } => Err(Error::msg(error)),
}
}
pub fn response_value(resp: Response) -> Result<Option<String>> {
match resp {
Response::Ok { value, .. } => Ok(value),
Response::Err { error } => Err(Error::msg(error)),
}
}
pub fn response_uuid(resp: Response) -> Result<uuid::Uuid> {
match resp {
Response::Ok { uuid: Some(id), .. } => Ok(id),
Response::Ok { .. } => Err(Error::msg("daemon response missing uuid")),
Response::Err { error } => Err(Error::msg(error)),
}
}
pub fn response_found(resp: Response) -> Result<bool> {
match resp {
Response::Ok { found: Some(v), .. } => Ok(v),
Response::Ok { .. } => Ok(true),
Response::Err { error } => Err(Error::msg(error)),
}
}
pub fn response_ok(resp: Response) -> Result<()> {
match resp {
Response::Ok { .. } => Ok(()),
Response::Err { error } => Err(Error::msg(error)),
}
}
pub fn response_dirty(resp: Response) -> Result<bool> {
match resp {
Response::Ok { dirty: Some(v), .. } => Ok(v),
Response::Ok { .. } => Ok(false),
Response::Err { error } => Err(Error::msg(error)),
}
}
pub fn ping(home: &UnifierHome) -> Result<()> {
let mut client = Client::connect(home)?;
response_ok(client.request(Request::Ping)?)
}
pub fn shutdown(home: &UnifierHome) -> Result<()> {
if !Client::is_running(home) {
return Err(Error::msg("daemon is not running"));
}
let mut client = Client::connect(home)?;
response_ok(client.request(Request::Shutdown)?)
}
pub fn flush(home: &UnifierHome) -> Result<bool> {
let mut client = Client::connect(home)?;
response_dirty(client.request(Request::Flush)?)
}