2
2
mirror of https://github.com/mastodon/mastodon-ios synced 2025-04-11 22:58:02 +02:00

Increasing thread safety for authentications.

Attempting to fix multiple crashes associated with the call to persist() triggered by the authentications setter called in restore().
This commit is contained in:
shannon 2024-10-30 12:26:51 -04:00
parent a9e088a9c7
commit 20f7d761bd
9 changed files with 35 additions and 22 deletions

View File

@ -17,7 +17,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
let appContext = AppContext()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
AuthenticationServiceProvider.shared.restore()
AuthenticationServiceProvider.shared.prepareForUse()
AppSecret.default.register()

View File

@ -11,7 +11,7 @@ import MastodonCore
class IntentHandler: INExtension {
override func handler(for intent: INIntent) -> Any {
AuthenticationServiceProvider.shared.restore()
AuthenticationServiceProvider.shared.prepareForUse()
switch intent {
case is SendPostIntent:

View File

@ -19,7 +19,7 @@ public class AuthenticationServiceProvider: ObservableObject {
@Published public var authentications: [MastodonAuthentication] = [] {
didSet {
persist() // todo: Is this too heavy and too often here???
persist(authentications)
}
}
@ -53,6 +53,7 @@ public class AuthenticationServiceProvider: ObservableObject {
return self
}
@MainActor
func delete(authentication: MastodonAuthentication) throws {
try Self.keychain.remove(authentication.persistenceIdentifier)
authentications.removeAll(where: { $0 == authentication })
@ -81,14 +82,22 @@ public extension AuthenticationServiceProvider {
func authenticationSortedByActivation() -> [MastodonAuthentication] { // fixme: why do we need this?
return authentications.sorted(by: { $0.activedAt > $1.activedAt })
}
func prepareForUse() {
if authentications.isEmpty {
restoreFromKeychain()
}
}
func restore() {
authentications = Self.keychain.allKeys().compactMap {
guard
let encoded = Self.keychain[$0],
let data = Data(base64Encoded: encoded)
else { return nil }
return try? JSONDecoder().decode(MastodonAuthentication.self, from: data)
private func restoreFromKeychain() {
DispatchQueue.main.async {
self.authentications = Self.keychain.allKeys().compactMap {
guard
let encoded = Self.keychain[$0],
let data = Data(base64Encoded: encoded)
else { return nil }
return try? JSONDecoder().decode(MastodonAuthentication.self, from: data)
}
}
}
@ -117,8 +126,10 @@ public extension AuthenticationServiceProvider {
logger.log(level: .default, "All account authentications were successful.")
}
self.authentications = migratedAuthentications
userDefaults.didMigrateAuthentications = true
DispatchQueue.main.async {
self.authentications = migratedAuthentications
self.userDefaults.didMigrateAuthentications = true
}
} catch {
userDefaults.didMigrateAuthentications = false
logger.log(level: .error, "Could not migrate legacy authentications")
@ -148,9 +159,11 @@ public extension AuthenticationServiceProvider {
// MARK: - Private
private extension AuthenticationServiceProvider {
func persist() {
for authentication in authentications {
Self.keychain[authentication.persistenceIdentifier] = try? JSONEncoder().encode(authentication).base64EncodedString()
func persist(_ authentications: [MastodonAuthentication]) {
DispatchQueue.main.async {
for authentication in authentications {
Self.keychain[authentication.persistenceIdentifier] = try? JSONEncoder().encode(authentication).base64EncodedString()
}
}
}
}

View File

@ -137,13 +137,13 @@ extension AuthenticationService {
}
public func signOutMastodonUser(authentication: MastodonAuthentication) async throws {
try AuthenticationServiceProvider.shared.delete(authentication: authentication)
try await AuthenticationServiceProvider.shared.delete(authentication: authentication)
_ = try await apiService?.cancelSubscription(domain: authentication.domain, authorization: authentication.authorization)
}
public func signOutMastodonUser(authenticationBox: MastodonAuthenticationBox) async throws {
do {
try AuthenticationServiceProvider.shared.delete(authentication: authenticationBox.authentication)
try await AuthenticationServiceProvider.shared.delete(authentication: authenticationBox.authentication)
} catch {
assertionFailure("Failed to delete Authentication: \(error)")
}

View File

@ -160,7 +160,7 @@ extension ShareViewController {
extension ShareViewController {
private func setupAuthContext() throws -> AuthContext? {
AuthenticationServiceProvider.shared.restore()
AuthenticationServiceProvider.shared.prepareForUse()
let authentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first
let authContext = authentication.flatMap { AuthContext(authentication: $0) }

View File

@ -73,7 +73,7 @@ private extension FollowersCountWidgetProvider {
func loadCurrentEntry(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersCountEntry) -> Void) {
Task {
AuthenticationServiceProvider.shared.restore()
AuthenticationServiceProvider.shared.prepareForUse()
guard
let authBox = WidgetExtension.appContext

View File

@ -25,7 +25,7 @@ struct HashtagWidgetProvider: IntentTimelineProvider {
extension HashtagWidgetProvider {
func loadMostRecentHashtag(for configuration: HashtagIntent, in context: Context, completion: @escaping (HashtagWidgetTimelineEntry) -> Void ) {
AuthenticationServiceProvider.shared.restore()
AuthenticationServiceProvider.shared.prepareForUse()
guard
let authBox = WidgetExtension.appContext

View File

@ -80,7 +80,7 @@ private extension LatestFollowersWidgetProvider {
func loadCurrentEntry(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (LatestFollowersEntry) -> Void) {
Task { @MainActor in
AuthenticationServiceProvider.shared.restore()
AuthenticationServiceProvider.shared.prepareForUse()
guard
let authBox = WidgetExtension.appContext

View File

@ -73,7 +73,7 @@ private extension MultiFollowersCountWidgetProvider {
func loadCurrentEntry(for configuration: MultiFollowersCountIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> Void) {
Task {
AuthenticationServiceProvider.shared.restore()
AuthenticationServiceProvider.shared.prepareForUse()
guard
let authBox = WidgetExtension.appContext