Merge fix-launch-delay: no blocking handshake, fire-and-forget shutdown, .app-only reinstall

This commit is contained in:
2026-06-15 13:58:05 +07:00
2 changed files with 28 additions and 32 deletions
+15 -4
View File
@@ -44,14 +44,18 @@ targets: ## add rust targets for the universal build
.PHONY: dmg
dmg: targets ## build the universal (Intel + Apple Silicon) .dmg — UNSIGNED
# Tauri's universal build compiles each arch separately and expects a sidecar
# named with THAT arch's triple; it lipo's them into the universal bundle and
# ships spaceshd inside spacesh.app/Contents/MacOS (else the GUI is offline).
# Tauri's universal build needs BOTH the per-arch sidecars (resolved during each
# arch sub-build) AND a fat spaceshd-universal-apple-darwin (copied into the final
# bundle — Tauri does not lipo sidecars itself). spaceshd ships inside
# spacesh.app/Contents/MacOS else the GUI is offline.
cargo build --release -p spaceshd --target aarch64-apple-darwin
cargo build --release -p spaceshd --target x86_64-apple-darwin
rm -rf $(SIDECAR_DIR) && mkdir -p $(SIDECAR_DIR) # avoid stale sidecars poisoning the bundle
cp target/aarch64-apple-darwin/release/spaceshd $(SIDECAR_DIR)/spaceshd-aarch64-apple-darwin
cp target/x86_64-apple-darwin/release/spaceshd $(SIDECAR_DIR)/spaceshd-x86_64-apple-darwin
lipo -create -output $(SIDECAR_DIR)/spaceshd-universal-apple-darwin \
target/aarch64-apple-darwin/release/spaceshd \
target/x86_64-apple-darwin/release/spaceshd
cd $(APP_DIR) && npm run tauri build -- --target $(TAURI_TARGET) --config $(BUNDLE_CONFIG)
@echo "$(DMG_DIR)" && ls -lh $(DMG_DIR)/*.dmg
@@ -63,6 +67,13 @@ dmg-native: ## build a .dmg for the current arch only (faster)
cd $(APP_DIR) && npm run tauri build -- --config $(BUNDLE_CONFIG)
@ls -lh $(NATIVE_DMG_DIR)/*.dmg
.PHONY: app-bundle
app-bundle: ## build just the native .app (no .dmg/hdiutil — fast, for self-install)
cargo build --release -p spaceshd
rm -rf $(SIDECAR_DIR) && mkdir -p $(SIDECAR_DIR)
cp target/release/spaceshd $(SIDECAR_DIR)/spaceshd-$(NATIVE_TRIPLE)
cd $(APP_DIR) && npm run tauri build -- --bundles app --config $(BUNDLE_CONFIG)
.PHONY: dev
dev: ## run the app in dev mode (tauri dev)
cd $(APP_DIR) && npm run tauri dev
@@ -90,7 +101,7 @@ install-universal: kill-daemon ## install the universal .app to /Applications
xattr -dr com.apple.quarantine /Applications/spacesh.app
.PHONY: reinstall
reinstall: dmg-native install ## native rebuild + reinstall + restart daemon (fast self-update)
reinstall: app-bundle install ## fast self-update: build .app (no dmg), reinstall, restart daemon
# ---- Tests ----
+13 -28
View File
@@ -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)
}