mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-07-01 17:53:36 +00:00
Compare commits
2 Commits
xcode_26_e
...
async
Author | SHA1 | Date | |
---|---|---|---|
2100803e0d | |||
e86b9d2465 |
@ -1,7 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// A release is a representation of a downloadable update.
|
/// 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"
|
/// The user-facing name of the release. Typically "Secretive 1.2.3"
|
||||||
public let name: String
|
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 {
|
extension Release: Identifiable {
|
||||||
|
|
||||||
public var id: String {
|
public var id: String {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// A representation of a Semantic Version.
|
/// A representation of a Semantic Version.
|
||||||
public struct SemVer {
|
public struct SemVer: Sendable {
|
||||||
|
|
||||||
/// The SemVer broken into an array of integers.
|
/// The SemVer broken into an array of integers.
|
||||||
let versionNumbers: [Int]
|
let versionNumbers: [Int]
|
||||||
|
@ -2,53 +2,59 @@ import Foundation
|
|||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
/// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version.
|
/// 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
|
public let testBuild: Bool
|
||||||
|
|
||||||
/// The current OS version.
|
/// The current OS version.
|
||||||
private let osVersion: SemVer
|
private let osVersion: SemVer
|
||||||
/// The current version of the app that is running.
|
/// The current version of the app that is running.
|
||||||
private let currentVersion: SemVer
|
private let currentVersion: SemVer
|
||||||
|
/// The timer responsible for checking for updates regularly.
|
||||||
|
private var timer: Timer? = nil
|
||||||
|
|
||||||
/// Initializes an Updater.
|
/// Initializes an Updater.
|
||||||
/// - Parameters:
|
/// - 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.
|
/// - osVersion: The current OS version.
|
||||||
/// - currentVersion: The current version of the app that is running.
|
/// - 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.osVersion = osVersion
|
||||||
self.currentVersion = currentVersion
|
self.currentVersion = currentVersion
|
||||||
testBuild = currentVersion == SemVer("0.0.0")
|
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
|
timer?.tolerance = 60*60
|
||||||
self.checkForUpdates()
|
}
|
||||||
}
|
|
||||||
timer.tolerance = 60*60
|
/// Ends checking for updates.
|
||||||
|
public func stopChecking() {
|
||||||
|
timer?.invalidate()
|
||||||
|
timer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manually trigger an update check.
|
/// Manually trigger an update check.
|
||||||
public func checkForUpdates() {
|
public func checkForUpdates() async {
|
||||||
URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in
|
guard let (data, _) = try? await URLSession.shared.data(from: Constants.updateURL),
|
||||||
guard let data = data else { return }
|
let releases = try? JSONDecoder().decode([Release].self, from: data) else { return }
|
||||||
guard let releases = try? JSONDecoder().decode([Release].self, from: data) else { return }
|
await evaluate(releases: releases)
|
||||||
self.evaluate(releases: releases)
|
|
||||||
}.resume()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ignores a specified release. `update` will be nil if the user has ignored the latest available release.
|
/// Ignores a specified release. `update` will be nil if the user has ignored the latest available release.
|
||||||
/// - Parameter release: The release to ignore.
|
/// - Parameter release: The release to ignore.
|
||||||
public func ignore(release: Release) {
|
public func ignore(release: Release) async {
|
||||||
guard !release.critical else { return }
|
guard !release.critical else { return }
|
||||||
defaults.set(true, forKey: release.name)
|
defaults.set(true, forKey: release.name)
|
||||||
DispatchQueue.main.async {
|
await setUpdate(update: update)
|
||||||
self.update = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -57,7 +63,7 @@ extension Updater {
|
|||||||
|
|
||||||
/// Evaluates the available downloadable releases, and selects the newest non-prerelease release that the user is able to run.
|
/// Evaluates the available downloadable releases, and selects the newest non-prerelease release that the user is able to run.
|
||||||
/// - Parameter releases: An array of ``Release`` objects.
|
/// - Parameter releases: An array of ``Release`` objects.
|
||||||
func evaluate(releases: [Release]) {
|
func evaluate(releases: [Release]) async {
|
||||||
guard let release = releases
|
guard let release = releases
|
||||||
.sorted()
|
.sorted()
|
||||||
.reversed()
|
.reversed()
|
||||||
@ -67,12 +73,14 @@ extension Updater {
|
|||||||
guard !release.prerelease else { return }
|
guard !release.prerelease else { return }
|
||||||
let latestVersion = SemVer(release.name)
|
let latestVersion = SemVer(release.name)
|
||||||
if latestVersion > currentVersion {
|
if latestVersion > currentVersion {
|
||||||
DispatchQueue.main.async {
|
await setUpdate(update: update)
|
||||||
self.update = release
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor private func setUpdate(update: Release?) {
|
||||||
|
self.update = update
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks whether the user has ignored a release.
|
/// Checks whether the user has ignored a release.
|
||||||
/// - Parameter release: The release to check.
|
/// - 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.
|
/// - Returns: A boolean describing whether the user has ignored the release. Will always be false if the release is critical.
|
||||||
@ -95,3 +103,21 @@ extension Updater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(macOS, deprecated: 12)
|
||||||
|
extension URLSession {
|
||||||
|
|
||||||
|
// Backport for macOS 11
|
||||||
|
func data(from url: URL) async throws -> (Data, URLResponse) {
|
||||||
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
|
URLSession.shared.dataTask(with: url) { data, response, error in
|
||||||
|
guard let data = data, let response = response else {
|
||||||
|
continuation.resume(throwing: error ?? NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, userInfo: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continuation.resume(returning: (data, response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -4,9 +4,9 @@ import Foundation
|
|||||||
public protocol UpdaterProtocol: ObservableObject {
|
public protocol UpdaterProtocol: ObservableObject {
|
||||||
|
|
||||||
/// The latest update
|
/// 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)
|
/// 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 }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
list.add(store: SmartCard.Store())
|
list.add(store: SmartCard.Store())
|
||||||
return list
|
return list
|
||||||
}()
|
}()
|
||||||
private let updater = Updater(checkOnLaunch: false)
|
private let updater = Updater()
|
||||||
private let notifier = Notifier()
|
private let notifier = Notifier()
|
||||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
||||||
private lazy var agent: Agent = {
|
private lazy var agent: Agent = {
|
||||||
@ -38,10 +38,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true)
|
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true)
|
||||||
notifier.prompt()
|
notifier.prompt()
|
||||||
updateSink = updater.$update.sink { update in
|
// updateSink = updater.$update.sink { update in
|
||||||
guard let update = update else { return }
|
// guard let update = update else { return }
|
||||||
self.notifier.notify(update: update, ignore: self.updater.ignore(release:))
|
// self.notifier.notify(update: update, ignore: self.updater.ignore(release:))
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ struct Secretive: App {
|
|||||||
}()
|
}()
|
||||||
private let agentStatusChecker = AgentStatusChecker()
|
private let agentStatusChecker = AgentStatusChecker()
|
||||||
private let justUpdatedChecker = JustUpdatedChecker()
|
private let justUpdatedChecker = JustUpdatedChecker()
|
||||||
|
private let updater = Updater()
|
||||||
|
|
||||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
||||||
@State private var showingSetup = false
|
@State private var showingSetup = false
|
||||||
@ -25,11 +26,15 @@ struct Secretive: App {
|
|||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView<Updater, AgentStatusChecker>(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
|
ContentView<Updater, AgentStatusChecker>(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
|
||||||
.environmentObject(storeList)
|
.environmentObject(storeList)
|
||||||
.environmentObject(Updater(checkOnLaunch: hasRunSetup))
|
.environmentObject(updater)
|
||||||
.environmentObject(agentStatusChecker)
|
.environmentObject(agentStatusChecker)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if !hasRunSetup {
|
if !hasRunSetup {
|
||||||
showingSetup = true
|
showingSetup = true
|
||||||
|
} else {
|
||||||
|
Task { [updater] in
|
||||||
|
await updater.checkForUpdates()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
||||||
|
@ -18,7 +18,9 @@ struct UpdateDetailView<UpdaterType: Updater>: View {
|
|||||||
HStack {
|
HStack {
|
||||||
if !update.critical {
|
if !update.critical {
|
||||||
Button("Ignore") {
|
Button("Ignore") {
|
||||||
updater.ignore(release: update)
|
Task { [updater, update] in
|
||||||
|
await updater.ignore(release: update)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user