Swift 6 / Concurrency fixes (#578)

* Enable language mode

* WIP

* WIP

* Fix concurrency issues in SmartCardStore

* Switch to SMAppService

* Bump runners

* Base

* Finish Testing migration

* Tweak async for updater

* More

* Backport mutex

* Revert "Backport mutex"

This reverts commit 9b02afb20c.

* WIP

* Reenable

* Fix preview.

* Update package.

* Bump to latest public macOS and Xcode

* Bump back down to 6.1

* Update to Xcode 26.

* Fixed tests.

* More cleanup

* Env fixes

* var->let

* Cleanup

* Persist auth async

* Whitespace.

* Whitespace.

* Cleanup.

* Cleanup

* Redoing locks in actors bc of observable

* Actors.

* .

* Specify b5

* Update package to 6.2

* Fix disabled updater

* Remove preconcurrency warning

* Move updater init
This commit is contained in:
Max Goedjen
2025-08-17 12:38:18 -05:00
committed by GitHub
parent 5cc62b628a
commit 0e6b218f1f
59 changed files with 834 additions and 857 deletions

View File

@@ -5,7 +5,7 @@ import SecretKit
import SecretAgentKit
import Brief
class Notifier {
final class Notifier: Sendable {
private let notificationDelegate = NotificationDelegate()
@@ -29,12 +29,13 @@ class Notifier {
formatter.unitsStyle = .spellOut
formatter.allowedUnits = [.hour, .minute, .day]
var identifiers: [String: TimeInterval] = [:]
for duration in rawDurations {
let seconds = duration.converted(to: .seconds).value
guard let string = formatter.string(from: seconds)?.capitalized else { continue }
let identifier = Constants.persistAuthenticationCategoryIdentitifier.appending("\(seconds)")
let action = UNNotificationAction(identifier: identifier, title: string, options: [])
notificationDelegate.persistOptions[identifier] = seconds
identifiers[identifier] = seconds
allPersistenceActions.append(action)
}
@@ -45,9 +46,11 @@ class Notifier {
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory])
UNUserNotificationCenter.current().delegate = notificationDelegate
notificationDelegate.persistAuthentication = { secret, store, duration in
guard let duration = duration else { return }
try? store.persistAuthentication(secret: secret, forDuration: duration)
Task {
await notificationDelegate.state.setPersistenceState(options: identifiers) { secret, store, duration in
guard let duration = duration else { return }
try? await store.persistAuthentication(secret: secret, forDuration: duration)
}
}
}
@@ -57,9 +60,8 @@ class Notifier {
notificationCenter.requestAuthorization(options: .alert) { _, _ in }
}
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) {
notificationDelegate.pendingPersistableSecrets[secret.id.description] = secret
notificationDelegate.pendingPersistableStores[store.id.description] = store
func notify(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async {
await notificationDelegate.state.setPending(secret: secret, store: store)
let notificationCenter = UNUserNotificationCenter.current()
let notificationContent = UNMutableNotificationContent()
notificationContent.title = String(localized: "signed_notification_title_\(provenance.origin.displayName)")
@@ -67,19 +69,18 @@ class Notifier {
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
notificationContent.interruptionLevel = .timeSensitive
if secret.requiresAuthentication && store.existingPersistedAuthenticationContext(secret: secret) == nil {
if await store.existingPersistedAuthenticationContext(secret: secret) == nil && secret.requiresAuthentication {
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
}
if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
notificationContent.attachments = [attachment]
}
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
notificationCenter.add(request, withCompletionHandler: nil)
try? await notificationCenter.add(request)
}
func notify(update: Release, ignore: ((Release) -> Void)?) {
notificationDelegate.release = update
notificationDelegate.ignore = ignore
func notify(update: Release, ignore: (@Sendable (Release) async -> Void)?) async {
await notificationDelegate.state.prepareForNotification(release: update, ignoreAction: ignore)
let notificationCenter = UNUserNotificationCenter.current()
let notificationContent = UNMutableNotificationContent()
if update.critical {
@@ -92,18 +93,18 @@ class Notifier {
notificationContent.body = update.body
notificationContent.categoryIdentifier = update.critical ? Constants.criticalUpdateCategoryIdentitifier : Constants.updateCategoryIdentitifier
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
notificationCenter.add(request, withCompletionHandler: nil)
try? await notificationCenter.add(request)
}
}
extension Notifier: SigningWitness {
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws {
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
}
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws {
notify(accessTo: secret, from: store, by: provenance)
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) async throws {
await notify(accessTo: secret, from: store, by: provenance)
}
}
@@ -129,55 +130,91 @@ extension Notifier {
}
class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
fileprivate var release: Release?
fileprivate var ignore: ((Release) -> Void)?
fileprivate var persistAuthentication: ((AnySecret, AnySecretStore, TimeInterval?) -> Void)?
fileprivate var persistOptions: [String: TimeInterval] = [:]
fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:]
fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]
fileprivate actor State {
typealias PersistAction = (@Sendable (AnySecret, AnySecretStore, TimeInterval?) async -> Void)
typealias IgnoreAction = (@Sendable (Release) async -> Void)
fileprivate var release: Release?
fileprivate var ignoreAction: IgnoreAction?
fileprivate var persistAction: PersistAction?
fileprivate var persistOptions: [String: TimeInterval] = [:]
fileprivate var pendingPersistableStores: [String: AnySecretStore] = [:]
fileprivate var pendingPersistableSecrets: [String: AnySecret] = [:]
func setPending(secret: AnySecret, store: AnySecretStore) {
pendingPersistableSecrets[secret.id.description] = secret
pendingPersistableStores[store.id.description] = store
}
func retrievePending(secretID: String, storeID: String, optionID: String) -> (AnySecret, AnySecretStore, TimeInterval)? {
guard let secret = pendingPersistableSecrets[secretID],
let store = pendingPersistableStores[storeID],
let options = persistOptions[optionID] else {
return nil
}
pendingPersistableSecrets.removeValue(forKey: secretID)
return (secret, store, options)
}
func setPersistenceState(options: [String: TimeInterval], action: @escaping PersistAction) {
self.persistOptions = options
self.persistAction = action
}
func prepareForNotification(release: Release, ignoreAction: IgnoreAction?) {
self.release = release
self.ignoreAction = ignoreAction
}
}
fileprivate let state = State()
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
let category = response.notification.request.content.categoryIdentifier
switch category {
case Notifier.Constants.updateCategoryIdentitifier:
handleUpdateResponse(response: response)
await handleUpdateResponse(response: response)
case Notifier.Constants.persistAuthenticationCategoryIdentitifier:
handlePersistAuthenticationResponse(response: response)
await handlePersistAuthenticationResponse(response: response)
default:
break
}
completionHandler()
}
func handleUpdateResponse(response: UNNotificationResponse) {
guard let update = release else { return }
switch response.actionIdentifier {
func handleUpdateResponse(response: UNNotificationResponse) async {
let id = response.actionIdentifier
guard let update = await state.release else { return }
switch id {
case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier:
NSWorkspace.shared.open(update.html_url)
case Notifier.Constants.ignoreActionIdentitifier:
ignore?(update)
await state.ignoreAction?(update)
default:
fatalError()
}
}
func handlePersistAuthenticationResponse(response: UNNotificationResponse) {
guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String, let secret = pendingPersistableSecrets[secretID],
let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String, let store = pendingPersistableStores[storeID]
else { return }
pendingPersistableSecrets[secretID] = nil
persistAuthentication?(secret, store, persistOptions[response.actionIdentifier])
func handlePersistAuthenticationResponse(response: UNNotificationResponse) async {
guard let secretID = response.notification.request.content.userInfo[Notifier.Constants.persistSecretIDKey] as? String,
let storeID = response.notification.request.content.userInfo[Notifier.Constants.persistStoreIDKey] as? String else {
return
}
let optionID = response.actionIdentifier
guard let (secret, store, persistOptions) = await state.retrievePending(secretID: secretID, storeID: storeID, optionID: optionID) else { return }
await state.persistAction?(secret, store, persistOptions)
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.list, .banner])
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
[.list, .banner]
}
}