a9fa1bf77b
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 lines
2.4 KiB
Rust
69 lines
2.4 KiB
Rust
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<UnixStream> {
|
|
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<Value> {
|
|
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<Value> {
|
|
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")),
|
|
}
|
|
}
|
|
}
|