Skip to content

Commit 1cd842d

Browse files
committed
Fix update attempt refreshing pill without actually updating
The attemptUpdate() subscriber watched for .updateAvailable state to auto-confirm, but showUpdateFound used setStateAfterMinimumCheckDelay which delays the transition by up to 2 seconds. During that window, dismissUpdateInstallation (from a background probe race) could cancel the pending transition, reverting state to idle without ever confirming. The subscriber then tore down on the transient idle, silently abandoning the update. Fix: move auto-confirm to the Sparkle driver level via an autoInstallOnNextUpdate flag. When set, showUpdateFound immediately calls reply(.install) bypassing the delay entirely. The subscriber is kept as a fallback but no longer tears down on transient idle while the flag is active. Closes #2166
1 parent 99ca3c9 commit 1cd842d

2 files changed

Lines changed: 28 additions & 0 deletions

File tree

Sources/Update/UpdateController.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ class UpdateController {
177177
func attemptUpdate() {
178178
stopAttemptUpdateMonitoring()
179179
didObserveAttemptUpdateProgress = false
180+
userDriver.autoInstallOnNextUpdate = true
180181

181182
attemptInstallCancellable = viewModel.$state
182183
.receive(on: DispatchQueue.main)
@@ -187,6 +188,9 @@ class UpdateController {
187188
self.didObserveAttemptUpdateProgress = true
188189
}
189190

191+
// Fallback: auto-confirm if we reach .updateAvailable
192+
// (e.g. the driver flag was cleared by a dismiss before
193+
// showUpdateFound fired).
190194
if case .updateAvailable = state {
191195
UpdateLogStore.shared.append("attemptUpdate auto-confirming available update")
192196
state.confirm()
@@ -196,6 +200,14 @@ class UpdateController {
196200
guard self.didObserveAttemptUpdateProgress, !state.isInstallable else {
197201
return
198202
}
203+
204+
// While the driver's auto-install flag is set, the Sparkle
205+
// check may still be starting up. Don't tear down on a
206+
// transient .idle that occurs during retry/probe races.
207+
if state.isIdle, self.userDriver.autoInstallOnNextUpdate {
208+
return
209+
}
210+
199211
self.stopAttemptUpdateMonitoring()
200212
}
201213

@@ -279,6 +291,7 @@ class UpdateController {
279291
attemptInstallCancellable?.cancel()
280292
attemptInstallCancellable = nil
281293
didObserveAttemptUpdateProgress = false
294+
userDriver.autoInstallOnNextUpdate = false
282295
}
283296

284297
private func installNoUpdateDismissObserver() {

Sources/Update/UpdateDriver.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ class UpdateDriver: NSObject, SPUUserDriver {
1010
private var checkTimeoutWorkItem: DispatchWorkItem?
1111
private var lastFeedURLString: String?
1212

13+
/// When true, the next update found by Sparkle is confirmed immediately
14+
/// without waiting for the minimum-check-display delay. This prevents
15+
/// the delayed `.updateAvailable` transition from being preempted by
16+
/// a `dismissUpdateInstallation` call (e.g. from a background probe race).
17+
var autoInstallOnNextUpdate: Bool = false
18+
1319
init(viewModel: UpdateViewModel, hostBundle _: Bundle) {
1420
self.viewModel = viewModel
1521
super.init()
@@ -44,6 +50,12 @@ class UpdateDriver: NSObject, SPUUserDriver {
4450
state: SPUUserUpdateState,
4551
reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) {
4652
UpdateLogStore.shared.append("show update found: \(appcastItem.displayVersionString)")
53+
if autoInstallOnNextUpdate {
54+
autoInstallOnNextUpdate = false
55+
UpdateLogStore.shared.append("auto-installing update (attemptUpdate)")
56+
reply(.install)
57+
return
58+
}
4759
setStateAfterMinimumCheckDelay(.updateAvailable(.init(appcastItem: appcastItem, reply: reply)))
4860
}
4961

@@ -57,12 +69,14 @@ class UpdateDriver: NSObject, SPUUserDriver {
5769

5870
func showUpdateNotFoundWithError(_ error: any Error,
5971
acknowledgement: @escaping () -> Void) {
72+
autoInstallOnNextUpdate = false
6073
UpdateLogStore.shared.append("show update not found: \(formatErrorForLog(error))")
6174
setStateAfterMinimumCheckDelay(.notFound(.init(acknowledgement: acknowledgement)))
6275
}
6376

6477
func showUpdaterError(_ error: any Error,
6578
acknowledgement: @escaping () -> Void) {
79+
autoInstallOnNextUpdate = false
6680
let details = formatErrorForLog(error)
6781
UpdateLogStore.shared.append("show updater error: \(details)")
6882
setState(.error(.init(
@@ -151,6 +165,7 @@ class UpdateDriver: NSObject, SPUUserDriver {
151165
}
152166

153167
func dismissUpdateInstallation() {
168+
autoInstallOnNextUpdate = false
154169
UpdateLogStore.shared.append("dismiss update installation")
155170
if case .error = viewModel.state {
156171
UpdateLogStore.shared.append("dismiss update installation ignored (error visible)")

0 commit comments

Comments
 (0)