Skip to content

feat: auto-install a staged update at the next quiet moment#21

Merged
pantafive merged 1 commit into
mainfrom
feat/quiet-update-retry
Jun 12, 2026
Merged

feat: auto-install a staged update at the next quiet moment#21
pantafive merged 1 commit into
mainfrom
feat/quiet-update-retry

Conversation

@pantafive

Copy link
Copy Markdown
Owner

Problem

With auto-update on, Sparkle silently downloads and stages the new build, then the app relaunches into it — but only if it's already in the background with the output device idle at the instant staging finishes. That check ran once. Catch the user mid-call with the popover open and the staged update fell back to a manual "Restart" click, which reads as "auto-update didn't work" — exactly what was reported for 0.16.0 → 0.17.0.

Fix

Re-evaluate the same quiet-window guard whenever one can open, instead of giving up after the single check:

  • App resigns active (popover dismissed, user switched away) — didResignActiveNotification.
  • Output device falls idle (playback or a call ended) — a HAL listener on kAudioDevicePropertyDeviceIsRunningSomewhere, re-armed across default-output changes.
  • A low-frequency timer (15s) backstops both so correctness never depends on those signals firing — the sibling per-process running property is a documented non-deliverer on macOS 26, so the device-level listener is treated as best-effort, not load-bearing.

Every watcher is armed only while a staged update waits and is torn down on relaunch — nothing runs in steady state, honoring the "don't burn resources idling" constraint. The audio guard is kept deliberately: a relaunch mid-playback drops process taps for a moment and would blip per-app volumes to unity. The "Restart to Update" row and install-on-quit remain as the explicit/backstop paths.

Net effect: with auto-update enabled it now installs fully silently at the first unobtrusive moment, no clicks.

Testing

make build (Swift 6, strict concurrency complete), make test (43 tests), pre-commit — all green. No unit test added: the path depends on NSApp.isActive, live HAL state, and Sparkle staging an update, none observable without a running app and a real newer build to stage; the bounded timer is what makes the outcome guaranteed rather than event-timing-dependent.

Auto-update staged the build but only relaunched if the app was already
backgrounded and silent at the instant staging finished — a one-shot
check. Miss it (on a call, popover open) and the update stranded on a
manual "Restart" click, against the intent of "update automatically".

Re-evaluate the same guard whenever a quiet window can open: the app
resigning active and the output device falling idle, for an instant
reaction — backed by a low-frequency timer so correctness never rests on
those signals firing (the sibling per-process running property is a known
non-deliverer on macOS 26). Every watcher exists only while an update
waits and is torn down on relaunch; nothing runs in steady state. The
audio guard stays — a relaunch mid-playback would blip per-app taps to
unity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pantafive pantafive merged commit 917f57e into main Jun 12, 2026
1 check passed
@pantafive pantafive deleted the feat/quiet-update-retry branch June 12, 2026 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant