From 92706c0780d1d2cd7262cbd0cc70d98f54d8cde5 Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Tue, 9 Jun 2026 23:31:52 +0700 Subject: [PATCH] fix(app): robust spaceshd discovery for tauri dev + non-fatal connect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- DOCS/RUNNING.md | 1 + app/src-tauri/src/bridge.rs | 46 +++++++++++++++++++++++++++++++++---- app/src-tauri/src/lib.rs | 18 +++++++++++---- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/DOCS/RUNNING.md b/DOCS/RUNNING.md index b6a7aaa..f42c8f4 100644 --- a/DOCS/RUNNING.md +++ b/DOCS/RUNNING.md @@ -179,6 +179,7 @@ rm -rf ~/.spacesh # сбрасывает сокет, лок, state.json, - **`daemon unavailable` / CLI висит:** сокет битый. `pkill spaceshd; rm -f ~/.spacesh/sock` (или свой `$SPACESH_SOCK`), повтори. - **«another spaceshd is already running»:** уже есть живой демон на этом сокете — это норма; используй его или `spacesh shutdown`. - **GUI не видит демон / пусто:** проверь, что GUI и демон на одном сокете (если задавал `SPACESH_SOCK` для демона — задай тот же для GUI: `SPACESH_SOCK=… npm run tauri dev`). +- **GUI пишет «could not connect to spaceshd» (lazy-start не нашёл бинарь):** в `tauri dev` app-бинарь лежит в `app/src-tauri/target/` и демон ему не «сосед» — GUI ищет его по dev-пути в корневом `target/debug/spaceshd` (убедись, что сделан `cargo build -p spaceshd`). Можно явно указать: `SPACESHD_BIN=$PWD/target/debug/spaceshd npm run tauri dev`, либо просто подними демон сам перед GUI: `./target/debug/spaceshd &`. Окно теперь открывается даже без демона (не падает) — команды заработают, как только демон поднимется (перезапусти GUI). - **Статусы у claude не меняются:** проверь, что `claude` в PATH, и что в `~/.spacesh/hooks//settings.json` абсолютный путь к `spacesh` верный. Имена/формат хуков Claude Code дрейфуют по версиям — при несовпадении правится только `crates/spaceshd/src/hooks.rs`. - **Нет уведомлений:** проверь разрешение macOS (Системные настройки → Уведомления → spacesh) и что окно действительно не в фокусе. - **`npm run tauri dev` падает на компиляции:** прогони `cargo build -p spaceshd` отдельно, посмотри ошибку; затем `cd app && npm install`. diff --git a/app/src-tauri/src/bridge.rs b/app/src-tauri/src/bridge.rs index 8fb9010..5dae7b0 100644 --- a/app/src-tauri/src/bridge.rs +++ b/app/src-tauri/src/bridge.rs @@ -27,24 +27,62 @@ pub struct Bridge { } fn socket_path() -> Result { + // 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 { 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 { diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 2e9cf2e..f2bf9d4 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -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(()) })