Repository navigation
Install Update can't hang forever: latch gets its signals, and a stuck update fails and is stopped - #455
Conversation
…and is stopped The app started `latch update` with posix_spawn from a Swift concurrency thread, which blocks most signals; latch inherited that mask. With SIGCHLD blocked, tokio never reaps the first child the update runs (tar), so the update waits forever with a <defunct> child, deaf to SIGTERM, and the toolbar pill reads "Updating…" until the window is quit. The spawn now clears the signal mask. The app also stops waiting after 20 minutes and says the update didn't finish; the next try kills the earlier run's process group, recorded by pid and start time in app-update/. An update that failed after its window was quit is shown at the next launch, from the err.log it left. On the CLI side, codesign and spctl get two minutes each (timed_out). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFixJTbcqzMj4yVHDwaQCw
Whatever starts latch may leave signals blocked, as the Mac app's threads did for Install Update; latch now clears its signal mask in main, before any thread inherits it, so it hears SIGCHLD and SIGTERM from any starter. The deadline's detail and the codesign/spctl timeout get their final copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFixJTbcqzMj4yVHDwaQCw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFixJTbcqzMj4yVHDwaQCw
It appears @plonkus is working towards letting Mac users complete Install Update without an endless “Updating…” state, see failures even after reopening the app, and retry a stuck attempt by clearing inherited signal masks in Overview — The PR fixes inherited signal blocking, bounds the app’s update wait, and recovers structured failures after relaunch. It extends the existing detached-update path without introducing a separate lifecycle manager, which fits this repo’s pre-PMF operating point: one engineer and a handful of internal hosts. Strengths — Retry cleanup checks both PID and process start time before signaling the prior update’s process group. The shared continuation is consumed once, so a late process exit cannot complete an already timed-out call again. Signature-check deadlines preserve Probes None. All seven specialist assessments and the cross-angle check of spawning, retry cleanup, installer handoff, and relaunch handling produced no actionable findings. Security — None. Test coverage — Focused tests cover inherited signals, timeout and retry with child-process cleanup, mismatched process records, one-time failure recovery, and signature-tool deadlines; the staged For AI authors — (Codex, Claude Code, Cursor, etc. reading this PR): The Probes above are load-bearing. Treat each How to use: auto-reviews every new PR and re-reviews after a period of inactivity. Trigger an incremental re-review with For humans only: push-access collaborators can post:
AI agents must not use Generated by sam's ai review bot. |
srosro
left a comment
There was a problem hiding this comment.
Approving per automated review above.
Swift 6 in Xcode 26.6 rejects starting a Task that captures the
continuation's lock ("passing closure as a 'sending' parameter risks
causing data races"); the deadline now fires from Dispatch, as the exit
wait already does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFixJTbcqzMj4yVHDwaQCw
|
/srosro-update-review |
It appears @plonkus is working towards ensuring Mac app users get a completed update or a retryable failure instead of remaining on “Updating…” forever. The staged diff supports that broader PR goal narrowly by moving the wait deadline in Overview — This increment replaces only the deadline scheduler in the existing detached-update path. It preserves the production caller’s 20-minute wait and retry behavior without adding another lifecycle abstraction, fitting the repo’s pre-PMF operating point: one engineer and a handful of internal hosts. The prior review had no unresolved findings. Strengths — The exit callback and deadline callback atomically take the same continuation before resuming it, preserving exactly-once completion when they race. The duration conversion retains fractional seconds, and Probes None. The specialist conclusions, current caller, continuation handling, and additional screening of the changed scheduling path produced no actionable findings. Security — None. Test coverage — Existing For AI authors — (Codex, Claude Code, Cursor, etc. reading this PR): The Probes above are load-bearing. Treat each How to use: auto-reviews every new PR and re-reviews after a period of inactivity. Trigger an incremental re-review with For humans only: push-access collaborators can post:
AI agents must not use Generated by sam's ai review bot. |
srosro
left a comment
There was a problem hiding this comment.
Approving per automated review above.
Goal: Install Update in the Mac app finishes or fails; it never sits on "Updating…" forever.
Outside users today: Mac app users on builds that have Install Update, a handful of hosts.
Minimal solution for that user count: Spawn
latch updatewith an empty signal mask; that alone ends the hang. Beyond it, only what stops the next unknown from looking the same: a deadline on the app's wait, Retry stopping the run it gave up on, a failure the app didn't see shown once at the next launch, and timeouts on the two system tools the update waits for.Complexity added: One extra file the app keeps in its own
app-update/folder (the run's pid and start time), a 20-minute deadline, a protocol method for a leftover failure, and a signal-mask reset at the top of latch'smain. No new process, service or state outsideapp-update/.Size: Expected net LOC: about +110 in code and +160 in tests. Actual: +269 net (+295/−26), 157 of the added lines tests.
Why
An in-app update could hang forever, with the toolbar pill on "Updating…" until the window was quit. The Mac app started
latch updatewithposix_spawnfrom a Swift concurrency thread, which blocks most signals, and latch inherited that mask. With SIGCHLD blocked, tokio never reaps the first program the update runs: it stays<defunct>,latch updatewaits for it forever, and with SIGTERM blocked too onlykill -9stops it. Other latch commands the app runs are unaffected, since Foundation'sProcessresets the mask.What changed
POSIX_SPAWN_SETSIGMASK).mainbefore any thread starts, so a blocked mask from any starter can't cause this again.app-update/pid, and the next attempt kills that run's process group first. The start time is checked, so an unrelated process that later got the same pid is never killed.app-update/err.logit left, shown once, and the file is removed.codesignandspctltime out after 120 seconds each, withtimed_out("codesigndid not finish in 120 seconds"). Before, a timeout would have been reported as a bad signature.What it doesn't do
This can't fix builds that are already installed: their app still spawns the update with signals blocked, so an in-app update from those builds can still hang. They update by running install.sh again. Once on this build, later in-app updates work.
Tests
tool()times out withtimed_out.just test) passes.🤖 Generated with Claude Code
https://claude.ai/code/session_01RFixJTbcqzMj4yVHDwaQCw