diff --git a/NotificationServiceExtension/NotificationService.swift b/NotificationServiceExtension/NotificationService.swift index 36acd6f620..1f26be381f 100644 --- a/NotificationServiceExtension/NotificationService.swift +++ b/NotificationServiceExtension/NotificationService.swift @@ -102,7 +102,12 @@ class NotificationService: UNNotificationServiceExtension { SSKEnvironment.shared.callMessageHandlerRef = NoopCallMessageHandler() SSKEnvironment.shared.notificationsManagerRef = NotificationPresenter() }, - migrationCompletion: { [weak self] in + migrationCompletion: { [weak self] error in + if let error = error { + // TODO: Maybe notify that you should open the main app. + owsFailDebug("Error \(error)") + return + } self?.versionMigrationsDidComplete() } ) diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 8f2a6dc84a..2bec233726 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -55,7 +55,8 @@ typedef NS_ENUM(NSUInteger, LaunchFailure) { LaunchFailure_None, LaunchFailure_CouldNotLoadDatabase, LaunchFailure_UnknownDatabaseVersion, - LaunchFailure_CouldNotRestoreTransferredData + LaunchFailure_CouldNotRestoreTransferredData, + LaunchFailure_DatabaseUnrecoverablyCorrupted }; NSString *NSStringForLaunchFailure(LaunchFailure launchFailure); @@ -70,6 +71,8 @@ NSString *NSStringForLaunchFailure(LaunchFailure launchFailure) return @"LaunchFailure_UnknownDatabaseVersion"; case LaunchFailure_CouldNotRestoreTransferredData: return @"LaunchFailure_CouldNotRestoreTransferredData"; + case LaunchFailure_DatabaseUnrecoverablyCorrupted: + return @"LaunchFailure_DatabaseUnrecoverablyCorrupted"; } } @@ -189,6 +192,8 @@ void uncaughtExceptionHandler(NSException *exception) // Prevent: // * Users with an unknown GRDB schema revert to using an earlier GRDB schema. launchFailure = LaunchFailure_UnknownDatabaseVersion; + } else if ([SSKPreferences hasGrdbDatabaseCorruption]) { + launchFailure = LaunchFailure_DatabaseUnrecoverablyCorrupted; } if (launchFailure != LaunchFailure_None) { OWSLogInfo(@"application: didFinishLaunchingWithOptions failed."); @@ -211,10 +216,15 @@ void uncaughtExceptionHandler(NSException *exception) [AppEnvironment.shared setup]; [SignalApp.shared setup]; } - migrationCompletion:^{ + migrationCompletion:^(NSError *_Nullable error) { OWSAssertIsOnMainThread(); - [self versionMigrationsDidComplete]; + if (error != nil) { + OWSFailDebug(@"Error: %@", error); + [self showUIForLaunchFailure:LaunchFailure_DatabaseUnrecoverablyCorrupted]; + } else { + [self versionMigrationsDidComplete]; + } }]; [UIUtil setupSignalAppearence]; @@ -309,8 +319,10 @@ void uncaughtExceptionHandler(NSException *exception) // We perform a subset of the [application:didFinishLaunchingWithOptions:]. [AppVersion shared]; - self.window = [OWSWindow new]; - CurrentAppContext().mainWindow = self.window; + if (self.window == nil) { + self.window = [OWSWindow new]; + CurrentAppContext().mainWindow = self.window; + } // Show the launch screen UIViewController *viewController = [[UIStoryboard storyboardWithName:@"Launch Screen" @@ -323,6 +335,8 @@ void uncaughtExceptionHandler(NSException *exception) NSString *alertMessage = NSLocalizedString(@"APP_LAUNCH_FAILURE_ALERT_MESSAGE", @"Message for the 'app launch failed' alert."); switch (launchFailure) { + case LaunchFailure_DatabaseUnrecoverablyCorrupted: + // Fallthrough case LaunchFailure_CouldNotLoadDatabase: alertTitle = NSLocalizedString(@"APP_LAUNCH_FAILURE_COULD_NOT_LOAD_DATABASE", @"Error indicating that the app could not launch because the database could not be loaded."); @@ -528,8 +542,6 @@ void uncaughtExceptionHandler(NSException *exception) return; } - [SignalApp.shared ensureRootViewController:launchStartedAt]; - AppReadinessRunNowOrWhenAppDidBecomeReadySync(^{ [self handleActivation]; }); // Clear all notifications whenever we become active. @@ -1060,6 +1072,11 @@ void uncaughtExceptionHandler(NSException *exception) { OWSAssertIsOnMainThread(); + // If launch failed, the app will never be ready. + if (self.didAppLaunchFail) { + return; + } + // App isn't ready until storage is ready AND all version migrations are complete. if (!self.areVersionMigrationsComplete) { return; diff --git a/Signal/src/util/Device Transfer/DeviceTransferService+MultipeerDelegates.swift b/Signal/src/util/Device Transfer/DeviceTransferService+MultipeerDelegates.swift index fcd2dabd9b..288bb238db 100644 --- a/Signal/src/util/Device Transfer/DeviceTransferService+MultipeerDelegates.swift +++ b/Signal/src/util/Device Transfer/DeviceTransferService+MultipeerDelegates.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // import Foundation @@ -148,7 +148,7 @@ extension DeviceTransferService: MCSessionDelegate { // Try and restore the received data. If for some reason the app exits // or crashes at this point, we will retry the restore when the app next // launches. - guard restoreTransferredData(hotSwapDatabase: true) else { + guard restoreTransferredData(hotswapDatabase: true) else { owsFail("Restore failed. Crashing, will try again on next launch.") } diff --git a/Signal/src/util/Device Transfer/DeviceTransferService+Restore.swift b/Signal/src/util/Device Transfer/DeviceTransferService+Restore.swift index bfc5b24e23..82de3ac128 100644 --- a/Signal/src/util/Device Transfer/DeviceTransferService+Restore.swift +++ b/Signal/src/util/Device Transfer/DeviceTransferService+Restore.swift @@ -20,6 +20,11 @@ extension DeviceTransferService { get { CurrentAppContext().appUserDefaults().bool(forKey: DeviceTransferService.pendingWasTransferedClearKey) } set { CurrentAppContext().appUserDefaults().set(newValue, forKey: DeviceTransferService.pendingWasTransferedClearKey) } } + private static let pendingPromotionFromHotSwapToPrimaryDatabaseKey = "DeviceTransferPendingPromotionFromHotSwapToPrimaryDatabase" + var pendingPromotionFromHotSwapToPrimaryDatabase: Bool { + get { CurrentAppContext().appUserDefaults().bool(forKey: DeviceTransferService.pendingPromotionFromHotSwapToPrimaryDatabaseKey) } + set { CurrentAppContext().appUserDefaults().set(newValue, forKey: DeviceTransferService.pendingPromotionFromHotSwapToPrimaryDatabaseKey) } + } func verifyTransferCompletedSuccessfully(receivedFileIds: [String], skippedFileIds: [String]) -> Bool { guard let manifest = readManifestFromTransferDirectory() else { @@ -91,7 +96,7 @@ extension DeviceTransferService { return true } - func restoreTransferredData(hotSwapDatabase: Bool) -> Bool { + func restoreTransferredData(hotswapDatabase: Bool) -> Bool { Logger.info("Attempting to restore transferred data.") guard hasPendingRestore else { @@ -145,39 +150,74 @@ extension DeviceTransferService { fileURLWithPath: file.identifier, relativeTo: DeviceTransferService.pendingTransferFilesDirectory ).path - let newFilePath = URL( - fileURLWithPath: file.relativePath, - relativeTo: DeviceTransferService.appSharedDataDirectory - ).path + + // We could be receiving a database in any of the directory modes, + // so we force the restore path to be the "primary" database since + // that is generally what we desire. If we're hotswapping, this + // path will be later overriden with the hotswap path. + let newFilePath: String + if DeviceTransferService.databaseIdentifier == file.identifier { + newFilePath = GRDBDatabaseStorageAdapter.databaseFileUrl( + baseDir: SDSDatabaseStorage.baseDir, + directoryMode: .primary + ).path + } else if DeviceTransferService.databaseWALIdentifier == file.identifier { + newFilePath = GRDBDatabaseStorageAdapter.databaseWalUrl( + baseDir: SDSDatabaseStorage.baseDir, + directoryMode: .primary + ).path + } else { + newFilePath = URL( + fileURLWithPath: file.relativePath, + relativeTo: DeviceTransferService.appSharedDataDirectory + ).path + } + + // If we're hot swapping the database, we move the database + // files to a special hotswap directory, since the primary + // database is already open. Trying to overwrite the file + // in situ can result in database corruption if something + // tries to perform a write. + var hotswapFilePath: String? + if DeviceTransferService.databaseIdentifier == file.identifier { + hotswapFilePath = GRDBDatabaseStorageAdapter.databaseFileUrl( + baseDir: SDSDatabaseStorage.baseDir, + directoryMode: .hotswap + ).path + } else if DeviceTransferService.databaseWALIdentifier == file.identifier { + hotswapFilePath = GRDBDatabaseStorageAdapter.databaseWalUrl( + baseDir: SDSDatabaseStorage.baseDir, + directoryMode: .hotswap + ).path + } let fileIsAwaitingRestoration = OWSFileSystem.fileOrFolderExists(atPath: pendingFilePath) - let fileWasAlreadyRestored = OWSFileSystem.fileOrFolderExists(atPath: newFilePath) + let fileWasAlreadyRestoredToHotSwapPath: Bool = { + guard let hotswapFilePath = hotswapFilePath else { return false } + return OWSFileSystem.fileOrFolderExists(atPath: hotswapFilePath) + }() + let fileWasAlreadyRestored = fileWasAlreadyRestoredToHotSwapPath || OWSFileSystem.fileOrFolderExists(atPath: newFilePath) if fileIsAwaitingRestoration { - guard OWSFileSystem.deleteFileIfExists(newFilePath) else { - owsFailDebug("Failed to delete existing file \(file.identifier).") - return false - } + let restorationPath: String = { + if hotswapDatabase, let hotswapFilePath = hotswapFilePath { return hotswapFilePath } + return newFilePath + }() - let pathComponents = file.relativePath.components(separatedBy: "/") - var path = "" - for component in pathComponents where !component.isEmpty { - guard component != pathComponents.last else { break } - path += component + "/" - OWSFileSystem.ensureDirectoryExists( - URL( - fileURLWithPath: path, - relativeTo: DeviceTransferService.appSharedDataDirectory - ).path - ) - } - - guard OWSFileSystem.moveFilePath(pendingFilePath, toFilePath: newFilePath) else { - owsFailDebug("Failed to restore \(file.identifier)") + guard move(pendingFilePath: pendingFilePath, to: restorationPath) else { + owsFailDebug("Failed to move file \(file.identifier)") return false } } else if fileWasAlreadyRestored { - Logger.info("Skipping restoration of file that was already restored: \(file.identifier)") + if !hotswapDatabase, fileWasAlreadyRestoredToHotSwapPath, let hotswapFilePath = hotswapFilePath { + Logger.info("No longer hot swapping, promoting hotswap database to primary database: \(file.identifier)") + guard move(pendingFilePath: hotswapFilePath, to: newFilePath) else { + owsFailDebug("Failed to promote hotswap database \(file.identifier)") + return false + } + } else { + Logger.info("Skipping restoration of file that was already restored: \(file.identifier)") + } } else if [ DeviceTransferService.databaseIdentifier, DeviceTransferService.databaseWALIdentifier @@ -196,13 +236,14 @@ extension DeviceTransferService { } pendingWasTransferredClear = true + pendingPromotionFromHotSwapToPrimaryDatabase = hotswapDatabase hasBeenRestored = true resetTransferDirectory() - if hotSwapDatabase { + if hotswapDatabase { DispatchMainThreadSafe { - self.databaseStorage.reload() + self.databaseStorage.reload(directoryMode: .hotswap) self.tsAccountManager.wasTransferred = false self.pendingWasTransferredClear = false self.tsAccountManager.isTransferInProgress = false @@ -230,6 +271,68 @@ extension DeviceTransferService { hasPendingRestore = false } + private func move(pendingFilePath: String, to newFilePath: String) -> Bool { + guard OWSFileSystem.deleteFileIfExists(newFilePath) else { + owsFailDebug("Failed to delete existing file.") + return false + } + + let relativeNewPath = newFilePath.replacingOccurrences( + of: DeviceTransferService.appSharedDataDirectory.path, + with: "" + ) + + let pathComponents = relativeNewPath.components(separatedBy: "/") + var path = "" + for component in pathComponents where !component.isEmpty { + guard component != pathComponents.last else { break } + path += component + "/" + OWSFileSystem.ensureDirectoryExists( + URL( + fileURLWithPath: path, + relativeTo: DeviceTransferService.appSharedDataDirectory + ).path + ) + } + + guard OWSFileSystem.moveFilePath(pendingFilePath, toFilePath: newFilePath) else { + owsFailDebug("Failed to restore file.") + return false + } + + return true + } + + private func promoteTransferDatabaseToPrimaryDatabase() -> Bool { + Logger.info("Promoting the hotswap database to the primary database") + + let primaryDatabaseDirectoryPath = GRDBDatabaseStorageAdapter.databaseDirUrl( + baseDir: SDSDatabaseStorage.baseDir, + directoryMode: .primary + ).path + let hotswapDatabaseDirectoryPath = GRDBDatabaseStorageAdapter.databaseDirUrl( + baseDir: SDSDatabaseStorage.baseDir, + directoryMode: .hotswap + ).path + + if OWSFileSystem.fileOrFolderExists(atPath: hotswapDatabaseDirectoryPath) { + guard move(pendingFilePath: hotswapDatabaseDirectoryPath, to: primaryDatabaseDirectoryPath) else { + owsFailDebug("Failed to promote hotswap database to primary database.") + return false + } + } else { + guard OWSFileSystem.fileOrFolderExists(atPath: primaryDatabaseDirectoryPath) else { + owsFailDebug("Missing primary and hotswap database directories.") + return false + } + Logger.info("Missing hotswap database, we may have previously restored. Assuming primary database is correct.") + } + + pendingPromotionFromHotSwapToPrimaryDatabase = false + + return true + } + @objc func launchCleanup() -> Bool { Logger.info("hasBeenRestored: \(hasBeenRestored)") @@ -244,7 +347,9 @@ extension DeviceTransferService { } if hasPendingRestore { - return restoreTransferredData(hotSwapDatabase: false) + return restoreTransferredData(hotswapDatabase: false) + } else if pendingPromotionFromHotSwapToPrimaryDatabase { + return promoteTransferDatabaseToPrimaryDatabase() } else { resetTransferDirectory() return true diff --git a/SignalMessaging/environment/AppSetup.h b/SignalMessaging/environment/AppSetup.h index d4f3f489a2..7ad24b1041 100644 --- a/SignalMessaging/environment/AppSetup.h +++ b/SignalMessaging/environment/AppSetup.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @@ -8,7 +8,7 @@ NS_ASSUME_NONNULL_BEGIN @interface AppSetup : NSObject + (void)setupEnvironmentWithAppSpecificSingletonBlock:(dispatch_block_t)appSpecificSingletonBlock - migrationCompletion:(dispatch_block_t)migrationCompletion; + migrationCompletion:(void (^)(NSError *_Nullable error))migrationCompletion; @end diff --git a/SignalMessaging/environment/AppSetup.m b/SignalMessaging/environment/AppSetup.m index 1e00784cba..967d7d2d14 100644 --- a/SignalMessaging/environment/AppSetup.m +++ b/SignalMessaging/environment/AppSetup.m @@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN @implementation AppSetup + (void)setupEnvironmentWithAppSpecificSingletonBlock:(dispatch_block_t)appSpecificSingletonBlock - migrationCompletion:(dispatch_block_t)migrationCompletion + migrationCompletion:(void (^)(NSError *_Nullable error))migrationCompletion { OWSAssertDebug(appSpecificSingletonBlock); OWSAssertDebug(migrationCompletion); @@ -197,7 +197,9 @@ NS_ASSUME_NONNULL_BEGIN NSError *_Nullable error; [databaseStorage.grdbStorage syncTruncatingCheckpointAndReturnError:&error]; if (error != nil) { - OWSFailDebug(@"error: %@", error); + OWSFailDebug(@"Failed to truncate database: %@", error); + + dispatch_async(dispatch_get_main_queue(), ^{ migrationCompletion(error); }); } } @@ -213,7 +215,7 @@ NS_ASSUME_NONNULL_BEGIN if (StorageCoordinator.dataStoreForUI == DataStoreGrdb) { [SSKEnvironment.shared warmCaches]; } - migrationCompletion(); + migrationCompletion(nil); OWSAssertDebug(backgroundTask); backgroundTask = nil; diff --git a/SignalMessaging/utils/OWSOrphanDataCleaner.m b/SignalMessaging/utils/OWSOrphanDataCleaner.m index 3d929e2ff7..2def3c0d8b 100644 --- a/SignalMessaging/utils/OWSOrphanDataCleaner.m +++ b/SignalMessaging/utils/OWSOrphanDataCleaner.m @@ -313,17 +313,29 @@ typedef void (^OrphanDataBlock)(OWSOrphanData *); // This should be redundant, but this will future-proof us against // ever accidentally removing the GRDB databases during // orphan clean up. - NSString *grdbDirectoryPath = [SDSDatabaseStorage grdbDatabaseDirUrl].path; + NSString *grdbPrimaryDirectoryPath = + [GRDBDatabaseStorageAdapter databaseDirUrlWithBaseDir:SDSDatabaseStorage.baseDir + directoryMode:DirectoryModePrimary] + .path; + NSString *grdbHotswapDirectoryPath = + [GRDBDatabaseStorageAdapter databaseDirUrlWithBaseDir:SDSDatabaseStorage.baseDir + directoryMode:DirectoryModeHotswap] + .path; + NSMutableSet *databaseFilePaths = [NSMutableSet new]; for (NSString *filePath in allOnDiskFilePaths) { - if ([filePath hasPrefix:grdbDirectoryPath]) { + if ([filePath hasPrefix:grdbPrimaryDirectoryPath]) { OWSLogInfo(@"Protecting database file: %@", filePath); [databaseFilePaths addObject:filePath]; + } else if ([filePath hasPrefix:grdbHotswapDirectoryPath]) { + OWSLogInfo(@"Protecting database hotswap file: %@", filePath); + [databaseFilePaths addObject:filePath]; } } [allOnDiskFilePaths minusSet:databaseFilePaths]; - OWSLogVerbose( - @"grdbDirectoryPath: %@ (%d)", grdbDirectoryPath, [OWSFileSystem fileOrFolderExistsAtPath:grdbDirectoryPath]); + OWSLogVerbose(@"grdbDirectoryPath: %@ (%d)", + grdbPrimaryDirectoryPath, + [OWSFileSystem fileOrFolderExistsAtPath:grdbPrimaryDirectoryPath]); OWSLogVerbose(@"databaseFilePaths: %lu", (unsigned long)databaseFilePaths.count); OWSLogVerbose(@"allOnDiskFilePaths: %lu", (unsigned long)allOnDiskFilePaths.count); diff --git a/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift b/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift index 6b9ecfefb5..f51c8fc771 100644 --- a/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift +++ b/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift @@ -11,16 +11,36 @@ public class GRDBDatabaseStorageAdapter: NSObject { // 256 bit key + 128 bit salt public static let kSQLCipherKeySpecLength: UInt = 48 - static func databaseDirUrl(baseDir: URL) -> URL { - return baseDir.appendingPathComponent("grdb", isDirectory: true) + @objc + public enum DirectoryMode: Int { + case primary + case hotswap + + var folderName: String { + switch self { + case .primary: return "grdb" + case .hotswap: return "grdb-hotswap" + } + } } - static func databaseFileUrl(baseDir: URL) -> URL { - let databaseDir = databaseDirUrl(baseDir: baseDir) + @objc + public static func databaseDirUrl(baseDir: URL, directoryMode: DirectoryMode = .primary) -> URL { + return baseDir.appendingPathComponent(directoryMode.folderName, isDirectory: true) + } + + public static func databaseFileUrl(baseDir: URL, directoryMode: DirectoryMode = .primary) -> URL { + let databaseDir = databaseDirUrl(baseDir: baseDir, directoryMode: directoryMode) OWSFileSystem.ensureDirectoryExists(databaseDir.path) return databaseDir.appendingPathComponent("signal.sqlite", isDirectory: false) } + public static func databaseWalUrl(baseDir: URL, directoryMode: DirectoryMode = .primary) -> URL { + let databaseDir = databaseDirUrl(baseDir: baseDir, directoryMode: directoryMode) + OWSFileSystem.ensureDirectoryExists(databaseDir.path) + return databaseDir.appendingPathComponent("signal.sqlite-wal", isDirectory: false) + } + private let databaseUrl: URL private let storage: GRDBStorage @@ -29,8 +49,8 @@ public class GRDBDatabaseStorageAdapter: NSObject { return storage.pool } - init(baseDir: URL) { - databaseUrl = GRDBDatabaseStorageAdapter.databaseFileUrl(baseDir: baseDir) + init(baseDir: URL, directoryMode: DirectoryMode = .primary) { + databaseUrl = GRDBDatabaseStorageAdapter.databaseFileUrl(baseDir: baseDir, directoryMode: directoryMode) do { // Crash if keychain is inaccessible. @@ -235,6 +255,19 @@ public class GRDBDatabaseStorageAdapter: NSObject { owsFailDebug("Could not clear keychain: \(error)") } } + + static func prepareDatabase(db: Database, keyspec: GRDBKeySpecSource, name: String? = nil) throws { + let prefix: String + if let name = name, !name.isEmpty { + prefix = name + "." + } else { + prefix = "" + } + + let keyspec = try keyspec.fetchString() + try db.execute(sql: "PRAGMA \(prefix)key = \"\(keyspec)\"") + try db.execute(sql: "PRAGMA \(prefix)cipher_plaintext_header_size = 32") + } } // MARK: - @@ -511,10 +544,8 @@ private struct GRDBStorage { return true } }) - configuration.prepareDatabase = { (db: Database) in - let keyspec = try keyspec.fetchString() - try db.execute(sql: "PRAGMA key = \"\(keyspec)\"") - try db.execute(sql: "PRAGMA cipher_plaintext_header_size = 32") + configuration.prepareDatabase = { db in + try GRDBDatabaseStorageAdapter.prepareDatabase(db: db, keyspec: keyspec) } configuration.defaultTransactionKind = .immediate configuration.allowsUnsafeTransactions = true diff --git a/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift b/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift index 64240ff33c..12cca4aaca 100644 --- a/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift +++ b/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift @@ -70,19 +70,22 @@ public class SDSDatabaseStorage: SDSTransactable { NotificationCenter.default.removeObserver(self) } - private class func baseDir() -> URL { - return URL(fileURLWithPath: CurrentAppContext().appDatabaseBaseDirectoryPath(), - isDirectory: true) + @objc + public class var baseDir: URL { + return URL( + fileURLWithPath: CurrentAppContext().appDatabaseBaseDirectoryPath(), + isDirectory: true + ) } @objc public static var grdbDatabaseDirUrl: URL { - return GRDBDatabaseStorageAdapter.databaseDirUrl(baseDir: baseDir()) + return GRDBDatabaseStorageAdapter.databaseDirUrl(baseDir: baseDir) } @objc public static var grdbDatabaseFileUrl: URL { - return GRDBDatabaseStorageAdapter.databaseFileUrl(baseDir: baseDir()) + return GRDBDatabaseStorageAdapter.databaseFileUrl(baseDir: baseDir) } @objc @@ -103,36 +106,43 @@ public class SDSDatabaseStorage: SDSTransactable { Logger.info("didPerformIncrementalMigrations: \(didPerformIncrementalMigrations)") if didPerformIncrementalMigrations { - let benchSteps = BenchSteps() - - // There seems to be a rare issue where at least one reader or writer - // (e.g. SQLite connection) in the GRDB pool ends up "stale" after - // a schema migration and does not reflect the migrations. - grdbStorage.pool.releaseMemory() - weak var weakPool = grdbStorage.pool - weak var weakGrdbStorage = grdbStorage - owsAssertDebug(weakPool != nil) - owsAssertDebug(weakGrdbStorage != nil) - _grdbStorage = createGrdbStorage() - - DispatchQueue.main.async { - // We want to make sure all db connections from the old adapter/pool are closed. - // - // We only reach this point by a predictable code path; the autoreleasepool - // should be drained by this point. - owsAssertDebug(weakPool == nil) - owsAssertDebug(weakGrdbStorage == nil) - - benchSteps.step("New GRDB adapter.") - - completion() - } + reopenGRDBStorage(completion: completion) } else { DispatchQueue.main.async(execute: completion) } } - public func reload() { + public func reopenGRDBStorage( + directoryMode: GRDBDatabaseStorageAdapter.DirectoryMode = .primary, + completion: @escaping () -> Void = {} + ) { + let benchSteps = BenchSteps() + + // There seems to be a rare issue where at least one reader or writer + // (e.g. SQLite connection) in the GRDB pool ends up "stale" after + // a schema migration and does not reflect the migrations. + grdbStorage.pool.releaseMemory() + weak var weakPool = grdbStorage.pool + weak var weakGrdbStorage = grdbStorage + owsAssertDebug(weakPool != nil) + owsAssertDebug(weakGrdbStorage != nil) + _grdbStorage = createGrdbStorage(directoryMode: directoryMode) + + DispatchQueue.main.async { + // We want to make sure all db connections from the old adapter/pool are closed. + // + // We only reach this point by a predictable code path; the autoreleasepool + // should be drained by this point. + owsAssertDebug(weakPool == nil) + owsAssertDebug(weakGrdbStorage == nil) + + benchSteps.step("New GRDB adapter.") + + completion() + } + } + + public func reload(directoryMode: GRDBDatabaseStorageAdapter.DirectoryMode = .primary) { AssertIsOnMainThread() assert(storageCoordinatorState == .GRDB) @@ -140,37 +150,36 @@ public class SDSDatabaseStorage: SDSTransactable { let wasRegistered = TSAccountManager.shared.isRegistered - let grdbStorage = createGrdbStorage() - _grdbStorage = grdbStorage + reopenGRDBStorage(directoryMode: directoryMode) { + _ = GRDBSchemaMigrator().runSchemaMigrations() + self.grdbStorage.forceUpdateSnapshot() - GRDBSchemaMigrator().runSchemaMigrations() - grdbStorage.forceUpdateSnapshot() + // We need to do this _before_ warmCaches(). + NotificationCenter.default.post(name: Self.storageDidReload, object: nil, userInfo: nil) - // We need to do this _before_ warmCaches(). - NotificationCenter.default.post(name: Self.storageDidReload, object: nil, userInfo: nil) + SSKEnvironment.shared.warmCaches() - SSKEnvironment.shared.warmCaches() - - if wasRegistered != TSAccountManager.shared.isRegistered { - NotificationCenter.default.post(name: .registrationStateDidChange, object: nil, userInfo: nil) + if wasRegistered != TSAccountManager.shared.isRegistered { + NotificationCenter.default.post(name: .registrationStateDidChange, object: nil, userInfo: nil) + } } } - func createGrdbStorage() -> GRDBDatabaseStorageAdapter { + func createGrdbStorage(directoryMode: GRDBDatabaseStorageAdapter.DirectoryMode = .primary) -> GRDBDatabaseStorageAdapter { return Bench(title: "Creating GRDB storage") { - return GRDBDatabaseStorageAdapter(baseDir: type(of: self).baseDir()) + return GRDBDatabaseStorageAdapter(baseDir: type(of: self).baseDir, directoryMode: directoryMode) } } @objc public func deleteGrdbFiles() { - GRDBDatabaseStorageAdapter.removeAllFiles(baseDir: type(of: self).baseDir()) + GRDBDatabaseStorageAdapter.removeAllFiles(baseDir: type(of: self).baseDir) } @objc public func resetAllStorage() { YDBStorage.deleteYDBStorage() - GRDBDatabaseStorageAdapter.resetAllStorage(baseDir: type(of: self).baseDir()) + GRDBDatabaseStorageAdapter.resetAllStorage(baseDir: type(of: self).baseDir) } // MARK: - Observation diff --git a/SignalServiceKit/src/Storage/Database/SDSKeyValueStore.swift b/SignalServiceKit/src/Storage/Database/SDSKeyValueStore.swift index 7f01c7d147..343dfcaeea 100644 --- a/SignalServiceKit/src/Storage/Database/SDSKeyValueStore.swift +++ b/SignalServiceKit/src/Storage/Database/SDSKeyValueStore.swift @@ -644,7 +644,20 @@ public class SDSKeyValueStore: NSObject { } // TODO: We could use setArgumentsWithValidation for more safety. statement.unsafeSetArguments(statementArguments) - try statement.execute() + + do { + try statement.execute() + } catch { + // If the attempt to write to GRDB flagged that the database was + // corrupt, in addition to crashing we flag this so that we can + // attempt to perform recovery. + if let error = error as? DatabaseError, error.resultCode == .SQLITE_CORRUPT { + SSKPreferences.setHasGrdbDatabaseCorruption(true) + owsFail("Error: \(error)") + } else { + throw error + } + } } private func allKeys(grdbTransaction: GRDBReadTransaction) -> [String] { diff --git a/SignalServiceKit/src/Storage/Database/SDSRecord.swift b/SignalServiceKit/src/Storage/Database/SDSRecord.swift index d668bff540..ff2824148c 100644 --- a/SignalServiceKit/src/Storage/Database/SDSRecord.swift +++ b/SignalServiceKit/src/Storage/Database/SDSRecord.swift @@ -83,6 +83,13 @@ public extension SDSRecord { statement.unsafeSetArguments(arguments) try statement.execute() } catch { + // If the attempt to write to GRDB flagged that the database was + // corrupt, in addition to crashing we flag this so that we can + // attempt to perform recovery. + if let error = error as? DatabaseError, error.resultCode == .SQLITE_CORRUPT { + SSKPreferences.setHasGrdbDatabaseCorruption(true) + } + owsFail("Write failed: \(error.grdbErrorForLogging)") } } diff --git a/SignalServiceKit/src/Storage/Database/SDSTransaction.swift b/SignalServiceKit/src/Storage/Database/SDSTransaction.swift index a78c731eaf..045969058b 100644 --- a/SignalServiceKit/src/Storage/Database/SDSTransaction.swift +++ b/SignalServiceKit/src/Storage/Database/SDSTransaction.swift @@ -266,6 +266,13 @@ public extension GRDBWriteTransaction { statement.unsafeSetArguments(arguments) try statement.execute() } catch { + // If the attempt to write to GRDB flagged that the database was + // corrupt, in addition to crashing we flag this so that we can + // attempt to perform recovery. + if let error = error as? DatabaseError, error.resultCode == .SQLITE_CORRUPT { + SSKPreferences.setHasGrdbDatabaseCorruption(true) + } + owsFail("Error: \(error)") } } @@ -280,6 +287,13 @@ public extension GRDBWriteTransaction { statement.unsafeSetArguments(arguments) try statement.execute() } catch { + // If the attempt to write to GRDB flagged that the database was + // corrupt, in addition to crashing we flag this so that we can + // attempt to perform recovery. + if let error = error as? DatabaseError, error.resultCode == .SQLITE_CORRUPT { + SSKPreferences.setHasGrdbDatabaseCorruption(true) + } + owsFail("Error: \(error)") } } diff --git a/SignalServiceKit/src/Util/AppVersion.m b/SignalServiceKit/src/Util/AppVersion.m index 7c416ea67e..0309f46356 100755 --- a/SignalServiceKit/src/Util/AppVersion.m +++ b/SignalServiceKit/src/Util/AppVersion.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // #import "AppVersion.h" diff --git a/SignalServiceKit/src/Util/SSKPreferences.swift b/SignalServiceKit/src/Util/SSKPreferences.swift index 17c7f368b9..920e379c93 100644 --- a/SignalServiceKit/src/Util/SSKPreferences.swift +++ b/SignalServiceKit/src/Util/SSKPreferences.swift @@ -265,4 +265,23 @@ public class SSKPreferences: NSObject { public static func setShouldKeepMutedChatsArchived(_ newValue: Bool, transaction: SDSAnyWriteTransaction) { store.setBool(newValue, key: shouldKeepMutedChatsArchivedKey, transaction: transaction) } + + private static let hasGrdbDatabaseCorruptionKey = "hasGrdbDatabaseCorruption" + @objc + public static func hasGrdbDatabaseCorruption() -> Bool { + let appUserDefaults = CurrentAppContext().appUserDefaults() + guard let preference = appUserDefaults.object(forKey: hasGrdbDatabaseCorruptionKey) as? NSNumber else { + return false + } + return preference.boolValue + } + + @objc + public static func setHasGrdbDatabaseCorruption(_ value: Bool) { + if value { Logger.warn("Flagging GRDB database as corrupted.") } + + let appUserDefaults = CurrentAppContext().appUserDefaults() + appUserDefaults.set(value, forKey: hasGrdbDatabaseCorruptionKey) + appUserDefaults.synchronize() + } } diff --git a/SignalShareExtension/ShareViewController.swift b/SignalShareExtension/ShareViewController.swift index c15990a3d1..cb0c7a4b7c 100644 --- a/SignalShareExtension/ShareViewController.swift +++ b/SignalShareExtension/ShareViewController.swift @@ -62,15 +62,21 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed AppSetup.setupEnvironment(appSpecificSingletonBlock: { SSKEnvironment.shared.callMessageHandlerRef = NoopCallMessageHandler() SSKEnvironment.shared.notificationsManagerRef = NoopNotificationsManager() - }, - migrationCompletion: { [weak self] in - AssertIsOnMainThread() + }, + migrationCompletion: { [weak self] error in + AssertIsOnMainThread() - guard let strongSelf = self else { return } + guard let strongSelf = self else { return } - // performUpdateCheck must be invoked after Environment has been initialized because - // upgrade process may depend on Environment. - strongSelf.versionMigrationsDidComplete() + if let error = error { + owsFailDebug("Error \(error)") + strongSelf.showNotReadyView() + return + } + + // performUpdateCheck must be invoked after Environment has been initialized because + // upgrade process may depend on Environment. + strongSelf.versionMigrationsDidComplete() }) let shareViewNavigationController = OWSNavigationController()