From 5a1b40033345d216669c6f4b58ba4c28bc90b461 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Sat, 18 Jun 2022 08:53:25 -0400 Subject: [PATCH] Log fatal crashes and present alert on next launch --- .../Extensions/UserDefaults+Extensions.swift | 2 + Shared/LocalStorageManager.swift | 39 ++++++++++++++++--- .../PostCollection/CollectionListModel.swift | 21 +++++++++- Shared/WriteFreely_MultiPlatformApp.swift | 18 +++++++++ 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/Shared/Extensions/UserDefaults+Extensions.swift b/Shared/Extensions/UserDefaults+Extensions.swift index b010fdc..dcf0267 100644 --- a/Shared/Extensions/UserDefaults+Extensions.swift +++ b/Shared/Extensions/UserDefaults+Extensions.swift @@ -13,6 +13,8 @@ enum WFDefaults { static let automaticallyChecksForUpdates = "automaticallyChecksForUpdates" static let subscribeToBetaUpdates = "subscribeToBetaUpdates" #endif + static let didHaveFatalError = "didHaveFatalError" + static let fatalErrorDescription = "fatalErrorDescription" } extension UserDefaults { diff --git a/Shared/LocalStorageManager.swift b/Shared/LocalStorageManager.swift index ae074b4..759bbf0 100644 --- a/Shared/LocalStorageManager.swift +++ b/Shared/LocalStorageManager.swift @@ -1,4 +1,5 @@ import CoreData +import os #if os(iOS) import UIKit @@ -21,9 +22,11 @@ final class LocalStorageManager { func saveContext() { if container.viewContext.hasChanges { do { + Self.logger.info("Saving context to local store started...") try container.viewContext.save() + Self.logger.notice("Context saved to local store.") } catch { - fatalError(LocalStoreError.couldNotSaveContext.localizedDescription) + logCrashAndSetFlag(error: LocalStoreError.couldNotSaveContext) } } } @@ -33,8 +36,11 @@ final class LocalStorageManager { let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) do { + Self.logger.info("Purging user collections from local store...") try container.viewContext.executeAndMergeChanges(using: deleteRequest) + Self.logger.notice("User collections purged from local store.") } catch { + Self.logger.error("\(LocalStoreError.couldNotPurgeCollections.localizedDescription)") throw LocalStoreError.couldNotPurgeCollections } } @@ -60,9 +66,11 @@ private extension LocalStorageManager { } container.loadPersistentStores { _, error in + Self.logger.info("Loading local store...") if let error = error { - fatalError(LocalStoreError.couldNotLoadStore(error.localizedDescription).localizedDescription) + self.logCrashAndSetFlag(error: LocalStoreError.couldNotLoadStore(error.localizedDescription)) } + Self.logger.notice("Loaded local store.") } migrateStore(for: container) container.viewContext.automaticallyMergesChangesFromParent = true @@ -83,21 +91,23 @@ private extension LocalStorageManager { // Attempt to migrate the old store over to the shared store URL. do { + Self.logger.info("Migrating local store to shared store...") try coordinator.migratePersistentStore(oldStore, to: sharedStoreURL, options: nil, withType: NSSQLiteStoreType) + Self.logger.notice("Migrated local store to shared store.") } catch { - fatalError(LocalStoreError.couldNotMigrateStore(error.localizedDescription).localizedDescription) + logCrashAndSetFlag(error: LocalStoreError.couldNotMigrateStore(error.localizedDescription)) } // Attempt to delete the old store. do { + Self.logger.info("Deleting migrated local store...") try FileManager.default.removeItem(at: oldStoreURL) + Self.logger.notice("Deleted migrated local store.") } catch { - fatalError( - LocalStoreError.couldNotDeleteStoreAfterMigration(error.localizedDescription).localizedDescription - ) + logCrashAndSetFlag(error: LocalStoreError.couldNotDeleteStoreAfterMigration(error.localizedDescription)) } } @@ -123,3 +133,20 @@ private extension LocalStorageManager { } } + +private extension LocalStorageManager { + + private static let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: String(describing: LocalStorageManager.self) + ) + + private func logCrashAndSetFlag(error: Error) { + let errorDescription = error.localizedDescription + UserDefaults.shared.set(true, forKey: WFDefaults.didHaveFatalError) + UserDefaults.shared.set(errorDescription, forKey: WFDefaults.fatalErrorDescription) + Self.logger.critical("\(errorDescription)") + fatalError(errorDescription) + } + +} diff --git a/Shared/PostCollection/CollectionListModel.swift b/Shared/PostCollection/CollectionListModel.swift index b2ac884..d1cd2bb 100644 --- a/Shared/PostCollection/CollectionListModel.swift +++ b/Shared/PostCollection/CollectionListModel.swift @@ -1,5 +1,6 @@ import SwiftUI import CoreData +import os class CollectionListModel: NSObject, ObservableObject { @Published var list: [WFACollection] = [] @@ -16,11 +17,12 @@ class CollectionListModel: NSObject, ObservableObject { collectionsController.delegate = self do { + Self.logger.info("Fetching collections from local store...") try collectionsController.performFetch() list = collectionsController.fetchedObjects ?? [] + Self.logger.notice("Fetched collections from local store.") } catch { - // FIXME: Errors cannot be thrown out of the CollectionListView property initializer - fatalError(LocalStoreError.couldNotFetchCollections.localizedDescription) + logCrashAndSetFlag(error: LocalStoreError.couldNotFetchCollections) } } } @@ -32,6 +34,21 @@ extension CollectionListModel: NSFetchedResultsControllerDelegate { } } +extension CollectionListModel { + private static let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: String(describing: CollectionListModel.self) + ) + + private func logCrashAndSetFlag(error: Error) { + let errorDescription = error.localizedDescription + UserDefaults.shared.set(true, forKey: WFDefaults.didHaveFatalError) + UserDefaults.shared.set(errorDescription, forKey: WFDefaults.fatalErrorDescription) + Self.logger.critical("\(errorDescription)") + fatalError(errorDescription) + } +} + extension WFACollection { static var collectionsFetchRequest: NSFetchRequest { let request: NSFetchRequest = WFACollection.createFetchRequest() diff --git a/Shared/WriteFreely_MultiPlatformApp.swift b/Shared/WriteFreely_MultiPlatformApp.swift index d17c965..feedb83 100644 --- a/Shared/WriteFreely_MultiPlatformApp.swift +++ b/Shared/WriteFreely_MultiPlatformApp.swift @@ -30,6 +30,8 @@ struct WriteFreely_MultiPlatformApp: App { @State private var selectedTab = 0 #endif + @State private var didCrash = UserDefaults.shared.bool(forKey: WFDefaults.didHaveFatalError) + var body: some Scene { WindowGroup { ContentView() @@ -48,6 +50,22 @@ struct WriteFreely_MultiPlatformApp: App { } } }) + .alert(isPresented: $didCrash) { + // TODO: - Confirm copy for this alert + Alert( + title: Text("Crash Detected"), + message: Text( + UserDefaults.shared.object(forKey: WFDefaults.fatalErrorDescription) as? String ?? + "Something went horribly wrong!" + ), + dismissButton: .default( + Text("Dismiss"), action: { + UserDefaults.shared.set(false, forKey: WFDefaults.didHaveFatalError) + UserDefaults.shared.removeObject(forKey: WFDefaults.fatalErrorDescription) + } + ) + ) + } .withErrorHandling() .environmentObject(model) .environment(\.managedObjectContext, LocalStorageManager.standard.container.viewContext)