Log fatal crashes and present alert on next launch

This commit is contained in:
Angelo Stavrow 2022-06-18 08:53:25 -04:00
parent a43bd801a8
commit 5a1b400333
No known key found for this signature in database
GPG Key ID: 1A49C7064E060EEE
4 changed files with 72 additions and 8 deletions

View File

@ -13,6 +13,8 @@ enum WFDefaults {
static let automaticallyChecksForUpdates = "automaticallyChecksForUpdates" static let automaticallyChecksForUpdates = "automaticallyChecksForUpdates"
static let subscribeToBetaUpdates = "subscribeToBetaUpdates" static let subscribeToBetaUpdates = "subscribeToBetaUpdates"
#endif #endif
static let didHaveFatalError = "didHaveFatalError"
static let fatalErrorDescription = "fatalErrorDescription"
} }
extension UserDefaults { extension UserDefaults {

View File

@ -1,4 +1,5 @@
import CoreData import CoreData
import os
#if os(iOS) #if os(iOS)
import UIKit import UIKit
@ -21,9 +22,11 @@ final class LocalStorageManager {
func saveContext() { func saveContext() {
if container.viewContext.hasChanges { if container.viewContext.hasChanges {
do { do {
Self.logger.info("Saving context to local store started...")
try container.viewContext.save() try container.viewContext.save()
Self.logger.notice("Context saved to local store.")
} catch { } catch {
fatalError(LocalStoreError.couldNotSaveContext.localizedDescription) logCrashAndSetFlag(error: LocalStoreError.couldNotSaveContext)
} }
} }
} }
@ -33,8 +36,11 @@ final class LocalStorageManager {
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do { do {
Self.logger.info("Purging user collections from local store...")
try container.viewContext.executeAndMergeChanges(using: deleteRequest) try container.viewContext.executeAndMergeChanges(using: deleteRequest)
Self.logger.notice("User collections purged from local store.")
} catch { } catch {
Self.logger.error("\(LocalStoreError.couldNotPurgeCollections.localizedDescription)")
throw LocalStoreError.couldNotPurgeCollections throw LocalStoreError.couldNotPurgeCollections
} }
} }
@ -60,9 +66,11 @@ private extension LocalStorageManager {
} }
container.loadPersistentStores { _, error in container.loadPersistentStores { _, error in
Self.logger.info("Loading local store...")
if let error = error { 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) migrateStore(for: container)
container.viewContext.automaticallyMergesChangesFromParent = true container.viewContext.automaticallyMergesChangesFromParent = true
@ -83,21 +91,23 @@ private extension LocalStorageManager {
// Attempt to migrate the old store over to the shared store URL. // Attempt to migrate the old store over to the shared store URL.
do { do {
Self.logger.info("Migrating local store to shared store...")
try coordinator.migratePersistentStore(oldStore, try coordinator.migratePersistentStore(oldStore,
to: sharedStoreURL, to: sharedStoreURL,
options: nil, options: nil,
withType: NSSQLiteStoreType) withType: NSSQLiteStoreType)
Self.logger.notice("Migrated local store to shared store.")
} catch { } catch {
fatalError(LocalStoreError.couldNotMigrateStore(error.localizedDescription).localizedDescription) logCrashAndSetFlag(error: LocalStoreError.couldNotMigrateStore(error.localizedDescription))
} }
// Attempt to delete the old store. // Attempt to delete the old store.
do { do {
Self.logger.info("Deleting migrated local store...")
try FileManager.default.removeItem(at: oldStoreURL) try FileManager.default.removeItem(at: oldStoreURL)
Self.logger.notice("Deleted migrated local store.")
} catch { } catch {
fatalError( logCrashAndSetFlag(error: LocalStoreError.couldNotDeleteStoreAfterMigration(error.localizedDescription))
LocalStoreError.couldNotDeleteStoreAfterMigration(error.localizedDescription).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)
}
}

View File

@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import CoreData import CoreData
import os
class CollectionListModel: NSObject, ObservableObject { class CollectionListModel: NSObject, ObservableObject {
@Published var list: [WFACollection] = [] @Published var list: [WFACollection] = []
@ -16,11 +17,12 @@ class CollectionListModel: NSObject, ObservableObject {
collectionsController.delegate = self collectionsController.delegate = self
do { do {
Self.logger.info("Fetching collections from local store...")
try collectionsController.performFetch() try collectionsController.performFetch()
list = collectionsController.fetchedObjects ?? [] list = collectionsController.fetchedObjects ?? []
Self.logger.notice("Fetched collections from local store.")
} catch { } catch {
// FIXME: Errors cannot be thrown out of the CollectionListView property initializer logCrashAndSetFlag(error: LocalStoreError.couldNotFetchCollections)
fatalError(LocalStoreError.couldNotFetchCollections.localizedDescription)
} }
} }
} }
@ -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 { extension WFACollection {
static var collectionsFetchRequest: NSFetchRequest<WFACollection> { static var collectionsFetchRequest: NSFetchRequest<WFACollection> {
let request: NSFetchRequest<WFACollection> = WFACollection.createFetchRequest() let request: NSFetchRequest<WFACollection> = WFACollection.createFetchRequest()

View File

@ -30,6 +30,8 @@ struct WriteFreely_MultiPlatformApp: App {
@State private var selectedTab = 0 @State private var selectedTab = 0
#endif #endif
@State private var didCrash = UserDefaults.shared.bool(forKey: WFDefaults.didHaveFatalError)
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() 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() .withErrorHandling()
.environmentObject(model) .environmentObject(model)
.environment(\.managedObjectContext, LocalStorageManager.standard.container.viewContext) .environment(\.managedObjectContext, LocalStorageManager.standard.container.viewContext)