Move some updater stuff to being an actor

This commit is contained in:
Max Goedjen 2022-01-18 16:38:59 -05:00
parent cb206a18c2
commit e86b9d2465
No known key found for this signature in database
GPG Key ID: E58C21DD77B9B8E8
7 changed files with 58 additions and 32 deletions

View File

@ -1,7 +1,7 @@
import Foundation
/// A release is a representation of a downloadable update.
public struct Release: Codable {
public struct Release: Codable, Sendable {
/// The user-facing name of the release. Typically "Secretive 1.2.3"
public let name: String
@ -30,6 +30,9 @@ public struct Release: Codable {
}
// TODO: REMOVE WHEN(?) URL GAINS NATIVE CONFORMANCE
extension URL: @unchecked Sendable {}
extension Release: Identifiable {
public var id: String {

View File

@ -1,7 +1,7 @@
import Foundation
/// A representation of a Semantic Version.
public struct SemVer {
public struct SemVer: Sendable {
/// The SemVer broken into an array of integers.
let versionNumbers: [Int]

View File

@ -2,43 +2,55 @@ import Foundation
import Combine
/// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version.
public class Updater: ObservableObject, UpdaterProtocol {
public actor Updater: ObservableObject, UpdaterProtocol {
@Published public var update: Release?
@MainActor @Published public var update: Release?
public let testBuild: Bool
/// The current OS version.
private let osVersion: SemVer
/// The current version of the app that is running.
private let currentVersion: SemVer
/// The timer responsible for checking for updates regularly.
private var timer: Timer? = nil
/// Initializes an Updater.
/// - Parameters:
/// - checkOnLaunch: A boolean describing whether the Updater should check for available updates on launch.
/// - checkFrequency: The interval at which the Updater should check for updates. Subject to a tolerance of 1 hour.
/// - osVersion: The current OS version.
/// - currentVersion: The current version of the app that is running.
public init(checkOnLaunch: Bool, checkFrequency: TimeInterval = Measurement(value: 24, unit: UnitDuration.hours).converted(to: .seconds).value, osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")) {
public init(osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")) {
self.osVersion = osVersion
self.currentVersion = currentVersion
testBuild = currentVersion == SemVer("0.0.0")
if checkOnLaunch {
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet.
checkForUpdates()
}
let timer = Timer.scheduledTimer(withTimeInterval: checkFrequency, repeats: true) { _ in
self.checkForUpdates()
/// Begins checking for updates with the specified frequency.
/// - Parameter checkFrequency: The interval at which the Updater should check for updates. Subject to a tolerance of 1 hour.
public func beginChecking(checkFrequency: TimeInterval = Measurement(value: 24, unit: UnitDuration.hours).converted(to: .seconds).value) {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: checkFrequency, repeats: true) { _ in
Task {
await self.checkForUpdates()
}
timer.tolerance = 60*60
}
timer?.tolerance = 60*60
}
/// Ends checking for updates.
public func stopChecking() {
timer?.invalidate()
timer = nil
}
/// Manually trigger an update check.
public func checkForUpdates() {
URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in
guard let data = data else { return }
guard let releases = try? JSONDecoder().decode([Release].self, from: data) else { return }
public func checkForUpdates() async {
if #available(macOS 12.0, *) {
guard let (data, _) = try? await URLSession.shared.data(from: Constants.updateURL),
let releases = try? JSONDecoder().decode([Release].self, from: data) else { return }
self.evaluate(releases: releases)
}.resume()
} else {
// Fallback on earlier versions
}
}
/// Ignores a specified release. `update` will be nil if the user has ignored the latest available release.
@ -46,8 +58,8 @@ public class Updater: ObservableObject, UpdaterProtocol {
public func ignore(release: Release) {
guard !release.critical else { return }
defaults.set(true, forKey: release.name)
DispatchQueue.main.async {
self.update = nil
Task {
await setUpdate(update: update)
}
}
@ -67,12 +79,16 @@ extension Updater {
guard !release.prerelease else { return }
let latestVersion = SemVer(release.name)
if latestVersion > currentVersion {
DispatchQueue.main.async {
self.update = release
Task {
await setUpdate(update: update)
}
}
}
@MainActor private func setUpdate(update: Release?) {
self.update = update
}
/// Checks whether the user has ignored a release.
/// - Parameter release: The release to check.
/// - Returns: A boolean describing whether the user has ignored the release. Will always be false if the release is critical.

View File

@ -4,9 +4,9 @@ import Foundation
public protocol UpdaterProtocol: ObservableObject {
/// The latest update
var update: Release? { get }
@MainActor var update: Release? { get }
/// A boolean describing whether or not the current build of the app is a "test" build (ie, a debug build or otherwise special build)
var testBuild: Bool { get }
@MainActor var testBuild: Bool { get }
}

View File

@ -16,7 +16,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
list.add(store: SmartCard.Store())
return list
}()
private let updater = Updater(checkOnLaunch: false)
private let updater = Updater()
private let notifier = Notifier()
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
private lazy var agent: Agent = {
@ -38,10 +38,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true)
notifier.prompt()
updateSink = updater.$update.sink { update in
guard let update = update else { return }
self.notifier.notify(update: update, ignore: self.updater.ignore(release:))
}
// updateSink = updater.$update.sink { update in
// guard let update = update else { return }
// self.notifier.notify(update: update, ignore: self.updater.ignore(release:))
// }
}
}

View File

@ -16,6 +16,7 @@ struct Secretive: App {
}()
private let agentStatusChecker = AgentStatusChecker()
private let justUpdatedChecker = JustUpdatedChecker()
private let updater = Updater()
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
@State private var showingSetup = false
@ -25,11 +26,15 @@ struct Secretive: App {
WindowGroup {
ContentView<Updater, AgentStatusChecker>(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
.environmentObject(storeList)
.environmentObject(Updater(checkOnLaunch: hasRunSetup))
.environmentObject(updater)
.environmentObject(agentStatusChecker)
.onAppear {
if !hasRunSetup {
showingSetup = true
} else {
Task { [updater] in
await updater.checkForUpdates()
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in

View File

@ -18,7 +18,9 @@ struct UpdateDetailView<UpdaterType: Updater>: View {
HStack {
if !update.critical {
Button("Ignore") {
updater.ignore(release: update)
Task { [updater, update] in
await updater.ignore(release: update)
}
}
Spacer()
}