Skip to content

tray: let Sparkle's install quit through the window-close interception - #451

Merged
m4ttheweric merged 4 commits into
mainfrom
rt-296-sparkle-quit
Sep 25, 2026
Merged

m4ttheweric merged 4 commits into
mainfrom
rt-296-sparkle-quit

Conversation

@m4ttheweric

@m4ttheweric m4ttheweric commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Install and Relaunch now works on the first click when the mattstack window is open. Before this, the app turned Sparkle's quit into a window close, and the update only installed once a quit came in with the window already closed.

Root cause

To install, Sparkle's installer sends the app an ordinary quit event (NSRunningApplication.terminate) with no kAEQuitReason. applicationShouldTerminate hands every quit to QuitReason.shouldTerminate. That function cancels any quit that isn't from the tray menu or the OS while the window is on screen. So the app closed its window and cancelled Sparkle's quit. Sparkle kept waiting, and each retry of Install and Relaunch hit the same interception.

Fix

  • QuitReason.shouldTerminate takes two new inputs: updateInstalling and senderBundleIdentifier. The app terminates when an update is installing and the quit came from Sparkle's installer agent (org.sparkle-project.Sparkle.Updater, as in the bundled Updater.app Info.plist). A Cmd-Q, which has no AppleEvent, or a Dock quit still closes the window, even if the flag outlives a failed install.
  • AppDelegate reads the quit event's keySenderPIDAttr and gets the sender's bundle id from NSRunningApplication(processIdentifier:). Updater.app is started with SMJobSubmit, but its main calls NSApplication.sharedApplication, so it registers with LaunchServices. Today's unified log shows launchservicesd CHECKIN ... org.sparkle-project.Sparkle.Updater for the installer run. While an update is installing, every quit logs quit during update install with the sender and the decision.
  • UpdaterController sets the flag in updater(_:willInstallUpdate:) and clears it in updater(_:didFinishUpdateCycleFor:error:). AppDelegate gets the flag through a callback and doesn't read updater, because touching that lazy property would start Sparkle on a copy that's only quitting.

Why willInstallUpdate (checked against the Sparkle 2.10.0 source):

  • SPUInstallerDriver.installWithToolAndRelaunch calls installerWillFinishInstallationAndRelaunch, which fires updater(_:willInstallUpdate:) and then updaterWillRelaunchApplication(_:). It does this on the main thread, before it sends SPUResumeInstallationToStage2. The installer only sends the quit after stage 2 (AppInstaller.performStage2Installation then sendTerminationSignal), so the flag is always set before the quit arrives.
  • willInstallUpdate fires whether or not Sparkle relaunches the app. updaterWillRelaunchApplication only fires on a relaunch.
  • Sparkle calls it once per session (_notifiedDelegateInstallationWillFinish) and doesn't call it again when Install and Relaunch is retried. That's why the flag stays set until the update cycle ends and isn't reset after each quit.
  • Every install route goes through installWithToolAndRelaunch: the Install and Relaunch button, the retry button, a resumed update, and the immediateInstallationBlock from willInstallUpdateOnQuit. Plain install-on-quit sends no quit from Sparkle. It waits for the app to exit.
  • The update cycle only finishes before the quit when the install fails, so clearing the flag there turns the window-close interception back on after a failed install.

Verification

  • New checks: with the window on screen, Sparkle's installer quitting during an install terminates the app. With the window on screen, the window still closes for any of these:

    • an install is running and the quit is a plain Cmd-Q, which has no sender;
    • an install is running and the Dock sends the quit;
    • Sparkle's installer sends a quit when no install is running.

    Each new check failed before its fix.

  • swift build --package-path rt-tray is green.

  • swift run --package-path rt-tray mattstack-checks passes 528 checks with 0 failures.

  • UpdaterController names both hooked SPUUpdaterDelegate selectors with #selector, so a Sparkle rename fails the build. I checked that a misspelled selector does fail to typecheck against the framework.

  • Not run: a real Sparkle install in the app. That needs a signed build and an appcast.

🤖 Generated with Claude Code

m4ttheweric and others added 2 commits September 25, 2026 08:25
Sparkle's installer quits the app with a plain quit event, which the
window-close interception cancelled whenever the window was on screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UpdaterController raises the flag in updater(_:willInstallUpdate:), which
Sparkle calls just before asking its installer to quit the app, and
clears it when the update cycle finishes. AppDelegate passes it to
QuitReason.shouldTerminate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 40 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 85 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: b20db2c7-b226-4414-8f69-b0ef21dd111f

📥 Commits

Reviewing files that changed from the base of the PR and between e8e9af4 and df49569.

📒 Files selected for processing (4)
  • rt-tray/Sources-core/Launch/QuitReason.swift
  • rt-tray/Sources/AppDelegate.swift
  • rt-tray/Sources/Updates/UpdaterController.swift
  • rt-tray/Tests/MattstackCoreChecks/QuitReasonChecks.swift

Comment @coderabbitai help to get the list of available commands.

m4ttheweric and others added 2 commits September 25, 2026 08:35
An installing flag that outlives a failed install no longer lets a
plain Cmd-Q or Dock quit through; the quit must also come from
org.sparkle-project.Sparkle.Updater.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ames

AppDelegate resolves the quit event's sender PID to a bundle id and
logs the decision while an update installs. UpdaterController names
both hooked SPUUpdaterDelegate selectors so a Sparkle rename fails the
build instead of silently dropping the hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@m4ttheweric
m4ttheweric merged commit a8389e7 into main Sep 25, 2026
9 of 10 checks passed
@m4ttheweric
m4ttheweric deleted the rt-296-sparkle-quit branch September 25, 2026 13:48
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