fix(app): drop blocking version-handshake; Shutdown is fire-and-forget
The handshake ran synchronously in Bridge::connect: on a build-id mismatch it sent Cmd::Shutdown and awaited a reply that never flushes (the daemon exits first), so request() hit its 5s timeout and the reconnect-retry respawned the daemon and re-sent Shutdown — a loop that produced repeated 'spaceshd listening' lines and a multi-second launch delay. The id stamps also differed between the separately-built daemon and GUI, so it fired on normal launches. - Remove the handshake auto-restart; `make install`/`reinstall` already kill and replace the daemon reliably. health.build stays for display in Settings. - Shutdown now goes through a fire-and-forget send (no reply wait, no retry), fixing the same loop for the Settings Restart button. - Makefile: `make app-bundle` builds just the .app via `tauri build --bundles app` (no .dmg, no hdiutil) and `reinstall` uses it — faster self-update that can't hang on a mounted DMG volume. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-28
@@ -120,7 +120,7 @@ impl Bridge {
|
||||
let pending: Arc<Mutex<HashMap<u64, oneshot::Sender<Envelope>>>> = Arc::default();
|
||||
let out_channels: Arc<Mutex<HashMap<String, Channel<Vec<u8>>>>> = Arc::default();
|
||||
let (tx, reader) = spawn_connection(&sock, &app, pending.clone(), out_channels.clone()).await?;
|
||||
let bridge = Self {
|
||||
Ok(Self {
|
||||
next_id: AtomicU64::new(1),
|
||||
app,
|
||||
sock,
|
||||
@@ -130,34 +130,16 @@ impl Bridge {
|
||||
reader: Mutex::new(reader),
|
||||
pending,
|
||||
out_channels,
|
||||
};
|
||||
bridge.ensure_matching_daemon().await;
|
||||
Ok(bridge)
|
||||
})
|
||||
}
|
||||
|
||||
/// If a previously-running daemon is a different build than the one bundled
|
||||
/// with this GUI (the daemon outlives the GUI, so an old one can still be
|
||||
/// serving the socket), restart it so our matching daemon takes over. No-op
|
||||
/// for unstamped dev builds or when the daemon is too old to report a build.
|
||||
async fn ensure_matching_daemon(&self) {
|
||||
let want = option_env!("SPACESH_BUILD").unwrap_or("dev");
|
||||
if want == "dev" {
|
||||
return;
|
||||
}
|
||||
let got = match self.request(Cmd::Health).await {
|
||||
Ok(env) => data_of(env)
|
||||
.ok()
|
||||
.and_then(|v| v.get("build").and_then(|b| b.as_str()).map(str::to_string))
|
||||
.unwrap_or_default(),
|
||||
Err(_) => return,
|
||||
};
|
||||
if got.is_empty() || got == want {
|
||||
return;
|
||||
}
|
||||
// Stale daemon — stop it and respawn our bundled one (matching code).
|
||||
let seen = self.gen.load(Ordering::Acquire);
|
||||
let _ = self.request(Cmd::Shutdown).await; // daemon exits; reply may not arrive
|
||||
let _ = self.reconnect(seen).await; // ensure_daemon respawns the bundled daemon
|
||||
/// Send a command without awaiting a reply or retrying. Used for Shutdown:
|
||||
/// the daemon exits before its reply is flushed, so a normal request() would
|
||||
/// time out and the reconnect-retry would respawn-and-reshutdown in a loop.
|
||||
async fn fire(&self, cmd: Cmd) {
|
||||
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
let tx = self.tx.lock().await.clone();
|
||||
let _ = tx.send(Envelope::Req { id, cmd }).await;
|
||||
}
|
||||
|
||||
/// Re-establish the daemon connection. Single-flight: callers pass the `gen`
|
||||
@@ -461,5 +443,8 @@ pub async fn set_config(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn shutdown_daemon(state: BridgeState<'_>) -> Result<Value, String> {
|
||||
data_of(state.request(Cmd::Shutdown).await.map_err(|e| e.to_string())?)
|
||||
// Fire-and-forget: the daemon exits without a flushed reply, so awaiting one
|
||||
// would time out and trigger a respawn-then-reshutdown loop.
|
||||
state.fire(Cmd::Shutdown).await;
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user