From e86b9d24657a7a6a1cb34bc3e11fa4749db19939 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Tue, 18 Jan 2022 16:38:59 -0500 Subject: [PATCH] Move some updater stuff to being an actor --- Sources/Packages/Sources/Brief/Release.swift | 5 +- Sources/Packages/Sources/Brief/SemVer.swift | 2 +- Sources/Packages/Sources/Brief/Updater.swift | 58 ++++++++++++------- .../Sources/Brief/UpdaterProtocol.swift | 4 +- Sources/SecretAgent/AppDelegate.swift | 10 ++-- Sources/Secretive/App.swift | 7 ++- Sources/Secretive/Views/UpdateView.swift | 4 +- 7 files changed, 58 insertions(+), 32 deletions(-) diff --git a/Sources/Packages/Sources/Brief/Release.swift b/Sources/Packages/Sources/Brief/Release.swift index 847dffe..29c55da 100644 --- a/Sources/Packages/Sources/Brief/Release.swift +++ b/Sources/Packages/Sources/Brief/Release.swift @@ -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 { diff --git a/Sources/Packages/Sources/Brief/SemVer.swift b/Sources/Packages/Sources/Brief/SemVer.swift index 8308521..8df8c4a 100644 --- a/Sources/Packages/Sources/Brief/SemVer.swift +++ b/Sources/Packages/Sources/Brief/SemVer.swift @@ -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] diff --git a/Sources/Packages/Sources/Brief/Updater.swift b/Sources/Packages/Sources/Brief/Updater.swift index c71e538..a810274 100644 --- a/Sources/Packages/Sources/Brief/Updater.swift +++ b/Sources/Packages/Sources/Brief/Updater.swift @@ -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() + } + + /// 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() + } } - let timer = Timer.scheduledTimer(withTimeInterval: checkFrequency, repeats: true) { _ in - 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. diff --git a/Sources/Packages/Sources/Brief/UpdaterProtocol.swift b/Sources/Packages/Sources/Brief/UpdaterProtocol.swift index 1930a0f..ed246a0 100644 --- a/Sources/Packages/Sources/Brief/UpdaterProtocol.swift +++ b/Sources/Packages/Sources/Brief/UpdaterProtocol.swift @@ -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 } } diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index 2c221dd..2b8b112 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -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:)) +// } } } diff --git a/Sources/Secretive/App.swift b/Sources/Secretive/App.swift index 52faacc..74e8f6d 100644 --- a/Sources/Secretive/App.swift +++ b/Sources/Secretive/App.swift @@ -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(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 diff --git a/Sources/Secretive/Views/UpdateView.swift b/Sources/Secretive/Views/UpdateView.swift index afe620e..4373d85 100644 --- a/Sources/Secretive/Views/UpdateView.swift +++ b/Sources/Secretive/Views/UpdateView.swift @@ -18,7 +18,9 @@ struct UpdateDetailView: View { HStack { if !update.critical { Button("Ignore") { - updater.ignore(release: update) + Task { [updater, update] in + await updater.ignore(release: update) + } } Spacer() }