use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::time::Duration;
use tracing::{info, warn};
use crate::protocol::{DaemonStatus, PushEvent, Request, Response};
use crate::state::EqBand;
pub struct DaemonClient {
stream: UnixStream,
reader: BufReader<UnixStream>,
}
impl DaemonClient {
pub fn connect() -> crate::AppResult<Self> {
let path = socket_path()?;
if let Ok(client) = Self::try_connect(&path) {
info!("Connected to existing daemon");
return Ok(client);
}
info!("No daemon found — auto-launching");
spawn_daemon();
for _ in 0..30 {
std::thread::sleep(Duration::from_millis(100));
if let Ok(client) = Self::try_connect(&path) {
info!("Connected to auto-launched daemon");
return Ok(client);
}
}
Err(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"Daemon failed to start within 3 seconds",
)
.into())
}
fn try_connect(path: &PathBuf) -> std::io::Result<Self> {
let stream = UnixStream::connect(path)?;
let timeout = Some(Duration::from_secs(5));
stream.set_read_timeout(timeout)?;
stream.set_write_timeout(timeout)?;
let reader = BufReader::new(stream.try_clone().map_err(|e| {
std::io::Error::new(
e.kind(),
format!("Failed to clone daemon socket for reading: {e}"),
)
})?);
Ok(Self { stream, reader })
}
pub fn request(&mut self, req: Request) -> crate::AppResult<Response> {
let json = serde_json::to_string(&req)?;
self.stream.write_all(json.as_bytes())?;
self.stream.write_all(b"\n")?;
self.stream.flush()?;
let mut line = String::new();
self.reader.read_line(&mut line)?;
Ok(serde_json::from_str(line.trim())?)
}
pub fn try_read_event(&mut self) -> std::io::Result<Option<PushEvent>> {
self.reader.get_mut().set_nonblocking(true)?;
let mut line = String::new();
let result = match self.reader.read_line(&mut line) {
Ok(0) => Ok(None),
Ok(_) => serde_json::from_str(line.trim())
.map(Some)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None),
Err(e) => Err(e),
};
self.reader.get_mut().set_nonblocking(false)?;
result
}
pub fn get_status(&mut self) -> crate::AppResult<DaemonStatus> {
let resp = self.request(Request::GetStatus)?;
resp.status.ok_or_else(|| {
std::io::Error::other(resp.error.unwrap_or_else(|| "No status in response".into()))
.into()
})
}
pub fn set_bands(&mut self, bands: &[EqBand]) -> crate::AppResult<()> {
let resp = self.request(Request::SetBands {
bands: bands.to_vec(),
})?;
check_ok(resp)
}
pub fn set_preamp(&mut self, gain: f32) -> crate::AppResult<()> {
let resp = self.request(Request::SetPreamp { gain })?;
check_ok(resp)
}
pub fn set_bypass(&mut self, bypass: bool) -> crate::AppResult<()> {
let resp = self.request(Request::SetBypass { bypass })?;
check_ok(resp)
}
pub fn connect_device(&mut self, node_id: u32) -> crate::AppResult<()> {
let resp = self.request(Request::ConnectDevice { node_id })?;
check_ok(resp)
}
pub fn disconnect_device(&mut self, node_id: u32) -> crate::AppResult<()> {
let resp = self.request(Request::DisconnectDevice { node_id })?;
check_ok(resp)
}
pub fn shutdown(&mut self) -> crate::AppResult<()> {
let _ = self.request(Request::Shutdown)?;
Ok(())
}
}
fn check_ok(resp: Response) -> crate::AppResult<()> {
if resp.ok {
Ok(())
} else {
Err(std::io::Error::other(resp.error.unwrap_or_else(|| "Unknown error".into())).into())
}
}
fn socket_path() -> crate::AppResult<PathBuf> {
Ok(runtime_dir()?.join("eqtui.sock"))
}
fn runtime_dir() -> crate::AppResult<PathBuf> {
match std::env::var("XDG_RUNTIME_DIR") {
Ok(dir) if !dir.is_empty() => Ok(PathBuf::from(dir)),
_ => Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"XDG_RUNTIME_DIR environment variable is not set or is empty. \
This is required for secure operation.",
)
.into()),
}
}
fn spawn_daemon() {
let Ok(exe) = std::env::current_exe() else {
warn!("Cannot determine own binary path — daemon auto-launch disabled");
return;
};
match Command::new(exe)
.arg("daemon")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
Ok(child) => {
info!(pid = child.id(), "Spawned daemon");
}
Err(e) => {
warn!(%e, "Failed to spawn daemon");
}
}
}