mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-09-20 03:10:57 +00:00
Redoing locks in actors bc of observable
This commit is contained in:
parent
c22e7d9ed8
commit
c227c90fd4
@ -27,16 +27,13 @@ let package = Package(
|
||||
.library(
|
||||
name: "Brief",
|
||||
targets: ["Brief"]),
|
||||
.library(
|
||||
name: "Common",
|
||||
targets: ["Common"]),
|
||||
],
|
||||
dependencies: [
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "SecretKit",
|
||||
dependencies: ["Common"],
|
||||
dependencies: [],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
.testTarget(
|
||||
@ -46,17 +43,17 @@ let package = Package(
|
||||
),
|
||||
.target(
|
||||
name: "SecureEnclaveSecretKit",
|
||||
dependencies: ["Common", "SecretKit"],
|
||||
dependencies: ["SecretKit"],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
.target(
|
||||
name: "SmartCardSecretKit",
|
||||
dependencies: ["Common", "SecretKit"],
|
||||
dependencies: ["SecretKit"],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
.target(
|
||||
name: "SecretAgentKit",
|
||||
dependencies: ["Common", "SecretKit", "SecretAgentKitHeaders"],
|
||||
dependencies: ["SecretKit", "SecretAgentKitHeaders"],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
.systemLibrary(
|
||||
@ -68,18 +65,13 @@ let package = Package(
|
||||
,
|
||||
.target(
|
||||
name: "Brief",
|
||||
dependencies: ["Common"],
|
||||
dependencies: [],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
.testTarget(
|
||||
name: "BriefTests",
|
||||
dependencies: ["Brief"]
|
||||
),
|
||||
.target(
|
||||
name: "Common",
|
||||
dependencies: [],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -1,15 +1,18 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import os
|
||||
import Common
|
||||
|
||||
/// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version.
|
||||
@Observable public final class Updater: UpdaterProtocol, Sendable {
|
||||
|
||||
public var update: Release? {
|
||||
_update.lockedValue
|
||||
private let state = State()
|
||||
@MainActor @Observable public final class State {
|
||||
var update: Release? = nil
|
||||
nonisolated init() {}
|
||||
}
|
||||
private let _update: OSAllocatedUnfairLock<Release?> = .init(uncheckedState: nil)
|
||||
public var update: Release? {
|
||||
state.update
|
||||
}
|
||||
|
||||
public let testBuild: Bool
|
||||
|
||||
/// The current OS version.
|
||||
@ -23,7 +26,12 @@ import Common
|
||||
/// - 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(
|
||||
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")
|
||||
) {
|
||||
self.osVersion = osVersion
|
||||
self.currentVersion = currentVersion
|
||||
testBuild = currentVersion == SemVer("0.0.0")
|
||||
@ -54,7 +62,7 @@ import Common
|
||||
guard !release.critical else { return }
|
||||
defaults.set(true, forKey: release.name)
|
||||
await MainActor.run {
|
||||
_update.lockedValue = nil
|
||||
state.update = nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +83,8 @@ extension Updater {
|
||||
let latestVersion = SemVer(release.name)
|
||||
if latestVersion > currentVersion {
|
||||
await MainActor.run {
|
||||
_update.lockedValue = release
|
||||
print("SET \(release)")
|
||||
state.update = release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import os
|
||||
public protocol UpdaterProtocol: Observable, Sendable {
|
||||
|
||||
/// 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 }
|
||||
|
||||
|
@ -1,14 +0,0 @@
|
||||
import os
|
||||
|
||||
public extension OSAllocatedUnfairLock where State: Sendable {
|
||||
|
||||
var lockedValue: State {
|
||||
get {
|
||||
withLock { $0 }
|
||||
}
|
||||
nonmutating set {
|
||||
withLock { $0 = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -22,7 +22,9 @@ public final class Agent: Sendable {
|
||||
logger.debug("Agent is running")
|
||||
self.storeList = storeList
|
||||
self.witness = witness
|
||||
certificateHandler.reloadCertificates(for: storeList.allSecrets)
|
||||
Task { @MainActor in
|
||||
certificateHandler.reloadCertificates(for: storeList.allSecrets)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -60,7 +62,7 @@ extension Agent {
|
||||
switch requestType {
|
||||
case .requestIdentities:
|
||||
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
|
||||
response.append(identities())
|
||||
response.append(await identities())
|
||||
logger.debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)")
|
||||
case .signRequest:
|
||||
let provenance = requestTracer.provenance(from: reader)
|
||||
@ -83,8 +85,8 @@ extension Agent {
|
||||
|
||||
/// Lists the identities available for signing operations
|
||||
/// - Returns: An OpenSSH formatted Data payload listing the identities available for signing operations.
|
||||
func identities() -> Data {
|
||||
let secrets = storeList.allSecrets
|
||||
func identities() async -> Data {
|
||||
let secrets = await storeList.allSecrets
|
||||
certificateHandler.reloadCertificates(for: secrets)
|
||||
var count = secrets.count
|
||||
var keyData = Data()
|
||||
@ -123,7 +125,7 @@ extension Agent {
|
||||
hash = payloadHash
|
||||
}
|
||||
|
||||
guard let (store, secret) = secret(matching: hash) else {
|
||||
guard let (store, secret) = await secret(matching: hash) else {
|
||||
logger.debug("Agent did not have a key matching \(hash as NSData)")
|
||||
throw AgentError.noMatchingKey
|
||||
}
|
||||
@ -189,7 +191,7 @@ extension Agent {
|
||||
|
||||
/// Gives any store with no loaded secrets a chance to reload.
|
||||
func reloadSecretsIfNeccessary() async {
|
||||
for store in storeList.stores {
|
||||
for store in await storeList.stores {
|
||||
if store.secrets.isEmpty {
|
||||
logger.debug("Store \(store.name, privacy: .public) has no loaded secrets. Reloading.")
|
||||
await store.reloadSecrets()
|
||||
@ -200,8 +202,8 @@ extension Agent {
|
||||
/// Finds a ``Secret`` matching a specified hash whos signature was requested.
|
||||
/// - Parameter hash: The hash to match against.
|
||||
/// - Returns: A ``Secret`` and the ``SecretStore`` containing it, if a match is found.
|
||||
func secret(matching hash: Data) -> (AnySecretStore, AnySecret)? {
|
||||
storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in
|
||||
func secret(matching hash: Data) async -> (AnySecretStore, AnySecret)? {
|
||||
await storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in
|
||||
let allMatching = store.secrets.filter { secret in
|
||||
hash == writer.data(secret: secret)
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ public final class OpenSSHCertificateHandler: Sendable {
|
||||
/// - Parameter secret: The secret to check for a certificate.
|
||||
/// - Returns: A boolean describing whether or not the certificate handler has a certifiicate associated with a given secret
|
||||
public func hasCertificate<SecretType: Secret>(for secret: SecretType) -> Bool {
|
||||
keyBlobsAndNames.lockedValue[AnySecret(secret)] != nil
|
||||
keyBlobsAndNames.withLock { $0[AnySecret(secret)] != nil }
|
||||
}
|
||||
|
||||
|
||||
@ -64,7 +64,7 @@ public final class OpenSSHCertificateHandler: Sendable {
|
||||
/// - Parameter secret: The secret to search for a certificate with
|
||||
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.
|
||||
public func keyBlobAndName<SecretType: Secret>(for secret: SecretType) throws -> (Data, Data)? {
|
||||
keyBlobsAndNames.lockedValue[AnySecret(secret)]
|
||||
keyBlobsAndNames.withLock { $0[AnySecret(secret)] }
|
||||
}
|
||||
|
||||
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
|
||||
|
@ -1,50 +1,39 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import os
|
||||
import Common
|
||||
|
||||
/// A "Store Store," which holds a list of type-erased stores.
|
||||
@Observable public final class SecretStoreList: Sendable {
|
||||
@Observable @MainActor public final class SecretStoreList: Sendable {
|
||||
|
||||
/// The Stores managed by the SecretStoreList.
|
||||
public var stores: [AnySecretStore] {
|
||||
__stores.lockedValue
|
||||
}
|
||||
private let __stores: OSAllocatedUnfairLock<[AnySecretStore]> = .init(uncheckedState: [])
|
||||
|
||||
public var stores: [AnySecretStore] = []
|
||||
/// A modifiable store, if one is available.
|
||||
public var modifiableStore: AnySecretStoreModifiable? {
|
||||
__modifiableStore.withLock { $0 }
|
||||
}
|
||||
private let __modifiableStore: OSAllocatedUnfairLock<AnySecretStoreModifiable?> = .init(uncheckedState: nil)
|
||||
public var modifiableStore: AnySecretStoreModifiable? = nil
|
||||
|
||||
/// Initializes a SecretStoreList.
|
||||
public init() {
|
||||
public nonisolated init() {
|
||||
}
|
||||
|
||||
/// Adds a non-type-erased SecretStore to the list.
|
||||
public func add<SecretStoreType: SecretStore>(store: SecretStoreType) {
|
||||
__stores.withLock {
|
||||
$0.append(AnySecretStore(store))
|
||||
}
|
||||
stores.append(AnySecretStore(store))
|
||||
}
|
||||
|
||||
/// Adds a non-type-erased modifiable SecretStore.
|
||||
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
|
||||
let modifiable = AnySecretStoreModifiable(modifiable: store)
|
||||
__modifiableStore.lockedValue = modifiable
|
||||
__stores.withLock {
|
||||
$0.append(modifiable)
|
||||
if modifiableStore == nil {
|
||||
modifiableStore = modifiable
|
||||
}
|
||||
stores.append(modifiable)
|
||||
}
|
||||
|
||||
/// A boolean describing whether there are any Stores available.
|
||||
public var anyAvailable: Bool {
|
||||
__stores.lockedValue.contains(where: \.isAvailable)
|
||||
stores.contains(where: \.isAvailable)
|
||||
}
|
||||
|
||||
public var allSecrets: [AnySecret] {
|
||||
__stores.lockedValue.flatMap(\.secrets)
|
||||
stores.flatMap(\.secrets)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,7 +5,20 @@ import CryptoKit
|
||||
@preconcurrency import LocalAuthentication
|
||||
import SecretKit
|
||||
import os
|
||||
import Common
|
||||
|
||||
public extension OSAllocatedUnfairLock where State: Sendable {
|
||||
|
||||
var lockedValue: State {
|
||||
get {
|
||||
withLock { $0 }
|
||||
}
|
||||
nonmutating set {
|
||||
withLock { $0 = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
extension SecureEnclave {
|
||||
|
||||
|
@ -11,13 +11,13 @@ import Observation
|
||||
@main
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
private let storeList: SecretStoreList = {
|
||||
@MainActor private let storeList: SecretStoreList = {
|
||||
let list = SecretStoreList()
|
||||
list.add(store: SecureEnclave.Store())
|
||||
list.add(store: SmartCard.Store())
|
||||
return list
|
||||
}()
|
||||
private let updater = Updater(checkOnLaunch: false)
|
||||
private let updater = Updater(checkOnLaunch: true)
|
||||
private let notifier = Notifier()
|
||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
||||
private lazy var agent: Agent = {
|
||||
@ -44,15 +44,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
}
|
||||
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||
notifier.prompt()
|
||||
_ = withObservationTracking {
|
||||
updater.update
|
||||
} onChange: { [updater, notifier] in
|
||||
notifier.notify(update: updater.update!) { release in
|
||||
Task {
|
||||
await updater.ignore(release: release)
|
||||
}
|
||||
}
|
||||
}
|
||||
// _ = withObservationTracking {
|
||||
// updater.update
|
||||
// } onChange: { [updater, notifier] in
|
||||
// notifier.notify(update: updater.update!) { release in
|
||||
// Task {
|
||||
// await updater.ignore(release: release)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,14 +6,19 @@ import SmartCardSecretKit
|
||||
import Brief
|
||||
|
||||
extension EnvironmentValues {
|
||||
@Entry var secretStoreList: SecretStoreList = {
|
||||
private static let _secretStoreList: SecretStoreList = {
|
||||
let list = SecretStoreList()
|
||||
list.add(store: SecureEnclave.Store())
|
||||
list.add(store: SmartCard.Store())
|
||||
Task { @MainActor in
|
||||
list.add(store: SecureEnclave.Store())
|
||||
list.add(store: SmartCard.Store())
|
||||
}
|
||||
return list
|
||||
}()
|
||||
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = AgentStatusChecker()
|
||||
@Entry var updater: any UpdaterProtocol = Updater(checkOnLaunch: false)
|
||||
@Entry var secretStoreList = _secretStoreList
|
||||
private static let _agentStatusChecker = AgentStatusChecker()
|
||||
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
|
||||
private static let _updater: any UpdaterProtocol = Updater(checkOnLaunch: true)
|
||||
@Entry var updater: any UpdaterProtocol = _updater
|
||||
}
|
||||
|
||||
@main
|
||||
@ -29,7 +34,7 @@ struct Secretive: App {
|
||||
WindowGroup {
|
||||
ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
|
||||
// This one is explicitly injected via environment to support hasRunSetup.
|
||||
.environment(Updater(checkOnLaunch: hasRunSetup))
|
||||
// .environment(Updater(checkOnLaunch: hasRunSetup))
|
||||
.onAppear {
|
||||
if !hasRunSetup {
|
||||
showingSetup = true
|
||||
|
@ -4,18 +4,20 @@ import AppKit
|
||||
import SecretKit
|
||||
import Observation
|
||||
|
||||
protocol AgentStatusCheckerProtocol: Observable {
|
||||
@MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable {
|
||||
var running: Bool { get }
|
||||
var developmentBuild: Bool { get }
|
||||
func check()
|
||||
}
|
||||
|
||||
@Observable class AgentStatusChecker: AgentStatusCheckerProtocol {
|
||||
@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol {
|
||||
|
||||
var running: Bool = false
|
||||
|
||||
init() {
|
||||
check()
|
||||
nonisolated init() {
|
||||
Task { @MainActor in
|
||||
check()
|
||||
}
|
||||
}
|
||||
|
||||
func check() {
|
||||
|
@ -2511,6 +2511,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"No Update: %@" : {
|
||||
|
||||
},
|
||||
"no_secure_storage_description" : {
|
||||
"localizations" : {
|
||||
|
@ -104,7 +104,7 @@ extension Preview {
|
||||
|
||||
extension Preview {
|
||||
|
||||
static func storeList(stores: [Store] = [], modifiableStores: [StoreModifiable] = []) -> SecretStoreList {
|
||||
@MainActor static func storeList(stores: [Store] = [], modifiableStores: [StoreModifiable] = []) -> SecretStoreList {
|
||||
let list = SecretStoreList()
|
||||
for store in stores {
|
||||
list.add(store: store)
|
||||
|
@ -91,6 +91,8 @@ extension ContentView {
|
||||
.popover(item: $selectedUpdate, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { update in
|
||||
UpdateDetailView(update: update)
|
||||
}
|
||||
} else {
|
||||
Text("No Update: \(updater.update as Any)")
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user