fix(app): robust spaceshd discovery for tauri dev + non-fatal connect

The app is its own cargo workspace, so in 'tauri dev' the app binary lives
in app/src-tauri/target/ and spaceshd is NOT a sibling — lazy-start failed
and the .expect() crashed the window. Now: find_daemon tries SPACESHD_BIN,
sibling, repo-root target/{debug,release}, then PATH; bridge honors
SPACESH_SOCK like the daemon/CLI; setup logs instead of panicking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:31:52 +07:00
parent ec4025a683
commit 92706c0780
3 changed files with 57 additions and 8 deletions
+42 -4
View File
@@ -27,24 +27,62 @@ pub struct Bridge {
}
fn socket_path() -> Result<PathBuf> {
// Honor SPACESH_SOCK so the GUI matches a daemon/CLI started with the same
// override (mirrors the daemon's lifecycle::socket_path and the CLI client).
if let Ok(p) = std::env::var("SPACESH_SOCK") {
if !p.is_empty() {
return Ok(PathBuf::from(p));
}
}
Ok(dirs::home_dir().context("no home")?.join(".spacesh").join("sock"))
}
/// Locate the `spaceshd` binary. The Tauri app is its own cargo workspace, so in
/// `tauri dev` the app binary lives in `app/src-tauri/target/debug/` while the
/// daemon is built into the repo-root `target/debug/` — they are NOT siblings.
/// Try, in order: SPACESHD_BIN override, a sibling (release/bundled layout),
/// the repo-root dev/release target relative to the app binary, then PATH.
fn find_daemon() -> PathBuf {
if let Ok(p) = std::env::var("SPACESHD_BIN") {
if !p.is_empty() {
return PathBuf::from(p);
}
}
if let Ok(exe) = std::env::current_exe() {
let sibling = exe.with_file_name("spaceshd");
if sibling.exists() {
return sibling;
}
if let Some(dir) = exe.parent() {
// app/src-tauri/target/{debug,release} → repo-root target/{debug,release}
for rel in ["../../../../target/debug/spaceshd", "../../../../target/release/spaceshd"] {
let cand = dir.join(rel);
if cand.exists() {
return cand;
}
}
}
}
PathBuf::from("spaceshd") // last resort: rely on PATH
}
async fn ensure_daemon(sock: &PathBuf) -> Result<UnixStream> {
if let Ok(s) = UnixStream::connect(sock).await {
return Ok(s);
}
// Lazy start: spawn the daemon binary, then poll for the socket.
let exe = std::env::current_exe()?;
let daemon = exe.with_file_name("spaceshd");
let _ = std::process::Command::new(daemon).spawn();
let daemon = find_daemon();
match std::process::Command::new(&daemon).spawn() {
Ok(_) => {}
Err(e) => anyhow::bail!("could not spawn daemon at {}: {e}", daemon.display()),
}
for _ in 0..100 {
if let Ok(s) = UnixStream::connect(sock).await {
return Ok(s);
}
tokio::time::sleep(tokio::time::Duration::from_millis(30)).await;
}
anyhow::bail!("daemon did not come up")
anyhow::bail!("daemon spawned ({}) but did not bind {} in time", daemon.display(), sock.display())
}
impl Bridge {
+14 -4
View File
@@ -10,10 +10,20 @@ pub fn run() {
let handle = app.handle().clone();
// Connect the bridge on a tokio runtime, then manage it.
tauri::async_runtime::block_on(async move {
let bridge = bridge::Bridge::connect(handle.clone())
.await
.expect("failed to connect to spaceshd");
handle.manage(bridge);
match bridge::Bridge::connect(handle.clone()).await {
Ok(bridge) => {
handle.manage(bridge);
}
// Don't crash the app — open the window and log. Commands will
// error until a daemon is reachable; start it manually
// (./target/debug/spaceshd) or set SPACESHD_BIN / SPACESH_SOCK.
Err(e) => {
eprintln!(
"spacesh: could not connect to spaceshd: {e}\
start it manually or set SPACESHD_BIN/SPACESH_SOCK"
);
}
}
});
Ok(())
})