use std::path::PathBuf; use anyhow::{anyhow, Context, Result}; use serde_json::Value; use spacesh_proto::codec::{read_frame, write_frame}; use spacesh_proto::{Cmd, Envelope}; use tokio::net::UnixStream; pub fn socket_path() -> PathBuf { if let Ok(p) = std::env::var("SPACESH_SOCK") { if !p.is_empty() { return PathBuf::from(p); } } let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); PathBuf::from(home).join(".spacesh").join("sock") } /// Connect, lazy-starting the daemon if the socket is absent. async fn connect_or_start() -> Result { let sock = socket_path(); if let Ok(s) = UnixStream::connect(&sock).await { return Ok(s); } // Locate the daemon next to this binary and spawn it. let exe = std::env::current_exe().context("current_exe")?; let daemon = exe.with_file_name("spaceshd"); let _ = std::process::Command::new(daemon).spawn(); for _ in 0..100 { if let Ok(s) = UnixStream::connect(&sock).await { return Ok(s); } tokio::time::sleep(std::time::Duration::from_millis(30)).await; } Err(anyhow!("daemon unavailable")) } /// One-shot request/response. Skips any interleaved events; returns `data` on ok. pub async fn request(cmd: Cmd) -> Result { let mut stream = connect_or_start().await?; send_and_read(&mut stream, cmd).await } /// Best-effort status notify: connect only (no spawn); silently succeed if absent. pub async fn notify(cmd: Cmd) -> Result<()> { let sock = socket_path(); let Ok(mut stream) = UnixStream::connect(&sock).await else { return Ok(()); // no daemon — best-effort no-op }; let _ = send_and_read(&mut stream, cmd).await; Ok(()) } async fn send_and_read(stream: &mut UnixStream, cmd: Cmd) -> Result { write_frame(stream, &Envelope::Req { id: 1, cmd }).await.map_err(|e| anyhow!(e.to_string()))?; loop { match read_frame(stream).await.map_err(|e| anyhow!(e.to_string()))? { Some(Envelope::Res { id: 1, ok, data, error }) => { if ok { return Ok(data); } let (code, msg) = error.map(|e| (e.code, e.msg)).unwrap_or_else(|| ("ERROR".into(), "error".into())); return Err(anyhow!("{code}: {msg}")); } Some(_) => continue, // events / non-matching res None => return Err(anyhow!("connection closed")), } } }