-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Address updater feedback #3690
Address updater feedback #3690
Changes from all commits
5f3c18c
07e380a
3d901ba
1c77b97
4dfb9f1
01eea2a
e31f33f
065775b
9e80ffa
ac2af47
4fed45f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -133,7 +133,7 @@ extension ReleaseNotesValues { | |
releaseNotes: [String]? = nil, | ||
releaseNotesPrivacyPro: [String]? = nil, | ||
downloadProgress: Double? = nil, | ||
automaticUpdate: Bool? = nil) { | ||
automaticUpdate: Bool?) { | ||
self.status = status.rawValue | ||
self.currentVersion = currentVersion | ||
self.latestVersion = latestVersion | ||
|
@@ -145,14 +145,15 @@ extension ReleaseNotesValues { | |
self.automaticUpdate = automaticUpdate | ||
} | ||
|
||
init(from updateController: UpdateController?) { | ||
init(from updateController: UpdateController) { | ||
let currentVersion = "\(AppVersion().versionNumber) (\(AppVersion().buildNumber))" | ||
let lastUpdate = UInt((updateController?.lastUpdateCheckDate ?? Date()).timeIntervalSince1970) | ||
let lastUpdate = UInt((updateController.lastUpdateCheckDate ?? Date()).timeIntervalSince1970) | ||
|
||
guard let updateController, let latestUpdate = updateController.latestUpdate else { | ||
self.init(status: updateController?.updateProgress.toStatus ?? .loaded, | ||
guard let latestUpdate = updateController.latestUpdate else { | ||
self.init(status: updateController.updateProgress.toStatus, | ||
currentVersion: currentVersion, | ||
lastUpdate: lastUpdate) | ||
lastUpdate: lastUpdate, | ||
automaticUpdate: updateController.areAutomaticUpdatesEnabled) | ||
return | ||
} | ||
|
||
|
@@ -194,11 +195,11 @@ private extension Update { | |
private extension UpdateCycleProgress { | ||
var toStatus: ReleaseNotesValues.Status { | ||
switch self { | ||
case .updateCycleDidStart: return .loading | ||
case .updateCycleNotStarted, .updateCycleDidStart: return .loading | ||
case .downloadDidStart, .downloading: return .updateDownloading | ||
case .extractionDidStart, .extracting, .readyToInstallAndRelaunch, .installationDidStart, .installing: return .updatePreparing | ||
case .updaterError: return .updateError | ||
case .updateCycleNotStarted, .updateCycleDone: return .updateReady | ||
case .updateCycleDone: return .loaded | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -77,7 +77,10 @@ final class ReleaseNotesUserScript: NSObject, Subfeature { | |
return | ||
} | ||
|
||
let updateController = Application.appDelegate.updateController | ||
guard let updateController = Application.appDelegate.updateController else { | ||
return | ||
} | ||
Comment on lines
+80
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to satisfy the compiler as AppDelegate.updateController is implicitly unwrapped already. |
||
|
||
let values = ReleaseNotesValues(from: updateController) | ||
broker?.push(method: "onUpdate", params: values, for: self, into: webView) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -105,7 +105,7 @@ final class UpdateController: NSObject, UpdateControllerProtocol { | |
@UserDefaultsWrapper(key: .automaticUpdates, defaultValue: true) | ||
var areAutomaticUpdatesEnabled: Bool { | ||
didSet { | ||
Logger.updates.log("areAutomaticUpdatesEnabled: \(self.areAutomaticUpdatesEnabled)") | ||
Logger.updates.log("areAutomaticUpdatesEnabled: \(self.areAutomaticUpdatesEnabled, privacy: .public)") | ||
if oldValue != areAutomaticUpdatesEnabled { | ||
userDriver?.cancelAndDismissCurrentUpdate() | ||
try? configureUpdater() | ||
|
@@ -239,7 +239,7 @@ extension UpdateController: SPUUpdaterDelegate { | |
} | ||
|
||
func updater(_ updater: SPUUpdater, didAbortWithError error: Error) { | ||
Logger.updates.error("Updater did abort with error: \(error.localizedDescription)") | ||
Logger.updates.error("Updater did abort with error: \(error.localizedDescription, privacy: .public) (\(error.pixelParameters, privacy: .public))") | ||
let errorCode = (error as NSError).code | ||
guard ![Int(Sparkle.SUError.noUpdateError.rawValue), | ||
Int(Sparkle.SUError.installationCanceledError.rawValue), | ||
|
@@ -252,7 +252,7 @@ extension UpdateController: SPUUpdaterDelegate { | |
} | ||
|
||
func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) { | ||
Logger.updates.log("Updater did find valid update: \(item.displayVersionString)(\(item.versionString))") | ||
Logger.updates.log("Updater did find valid update: \(item.displayVersionString, privacy: .public)(\(item.versionString, privacy: .public))") | ||
PixelKit.fire(DebugEvent(GeneralPixel.updaterDidFindUpdate)) | ||
cachedUpdateResult = UpdateCheckResult(item: item, isInstalled: false) | ||
} | ||
|
@@ -261,7 +261,7 @@ extension UpdateController: SPUUpdaterDelegate { | |
let nsError = error as NSError | ||
guard let item = nsError.userInfo[SPULatestAppcastItemFoundKey] as? SUAppcastItem else { return } | ||
|
||
Logger.updates.log("Updater did not find update: \(String(describing: item.displayVersionString))(\(String(describing: item.versionString)))") | ||
Logger.updates.log("Updater did not find valid update: \(item.displayVersionString, privacy: .public)(\(item.versionString, privacy: .public))") | ||
PixelKit.fire(DebugEvent(GeneralPixel.updaterDidNotFindUpdate, error: error)) | ||
|
||
// Edge case: User upgrades to latest version within their rollout group | ||
|
@@ -274,30 +274,51 @@ extension UpdateController: SPUUpdaterDelegate { | |
} | ||
|
||
func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) { | ||
Logger.updates.log("Updater did download update: \(item.displayVersionString)(\(item.versionString))") | ||
Logger.updates.log("Updater did download update: \(item.displayVersionString, privacy: .public)(\(item.versionString, privacy: .public))") | ||
PixelKit.fire(DebugEvent(GeneralPixel.updaterDidDownloadUpdate)) | ||
} | ||
|
||
func updater(_ updater: SPUUpdater, didExtractUpdate item: SUAppcastItem) { | ||
Logger.updates.log("Updater did extract update: \(item.displayVersionString)(\(item.versionString))") | ||
Logger.updates.log("Updater did extract update: \(item.displayVersionString, privacy: .public)(\(item.versionString, privacy: .public))") | ||
} | ||
|
||
func updater(_ updater: SPUUpdater, willInstallUpdate item: SUAppcastItem) { | ||
Logger.updates.log("Updater will install update: \(item.displayVersionString)(\(item.versionString))") | ||
Logger.updates.log("Updater will install update: \(item.displayVersionString, privacy: .public)(\(item.versionString, privacy: .public))") | ||
} | ||
|
||
func updater(_ updater: SPUUpdater, willInstallUpdateOnQuit item: SUAppcastItem, immediateInstallationBlock immediateInstallHandler: @escaping () -> Void) -> Bool { | ||
Logger.updates.log("Updater will install update on quit: \(item.displayVersionString, privacy: .public)(\(item.versionString, privacy: .public))") | ||
userDriver?.configureResumeBlock(immediateInstallHandler) | ||
return true | ||
Comment on lines
+289
to
+292
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This delegate method isn't called in my testing scenarios, but when it does in prod, let's make sure we use the |
||
} | ||
|
||
func updater(_ updater: SPUUpdater, didFinishUpdateCycleFor updateCheck: SPUUpdateCheck, error: (any Error)?) { | ||
if error == nil { | ||
Logger.updates.log("Updater did finish update cycle") | ||
updateProgress = .updateCycleDone | ||
Logger.updates.log("Updater did finish update cycle with no error") | ||
updateProgress = .updateCycleDone(.finishedWithNoError) | ||
} else if let errorCode = (error as? NSError)?.code, errorCode == Int(Sparkle.SUError.noUpdateError.rawValue) { | ||
Logger.updates.log("Updater did finish update cycle with no update found") | ||
updateProgress = .updateCycleDone | ||
} else { | ||
Logger.updates.log("Updater did finish update cycle with error") | ||
updateProgress = .updateCycleDone(.finishedWithNoUpdateFound) | ||
} else if let error { | ||
Logger.updates.log("Updater did finish update cycle with error: \(error.localizedDescription, privacy: .public) (\(error.pixelParameters, privacy: .public))") | ||
} | ||
} | ||
|
||
func log() { | ||
Logger.updates.log("areAutomaticUpdatesEnabled: \(self.areAutomaticUpdatesEnabled, privacy: .public)") | ||
Logger.updates.log("updateProgress: \(self.updateProgress, privacy: .public)") | ||
if let cachedUpdateResult { | ||
Logger.updates.log("cachedUpdateResult: \(cachedUpdateResult.item.displayVersionString, privacy: .public)(\(cachedUpdateResult.item.versionString, privacy: .public))") | ||
} | ||
if let state = userDriver?.sparkleUpdateState { | ||
Logger.updates.log("Sparkle update state: (userInitiated: \(state.userInitiated, privacy: .public), stage: \(state.stage.rawValue, privacy: .public))") | ||
} else { | ||
Logger.updates.log("Sparkle update state: Unknown") | ||
} | ||
if let userDriver { | ||
Logger.updates.log("isResumable: \(userDriver.isResumable, privacy: .public)") | ||
} | ||
} | ||
} | ||
|
||
#endif |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,10 +40,19 @@ enum UpdateState { | |
} | ||
} | ||
|
||
enum UpdateCycleProgress { | ||
enum UpdateCycleProgress: CustomStringConvertible { | ||
enum DoneReason: Int { | ||
case finishedWithNoError = 100 | ||
case finishedWithNoUpdateFound = 101 | ||
case pausedAtDownloadCheckpoint = 102 | ||
case pausedAtRestartCheckpoint = 103 | ||
case proceededToInstallationAtRestartCheckpoint = 104 | ||
case dismissedWithNoError = 105 | ||
} | ||
|
||
case updateCycleNotStarted | ||
case updateCycleDidStart | ||
case updateCycleDone | ||
case updateCycleDone(DoneReason) | ||
case downloadDidStart | ||
case downloading(Double) | ||
case extractionDidStart | ||
|
@@ -75,19 +84,39 @@ enum UpdateCycleProgress { | |
default: return false | ||
} | ||
} | ||
|
||
var description: String { | ||
switch self { | ||
case .updateCycleNotStarted: return "updateCycleNotStarted" | ||
case .updateCycleDidStart: return "updateCycleDidStart" | ||
case .updateCycleDone(let reason): return "updateCycleDone(\(reason.rawValue))" | ||
case .downloadDidStart: return "downloadDidStart" | ||
case .downloading(let percentage): return "downloading(\(percentage))" | ||
case .extractionDidStart: return "extractionDidStart" | ||
case .extracting(let percentage): return "extracting(\(percentage))" | ||
case .readyToInstallAndRelaunch: return "readyToInstallAndRelaunch" | ||
case .installationDidStart: return "installationDidStart" | ||
case .installing: return "installing" | ||
case .updaterError(let error): return "updaterError(\(error.localizedDescription))(\(error.pixelParameters))" | ||
} | ||
} | ||
} | ||
|
||
final class UpdateUserDriver: NSObject, SPUUserDriver { | ||
enum Checkpoint: Equatable { | ||
case download | ||
case restart | ||
case download // for manual updates, pause the process before downloading the update | ||
case restart // for automatic updates, pause the process before attempting to restart | ||
} | ||
|
||
private var internalUserDecider: InternalUserDecider | ||
|
||
private var checkpoint: Checkpoint | ||
|
||
// Resume the update process when the user explicitly chooses to do so | ||
private var onResuming: (() -> Void)? | ||
private var onSkipping: () -> Void = {} | ||
|
||
// Dismiss the current update for the time being but keep the downloaded file around | ||
private var onDismiss: () -> Void = {} | ||
|
||
var isResumable: Bool { | ||
onResuming != nil | ||
|
@@ -99,6 +128,8 @@ final class UpdateUserDriver: NSObject, SPUUserDriver { | |
@Published var updateProgress = UpdateCycleProgress.default | ||
var updateProgressPublisher: Published<UpdateCycleProgress>.Publisher { $updateProgress } | ||
|
||
private(set) var sparkleUpdateState: SPUUserUpdateState? | ||
|
||
init(internalUserDecider: InternalUserDecider, | ||
areAutomaticUpdatesEnabled: Bool) { | ||
self.internalUserDecider = internalUserDecider | ||
|
@@ -109,8 +140,13 @@ final class UpdateUserDriver: NSObject, SPUUserDriver { | |
onResuming?() | ||
} | ||
|
||
func configureResumeBlock(_ block: @escaping () -> Void) { | ||
guard !isResumable else { return } | ||
onResuming = block | ||
} | ||
|
||
func cancelAndDismissCurrentUpdate() { | ||
onSkipping() | ||
onDismiss() | ||
} | ||
|
||
func show(_ request: SPUUpdatePermissionRequest) async -> SUUpdatePermissionResponse { | ||
|
@@ -122,21 +158,27 @@ final class UpdateUserDriver: NSObject, SPUUserDriver { | |
} | ||
|
||
func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { | ||
Logger.updates.log("Updater started performing the update check. (isInternalUser: \(self.internalUserDecider.isInternalUser)") | ||
Logger.updates.log("Updater started performing the update check. (isInternalUser: \(self.internalUserDecider.isInternalUser, privacy: .public))") | ||
updateProgress = .updateCycleDidStart | ||
} | ||
|
||
func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping (SPUUserUpdateChoice) -> Void) { | ||
Logger.updates.log("Updater shown update found: (userInitiated: \(state.userInitiated, privacy: .public), stage: \(state.stage.rawValue, privacy: .public))") | ||
sparkleUpdateState = state | ||
|
||
if appcastItem.isInformationOnlyUpdate { | ||
Logger.updates.log("Updater dismissed due to information only update") | ||
reply(.dismiss) | ||
} | ||
|
||
onSkipping = { reply(.skip) } | ||
onDismiss = { reply(.dismiss) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Skip can ignore the update, which we don't want. |
||
|
||
if checkpoint == .download { | ||
onResuming = { reply(.install) } | ||
updateProgress = .updateCycleDone | ||
updateProgress = .updateCycleDone(.pausedAtDownloadCheckpoint) | ||
Logger.updates.log("Updater paused at download checkpoint (manual update pending user decision)") | ||
} else { | ||
Logger.updates.log("Updater proceeded to installation at download checkpoint") | ||
reply(.install) | ||
} | ||
} | ||
|
@@ -154,11 +196,13 @@ final class UpdateUserDriver: NSObject, SPUUserDriver { | |
} | ||
|
||
func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) { | ||
Logger.updates.error("Updater encountered an error: \(error.localizedDescription, privacy: .public) (\(error.pixelParameters, privacy: .public))") | ||
updateProgress = .updaterError(error) | ||
acknowledgement() | ||
} | ||
|
||
func showDownloadInitiated(cancellation: @escaping () -> Void) { | ||
Logger.updates.log("Updater started downloading the update") | ||
updateProgress = .downloadDidStart | ||
} | ||
|
||
|
@@ -176,6 +220,7 @@ final class UpdateUserDriver: NSObject, SPUUserDriver { | |
} | ||
|
||
func showDownloadDidStartExtractingUpdate() { | ||
Logger.updates.log("Updater started extracting the update") | ||
updateProgress = .extractionDidStart | ||
} | ||
|
||
|
@@ -184,19 +229,27 @@ final class UpdateUserDriver: NSObject, SPUUserDriver { | |
} | ||
|
||
func showReady(toInstallAndRelaunch reply: @escaping (SPUUserUpdateChoice) -> Void) { | ||
onSkipping = { reply(.skip) } | ||
onDismiss = { reply(.dismiss) } | ||
|
||
if checkpoint == .restart { | ||
onResuming = { reply(.install) } | ||
updateProgress = .updateCycleDone(.pausedAtRestartCheckpoint) | ||
Logger.updates.log("Updater paused at restart checkpoint (automatic update pending user decision)") | ||
} else { | ||
reply(.install) | ||
updateProgress = .updateCycleDone(.proceededToInstallationAtRestartCheckpoint) | ||
Logger.updates.log("Updater proceeded to installation at restart checkpoint") | ||
} | ||
|
||
updateProgress = .updateCycleDone | ||
} | ||
|
||
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) { | ||
Logger.updates.info("Updater started the installation") | ||
updateProgress = .installationDidStart | ||
|
||
if !applicationTerminated { | ||
Logger.updates.log("Updater re-sent a quit event") | ||
retryTerminatingApplication() | ||
} | ||
Comment on lines
+249
to
+252
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not strictly necessary, but there seems to be no harm in doing this. |
||
} | ||
|
||
func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { | ||
|
@@ -210,7 +263,7 @@ final class UpdateUserDriver: NSObject, SPUUserDriver { | |
|
||
func dismissUpdateInstallation() { | ||
guard !updateProgress.isFailed else { return } | ||
updateProgress = .updateCycleDone | ||
updateProgress = .updateCycleDone(.dismissedWithNoError) | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This happens when Release Notes screen is displayed but the appcast hasn't been loaded yet. We get both incorrect status (updateReady), wrong button text (Restart to Update / Update DuckDuckGo), and the button doesn't even work.
For more details: https://app.asana.com/0/0/1209058536716904/f