From 68f3d3c5accdca93a8f676484d98ad8b23482672 Mon Sep 17 00:00:00 2001 From: Omar Baradei Date: Tue, 9 Jun 2026 20:05:43 -0700 Subject: [PATCH] fix: make macOS desktop self-update swap+relaunch fail-fast and recoverable The macOS in-app updater's detached swap script ran with plain 'set -u' and a success-gated ditto: when ditto or the destination move failed, the script fell through silently -- the app had already quit, the old bundle stayed (or was left moved aside), and 'open "$DST"' either relaunched the stale build or nothing at all. This matches the reported "Update now does not actually update" behavior. Make the swap fail-fast and recoverable: - set -euo pipefail so unexpected failures stop the script instead of compounding. - Detect ditto and destination-replace failures explicitly; on either, fall back to opening the freshly rebuilt bundle directly so the user is never left with a dead quit. - Clean up the .hermes-update-old copy via an EXIT trap so it is removed on every exit path. Recovered from the pre-force-push head of desktop-mac-swap-fix (NousResearch/hermes-agent#38410, which lost this work to a force-push and was closed). The sqlite busy_timeout half of that PR is intentionally not revived: the BEGIN IMMEDIATE + jitter-retry redesign (#3385) deliberately keeps the connection busy handler short, and a 5s busy_timeout would override that design. Co-Authored-By: Claude Fable 5 --- apps/desktop/electron/main.cjs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index 230ef8f7fa1e..844e5b19cd95 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -1922,9 +1922,11 @@ async function applyUpdatesPosixInApp() { emitUpdateProgress({ stage: 'restart', message: 'Installing the updated app and restarting…', percent: 95 }) // Detached swapper: wait for THIS process to exit (so the bundle is free), - // ditto the rebuilt app over the running one, clear quarantine, relaunch. + // then atomically replace the running .app, clear quarantine, and reopen it. + // If the swap fails, fall back to launching the freshly rebuilt bundle + // directly so we do not leave the user with a dead quit. const swapScript = `#!/bin/bash -set -u +set -euo pipefail APP_PID=${process.pid} SRC=${shellQuote(rebuiltApp)} DST=${shellQuote(targetApp)} @@ -1932,13 +1934,21 @@ for _ in $(seq 1 240); do kill -0 "$APP_PID" 2>/dev/null || break sleep 0.5 done +cleanup() { rm -rf "$DST.hermes-update-old" 2>/dev/null || true; } +trap cleanup EXIT if [ "$SRC" != "$DST" ]; then - if /usr/bin/ditto "$SRC" "$DST.hermes-update-new"; then - rm -rf "$DST.hermes-update-old" 2>/dev/null || true - mv "$DST" "$DST.hermes-update-old" 2>/dev/null || rm -rf "$DST" - mv "$DST.hermes-update-new" "$DST" - rm -rf "$DST.hermes-update-old" 2>/dev/null || true + if ! /usr/bin/ditto "$SRC" "$DST.hermes-update-new"; then + echo "[updates] ditto failed" >&2 + /usr/bin/open "$SRC" + exit 0 fi + mv "$DST" "$DST.hermes-update-old" 2>/dev/null || rm -rf "$DST" + if ! mv "$DST.hermes-update-new" "$DST"; then + echo "[updates] destination replace failed" >&2 + /usr/bin/open "$SRC" + exit 0 + fi + rm -rf "$DST.hermes-update-old" 2>/dev/null || true fi /usr/bin/xattr -dr com.apple.quarantine "$DST" 2>/dev/null || true /usr/bin/open "$DST"