Files
spaceshell/crates/spacesh-cli/src/client.rs
T
2026-06-09 22:17:15 +07:00

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")),
}
}
}