From 0fee67ae60d21b22636b6d006a142675b3c836b9 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Mon, 22 Mar 2021 15:22:32 -0700 Subject: [PATCH 1/6] Attempt to recover database if corruption was detected --- Pods | 2 +- Signal/src/AppDelegate.m | 8 ++ .../Database/GRDBDatabaseStorageAdapter.swift | 41 ++++-- .../Storage/Database/SDSDatabaseStorage.swift | 87 ++++++------ .../Storage/Database/SDSKeyValueStore.swift | 15 +- .../src/Storage/Database/SDSRecord.swift | 7 + .../src/Storage/Database/SDSTransaction.swift | 14 ++ .../src/Storage/StorageCoordinator.swift | 130 ++++++++++++++++++ SignalServiceKit/src/Util/AppVersion.m | 4 +- .../src/Util/SSKPreferences.swift | 37 +++++ 10 files changed, 293 insertions(+), 52 deletions(-) create mode 100644 SignalServiceKit/src/Storage/StorageCoordinator.swift diff --git a/Pods b/Pods index dbde19c754..7cea8fc959 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit dbde19c75497cec39e438cd81ca09d5382e889c1 +Subproject commit 7cea8fc959e779923220150ae876731f629b7f44 diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 8f2a6dc84a..c7dbeb9fe2 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -189,6 +189,14 @@ void uncaughtExceptionHandler(NSException *exception) // Prevent: // * Users with an unknown GRDB schema revert to using an earlier GRDB schema. launchFailure = LaunchFailure_UnknownDatabaseVersion; + } else if (StorageCoordinator.hasDatabaseCorruption) { + OWSLogInfo(@"Attempting to recover database corruption."); + NSError *error; + [StorageCoordinator attemptDatabaseRecoveryAndReturnError:&error]; + if (error) { + OWSFailDebug(@"Failed to recovery corrupted database %@", error); + launchFailure = LaunchFailure_UnknownDatabaseVersion; + } } if (launchFailure != LaunchFailure_None) { OWSLogInfo(@"application: didFinishLaunchingWithOptions failed."); diff --git a/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift b/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift index 6b9ecfefb5..fab733990c 100644 --- a/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift +++ b/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift @@ -11,12 +11,26 @@ 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) + enum DirectoryMode { + case primary + case recovery + case backup + + var folderName: String { + switch self { + case .primary: return "grdb" + case .recovery: return "grdb-recovery" + case .backup: return "grdb-backup" + } + } } - static func databaseFileUrl(baseDir: URL) -> URL { - let databaseDir = databaseDirUrl(baseDir: baseDir) + static func databaseDirUrl(baseDir: URL, directoryMode: DirectoryMode = .primary) -> URL { + return baseDir.appendingPathComponent(directoryMode.folderName, isDirectory: true) + } + + 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) } @@ -235,6 +249,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 +538,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..ce92b9214d 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,35 +106,39 @@ 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 reopenGRDBStorage(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() + + 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() { AssertIsOnMainThread() assert(storageCoordinatorState == .GRDB) @@ -140,16 +147,14 @@ public class SDSDatabaseStorage: SDSTransactable { let wasRegistered = TSAccountManager.shared.isRegistered - let grdbStorage = createGrdbStorage() - _grdbStorage = grdbStorage + reopenGRDBStorage { + _ = 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) @@ -158,19 +163,19 @@ public class SDSDatabaseStorage: SDSTransactable { func createGrdbStorage() -> GRDBDatabaseStorageAdapter { return Bench(title: "Creating GRDB storage") { - return GRDBDatabaseStorageAdapter(baseDir: type(of: self).baseDir()) + return GRDBDatabaseStorageAdapter(baseDir: type(of: self).baseDir) } } @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()) + OWSStorage.resetAllStorage() + 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/Storage/StorageCoordinator.swift b/SignalServiceKit/src/Storage/StorageCoordinator.swift new file mode 100644 index 0000000000..905185617b --- /dev/null +++ b/SignalServiceKit/src/Storage/StorageCoordinator.swift @@ -0,0 +1,130 @@ +// +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// + +import Foundation +import GRDB + +extension StorageCoordinator { + @objc + public static var hasDatabaseCorruption: Bool { + guard dataStoreForUI == .grdb else { return false } + return SSKPreferences.hasGrdbDatabaseCorruption() + } + + @objc + public static func attemptDatabaseRecovery() throws { + guard hasDatabaseCorruption else { return } + + let baseDir = SDSDatabaseStorage.baseDir + + let primaryDatabaseDirURL = SDSDatabaseStorage.grdbDatabaseDirUrl + let primaryDatabaseFileURL = SDSDatabaseStorage.grdbDatabaseFileUrl + + let backupDatabaseDirURL = GRDBDatabaseStorageAdapter.databaseDirUrl(baseDir: baseDir, directoryMode: .backup) + let backupDatabaseFileURL = GRDBDatabaseStorageAdapter.databaseFileUrl(baseDir: baseDir, directoryMode: .backup) + owsAssert(primaryDatabaseDirURL != backupDatabaseDirURL) + owsAssert(primaryDatabaseFileURL != backupDatabaseFileURL) + + let recoveryDatabaseDirURL = GRDBDatabaseStorageAdapter.databaseDirUrl(baseDir: baseDir, directoryMode: .recovery) + let recoveryDatabaseFileURL = GRDBDatabaseStorageAdapter.databaseFileUrl(baseDir: baseDir, directoryMode: .recovery) + owsAssert(primaryDatabaseDirURL != recoveryDatabaseDirURL) + owsAssert(primaryDatabaseFileURL != recoveryDatabaseFileURL) + + if !OWSFileSystem.fileOrFolderExists(url: primaryDatabaseFileURL) + && OWSFileSystem.fileOrFolderExists(url: backupDatabaseFileURL) + && OWSFileSystem.fileOrFolderExists(url: recoveryDatabaseFileURL) { + // If we have a backup *and* recovery file already, and the primary database no longer exists, + // we probably were terminated during some sensitive file operations. Lets just try and finish + // that work and not try and make a new recovery DB + Logger.info("Missing primary database, but a recovery DB is present. Attempting recovery.") + } else { + Logger.info("Attempting database recovery.") + + // If there is already a "recovery" database lingering, delete it. + try OWSFileSystem.deleteFileIfExists(url: recoveryDatabaseDirURL) + OWSFileSystem.ensureDirectoryExists(recoveryDatabaseDirURL.path) + + // Before we open the database, create a backup copy. This is just + // out of paranoia, we'll clean it up if later everything seems fine. + try OWSFileSystem.deleteFileIfExists(url: backupDatabaseDirURL) + try FileManager.default.copyItem(at: primaryDatabaseDirURL, to: backupDatabaseDirURL) + + // Initialize the GRDB storage for the primary DB. It is + // critical that this is the first time we access GRDB storage, + // but it is equally critical we close the connection and release + // it before the app continues on. + + try autoreleasepool { + let keyspec = GRDBDatabaseStorageAdapter.keyspec + let grdbStorage = GRDBDatabaseStorageAdapter(baseDir: baseDir) + defer { grdbStorage.pool.releaseMemory() } + + // Attempt to export a copy of the database. The theory is that, + // in export, some corrupted aspects of the database can be + // resolved in the newly exported database. + // TODO: we could eventually try and use sqlite's ".recover" + // command which supposedly does a more thorough job, but it + // provides complications with sqlcipher so for now we avoid it. + try grdbStorage.pool.writeWithoutTransaction { db in + // Attach a recovery database + try db.execute( + sql: "ATTACH DATABASE ? AS recovery_db", + arguments: [ + recoveryDatabaseFileURL.absoluteString + ] + ) + + // Setup sqlcipher for the attached database exactly + // matching the primary database. + try GRDBDatabaseStorageAdapter.prepareDatabase( + db: db, + keyspec: keyspec, + name: "recovery_db" + ) + + // Export our database to the recovery database + try db.execute( + sql: "SELECT sqlcipher_export('recovery_db')" + ) + + // Detach the recovery database + try db.execute( + sql: "DETACH DATABASE recovery_db" + ) + } + + // Verify we can open the recovery database + var configuration = Configuration() + configuration.prepareDatabase = { db in + try GRDBDatabaseStorageAdapter.prepareDatabase(db: db, keyspec: keyspec) + } + let recoveryPool = try DatabasePool(path: recoveryDatabaseFileURL.path, configuration: configuration) + recoveryPool.releaseMemory() + } + } + + // Finally, we attempt to replace the primary database with the + // recovery database. This is dangerous, but the user is already + // in a corrupted state so some risk is worthwhile. In theory, + // there should be no permanent data loss that doesn't already + // exist since we've established a backup database. + try OWSFileSystem.deleteFileIfExists(url: primaryDatabaseDirURL) + + // Move the recovery database into place. + try FileManager.default.moveItem(at: recoveryDatabaseDirURL, to: primaryDatabaseDirURL) + + Logger.info("Database recovery complete, attempting to continue with recovered database.") + + // Mark DB as no longer corrupted + SSKPreferences.setHasGrdbDatabaseCorruption(false) + SSKPreferences.setHasGrdbEverRecoveredCorruptedDatabase(true) + + // TODO: At some point, we must delete the backup database. + // I'm not doing that while we can (hopefully) verify success + // of this method with a user currently encountering corruption + // since it leaves us a channel of recovery if things go horribly + // wrong. Before this makes it into a non-beta build we must do + // that cleanup. + } +} diff --git a/SignalServiceKit/src/Util/AppVersion.m b/SignalServiceKit/src/Util/AppVersion.m index 7c416ea67e..6d1c987a4b 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" @@ -107,6 +107,8 @@ NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_NSE OWSLogInfo(@"firstAppVersion: %@", self.firstAppVersion); OWSLogInfo(@"lastAppVersion: %@", self.lastAppVersion); OWSLogInfo(@"currentAppVersion: %@ (%@)", self.currentAppVersion, longVersionString); + OWSLogInfo(@"hasGrdbEverRecoveredCorruptedDatabase: %@", + [SSKPreferences hasGrdbEverRecoveredCorruptedDatabase] ? @"YES" : @"NO"); OWSLogInfo(@"lastCompletedLaunchAppVersion: %@", self.lastCompletedLaunchAppVersion); OWSLogInfo(@"lastCompletedLaunchMainAppVersion: %@", self.lastCompletedLaunchMainAppVersion); diff --git a/SignalServiceKit/src/Util/SSKPreferences.swift b/SignalServiceKit/src/Util/SSKPreferences.swift index 17c7f368b9..891d8c6c57 100644 --- a/SignalServiceKit/src/Util/SSKPreferences.swift +++ b/SignalServiceKit/src/Util/SSKPreferences.swift @@ -265,4 +265,41 @@ 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 { + return true + 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() + } + + private static let hasGrdbEverRecoveredCorruptedDatabaseKey = "hasGrdbEverRecoveredCorruptedDatabase" + @objc + public static func hasGrdbEverRecoveredCorruptedDatabase() -> Bool { + let appUserDefaults = CurrentAppContext().appUserDefaults() + guard let preference = appUserDefaults.object(forKey: hasGrdbEverRecoveredCorruptedDatabaseKey) as? NSNumber else { + return false + } + return preference.boolValue + } + + @objc + public static func setHasGrdbEverRecoveredCorruptedDatabase(_ value: Bool) { + let appUserDefaults = CurrentAppContext().appUserDefaults() + appUserDefaults.set(value, forKey: hasGrdbEverRecoveredCorruptedDatabaseKey) + appUserDefaults.synchronize() + } } From 4e535fa7efd2b9f9c974eecb17047f36d577ef83 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Mon, 29 Mar 2021 12:25:10 -0700 Subject: [PATCH 2/6] Make database recovery async --- .../NotificationService.swift | 7 +++- Signal/src/AppDelegate.m | 37 +++++++++++-------- SignalMessaging/environment/AppSetup.h | 4 +- SignalMessaging/environment/AppSetup.m | 19 ++++++++-- .../Storage/Database/SDSDatabaseStorage.swift | 7 ++-- .../src/Storage/StorageCoordinator.swift | 2 + .../src/Util/SSKPreferences.swift | 1 - .../ShareViewController.swift | 20 ++++++---- 8 files changed, 65 insertions(+), 32 deletions(-) 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 c7dbeb9fe2..a1630a2a72 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,14 +192,6 @@ void uncaughtExceptionHandler(NSException *exception) // Prevent: // * Users with an unknown GRDB schema revert to using an earlier GRDB schema. launchFailure = LaunchFailure_UnknownDatabaseVersion; - } else if (StorageCoordinator.hasDatabaseCorruption) { - OWSLogInfo(@"Attempting to recover database corruption."); - NSError *error; - [StorageCoordinator attemptDatabaseRecoveryAndReturnError:&error]; - if (error) { - OWSFailDebug(@"Failed to recovery corrupted database %@", error); - launchFailure = LaunchFailure_UnknownDatabaseVersion; - } } if (launchFailure != LaunchFailure_None) { OWSLogInfo(@"application: didFinishLaunchingWithOptions failed."); @@ -219,10 +214,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]; @@ -317,8 +317,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" @@ -331,6 +333,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."); @@ -536,8 +540,6 @@ void uncaughtExceptionHandler(NSException *exception) return; } - [SignalApp.shared ensureRootViewController:launchStartedAt]; - AppReadinessRunNowOrWhenAppDidBecomeReadySync(^{ [self handleActivation]; }); // Clear all notifications whenever we become active. @@ -1068,6 +1070,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/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..4eae1a4163 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); @@ -191,13 +191,26 @@ NS_ASSUME_NONNULL_BEGIN dispatch_block_t completionBlock = ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (StorageCoordinator.hasDatabaseCorruption) { + OWSLogInfo(@"Attempting to recover database corruption."); + NSError *error; + [StorageCoordinator attemptDatabaseRecoveryAndReturnError:&error]; + if (error != nil) { + OWSFailDebug(@"Failed to recovery corrupted database %@", error); + + dispatch_async(dispatch_get_main_queue(), ^{ migrationCompletion(error); }); + } + } + if (AppSetup.shouldTruncateGrdbWal) { // Try to truncate GRDB WAL before any readers or writers are // active. 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 +226,7 @@ NS_ASSUME_NONNULL_BEGIN if (StorageCoordinator.dataStoreForUI == DataStoreGrdb) { [SSKEnvironment.shared warmCaches]; } - migrationCompletion(); + migrationCompletion(nil); OWSAssertDebug(backgroundTask); backgroundTask = nil; diff --git a/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift b/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift index ce92b9214d..4d4ffd66a7 100644 --- a/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift +++ b/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift @@ -156,8 +156,9 @@ public class SDSDatabaseStorage: SDSTransactable { 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) + } } } @@ -174,7 +175,7 @@ public class SDSDatabaseStorage: SDSTransactable { @objc public func resetAllStorage() { - OWSStorage.resetAllStorage() + YDBStorage.deleteYDBStorage() GRDBDatabaseStorageAdapter.resetAllStorage(baseDir: type(of: self).baseDir) } diff --git a/SignalServiceKit/src/Storage/StorageCoordinator.swift b/SignalServiceKit/src/Storage/StorageCoordinator.swift index 905185617b..305b323103 100644 --- a/SignalServiceKit/src/Storage/StorageCoordinator.swift +++ b/SignalServiceKit/src/Storage/StorageCoordinator.swift @@ -114,6 +114,8 @@ extension StorageCoordinator { // Move the recovery database into place. try FileManager.default.moveItem(at: recoveryDatabaseDirURL, to: primaryDatabaseDirURL) + databaseStorage.reopenGRDBStorage() + Logger.info("Database recovery complete, attempting to continue with recovered database.") // Mark DB as no longer corrupted diff --git a/SignalServiceKit/src/Util/SSKPreferences.swift b/SignalServiceKit/src/Util/SSKPreferences.swift index 891d8c6c57..5c1f24c4c6 100644 --- a/SignalServiceKit/src/Util/SSKPreferences.swift +++ b/SignalServiceKit/src/Util/SSKPreferences.swift @@ -269,7 +269,6 @@ public class SSKPreferences: NSObject { private static let hasGrdbDatabaseCorruptionKey = "hasGrdbDatabaseCorruption" @objc public static func hasGrdbDatabaseCorruption() -> Bool { - return true let appUserDefaults = CurrentAppContext().appUserDefaults() guard let preference = appUserDefaults.object(forKey: hasGrdbDatabaseCorruptionKey) as? NSNumber else { return false 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() From db5532b590bfb58575d8b093da083490226d6b78 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Mon, 29 Mar 2021 14:39:23 -0700 Subject: [PATCH 3/6] protect database recovery from orphan data cleaner --- SignalMessaging/utils/OWSOrphanDataCleaner.m | 25 ++++++++++++++++--- .../Database/GRDBDatabaseStorageAdapter.swift | 6 +++-- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/SignalMessaging/utils/OWSOrphanDataCleaner.m b/SignalMessaging/utils/OWSOrphanDataCleaner.m index 3d929e2ff7..2444a90ca3 100644 --- a/SignalMessaging/utils/OWSOrphanDataCleaner.m +++ b/SignalMessaging/utils/OWSOrphanDataCleaner.m @@ -313,17 +313,34 @@ 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 *grdbRecoveryDirectoryPath = + [GRDBDatabaseStorageAdapter databaseDirUrlWithBaseDir:SDSDatabaseStorage.baseDir + directoryMode:DirectoryModeRecovery] + .path; + NSString *grdbBackupDirectoryPath = [GRDBDatabaseStorageAdapter databaseDirUrlWithBaseDir:SDSDatabaseStorage.baseDir + directoryMode:DirectoryModeBackup] + .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:grdbRecoveryDirectoryPath]) { + OWSLogInfo(@"Protecting database recovery file: %@", filePath); + [databaseFilePaths addObject:filePath]; + } else if ([filePath hasPrefix:grdbBackupDirectoryPath]) { + OWSLogInfo(@"Protecting database backup 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 fab733990c..6296a9f04e 100644 --- a/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift +++ b/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift @@ -11,7 +11,8 @@ public class GRDBDatabaseStorageAdapter: NSObject { // 256 bit key + 128 bit salt public static let kSQLCipherKeySpecLength: UInt = 48 - enum DirectoryMode { + @objc + public enum DirectoryMode: Int { case primary case recovery case backup @@ -25,7 +26,8 @@ public class GRDBDatabaseStorageAdapter: NSObject { } } - static func databaseDirUrl(baseDir: URL, directoryMode: DirectoryMode = .primary) -> URL { + @objc + public static func databaseDirUrl(baseDir: URL, directoryMode: DirectoryMode = .primary) -> URL { return baseDir.appendingPathComponent(directoryMode.folderName, isDirectory: true) } From 722675525de9b6dba1511a55b14585ba7cb98bfc Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Mon, 29 Mar 2021 14:44:58 -0700 Subject: [PATCH 4/6] Change device transfer hotswap to avoid database corruption. --- ...ceTransferService+MultipeerDelegates.swift | 4 +- .../DeviceTransferService+Restore.swift | 181 +++++++++++++++--- SignalMessaging/utils/OWSOrphanDataCleaner.m | 7 + .../Database/GRDBDatabaseStorageAdapter.swift | 14 +- .../Storage/Database/SDSDatabaseStorage.swift | 15 +- 5 files changed, 181 insertions(+), 40 deletions(-) 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..89153c8243 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,62 @@ 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 = databasePrimaryPath + } else if DeviceTransferService.databaseWALIdentifier == file.identifier { + newFilePath = databaseWalPrimaryPath + } 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 = databaseHotswapPath + } else if DeviceTransferService.databaseWALIdentifier == file.identifier { + hotswapFilePath = databaseWalHotswapPath + } 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 +224,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 +259,98 @@ 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 var databasePrimaryPath: String { + GRDBDatabaseStorageAdapter.databaseFileUrl( + baseDir: SDSDatabaseStorage.baseDir, + directoryMode: .primary + ).path + } + private var databaseWalPrimaryPath: String { + GRDBDatabaseStorageAdapter.databaseWalUrl( + baseDir: SDSDatabaseStorage.baseDir, + directoryMode: .primary + ).path + } + + private var databaseHotswapPath: String { + GRDBDatabaseStorageAdapter.databaseFileUrl( + baseDir: SDSDatabaseStorage.baseDir, + directoryMode: .hotswap + ).path + } + private var databaseWalHotswapPath: String { + GRDBDatabaseStorageAdapter.databaseWalUrl( + baseDir: SDSDatabaseStorage.baseDir, + directoryMode: .hotswap + ).path + } + + private func promoteTransferDatabaseToPrimaryDatabase() -> Bool { + Logger.info("Promoting the hotswap database to the primary database") + + if OWSFileSystem.fileOrFolderExists(atPath: databaseHotswapPath) { + guard move(pendingFilePath: databaseHotswapPath, to: databasePrimaryPath) else { + owsFailDebug("Failed to promote hotswap database to primary database.") + return false + } + } else { + guard OWSFileSystem.fileOrFolderExists(atPath: databasePrimaryPath) else { + owsFailDebug("Missing primary and hotswap database files.") + return false + } + Logger.info("Missing hotswap database, we may have partially restored. Assuming primary database is correct.") + } + + if OWSFileSystem.fileOrFolderExists(atPath: databaseWalHotswapPath) { + guard move(pendingFilePath: databaseWalHotswapPath, to: databaseWalPrimaryPath) else { + owsFailDebug("Failed to promote hotswap database wal to primary database wal.") + return false + } + } else { + guard OWSFileSystem.fileOrFolderExists(atPath: databaseWalPrimaryPath) else { + owsFailDebug("Missing primary and hotswap database wal files.") + return false + } + Logger.info("Missing hotswap database wal, we may have partially restored. Assuming primary database wal is correct.") + } + + pendingPromotionFromHotSwapToPrimaryDatabase = false + + return true + } + @objc func launchCleanup() -> Bool { Logger.info("hasBeenRestored: \(hasBeenRestored)") @@ -244,7 +365,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/utils/OWSOrphanDataCleaner.m b/SignalMessaging/utils/OWSOrphanDataCleaner.m index 2444a90ca3..3bbc5f2863 100644 --- a/SignalMessaging/utils/OWSOrphanDataCleaner.m +++ b/SignalMessaging/utils/OWSOrphanDataCleaner.m @@ -317,6 +317,10 @@ typedef void (^OrphanDataBlock)(OWSOrphanData *); [GRDBDatabaseStorageAdapter databaseDirUrlWithBaseDir:SDSDatabaseStorage.baseDir directoryMode:DirectoryModePrimary] .path; + NSString *grdbHotswapDirectoryPath = + [GRDBDatabaseStorageAdapter databaseDirUrlWithBaseDir:SDSDatabaseStorage.baseDir + directoryMode:DirectoryModeHotswap] + .path; NSString *grdbRecoveryDirectoryPath = [GRDBDatabaseStorageAdapter databaseDirUrlWithBaseDir:SDSDatabaseStorage.baseDir directoryMode:DirectoryModeRecovery] @@ -329,6 +333,9 @@ typedef void (^OrphanDataBlock)(OWSOrphanData *); 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]; } else if ([filePath hasPrefix:grdbRecoveryDirectoryPath]) { OWSLogInfo(@"Protecting database recovery file: %@", filePath); [databaseFilePaths addObject:filePath]; diff --git a/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift b/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift index 6296a9f04e..4b20c3d823 100644 --- a/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift +++ b/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift @@ -16,12 +16,14 @@ public class GRDBDatabaseStorageAdapter: NSObject { case primary case recovery case backup + case hotswap var folderName: String { switch self { case .primary: return "grdb" case .recovery: return "grdb-recovery" case .backup: return "grdb-backup" + case .hotswap: return "grdb-hotswap" } } } @@ -31,12 +33,18 @@ public class GRDBDatabaseStorageAdapter: NSObject { return baseDir.appendingPathComponent(directoryMode.folderName, isDirectory: true) } - static func databaseFileUrl(baseDir: URL, directoryMode: DirectoryMode = .primary) -> URL { + 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 @@ -45,8 +53,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. diff --git a/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift b/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift index 4d4ffd66a7..12cca4aaca 100644 --- a/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift +++ b/SignalServiceKit/src/Storage/Database/SDSDatabaseStorage.swift @@ -112,7 +112,10 @@ public class SDSDatabaseStorage: SDSTransactable { } } - public func reopenGRDBStorage(completion: @escaping () -> Void = {}) { + 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 @@ -123,7 +126,7 @@ public class SDSDatabaseStorage: SDSTransactable { weak var weakGrdbStorage = grdbStorage owsAssertDebug(weakPool != nil) owsAssertDebug(weakGrdbStorage != nil) - _grdbStorage = createGrdbStorage() + _grdbStorage = createGrdbStorage(directoryMode: directoryMode) DispatchQueue.main.async { // We want to make sure all db connections from the old adapter/pool are closed. @@ -139,7 +142,7 @@ public class SDSDatabaseStorage: SDSTransactable { } } - public func reload() { + public func reload(directoryMode: GRDBDatabaseStorageAdapter.DirectoryMode = .primary) { AssertIsOnMainThread() assert(storageCoordinatorState == .GRDB) @@ -147,7 +150,7 @@ public class SDSDatabaseStorage: SDSTransactable { let wasRegistered = TSAccountManager.shared.isRegistered - reopenGRDBStorage { + reopenGRDBStorage(directoryMode: directoryMode) { _ = GRDBSchemaMigrator().runSchemaMigrations() self.grdbStorage.forceUpdateSnapshot() @@ -162,9 +165,9 @@ public class SDSDatabaseStorage: SDSTransactable { } } - 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) } } From 0ef7fc24e690c25a11ae6cf4971b1c72699948ae Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Tue, 30 Mar 2021 12:01:30 -0700 Subject: [PATCH 5/6] Revert database recovery attempts, just flag when we detect corruption and prevent app launch. --- Pods | 2 +- Signal/src/AppDelegate.m | 2 + SignalMessaging/environment/AppSetup.m | 11 -- SignalMessaging/utils/OWSOrphanDataCleaner.m | 14 +- .../Database/GRDBDatabaseStorageAdapter.swift | 4 - .../src/Storage/StorageCoordinator.swift | 132 ------------------ SignalServiceKit/src/Util/AppVersion.m | 2 - .../src/Util/SSKPreferences.swift | 17 --- 8 files changed, 4 insertions(+), 180 deletions(-) delete mode 100644 SignalServiceKit/src/Storage/StorageCoordinator.swift diff --git a/Pods b/Pods index 7cea8fc959..dbde19c754 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit 7cea8fc959e779923220150ae876731f629b7f44 +Subproject commit dbde19c75497cec39e438cd81ca09d5382e889c1 diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index a1630a2a72..2bec233726 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -192,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."); diff --git a/SignalMessaging/environment/AppSetup.m b/SignalMessaging/environment/AppSetup.m index 4eae1a4163..967d7d2d14 100644 --- a/SignalMessaging/environment/AppSetup.m +++ b/SignalMessaging/environment/AppSetup.m @@ -191,17 +191,6 @@ NS_ASSUME_NONNULL_BEGIN dispatch_block_t completionBlock = ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - if (StorageCoordinator.hasDatabaseCorruption) { - OWSLogInfo(@"Attempting to recover database corruption."); - NSError *error; - [StorageCoordinator attemptDatabaseRecoveryAndReturnError:&error]; - if (error != nil) { - OWSFailDebug(@"Failed to recovery corrupted database %@", error); - - dispatch_async(dispatch_get_main_queue(), ^{ migrationCompletion(error); }); - } - } - if (AppSetup.shouldTruncateGrdbWal) { // Try to truncate GRDB WAL before any readers or writers are // active. diff --git a/SignalMessaging/utils/OWSOrphanDataCleaner.m b/SignalMessaging/utils/OWSOrphanDataCleaner.m index 3bbc5f2863..2def3c0d8b 100644 --- a/SignalMessaging/utils/OWSOrphanDataCleaner.m +++ b/SignalMessaging/utils/OWSOrphanDataCleaner.m @@ -321,13 +321,7 @@ typedef void (^OrphanDataBlock)(OWSOrphanData *); [GRDBDatabaseStorageAdapter databaseDirUrlWithBaseDir:SDSDatabaseStorage.baseDir directoryMode:DirectoryModeHotswap] .path; - NSString *grdbRecoveryDirectoryPath = - [GRDBDatabaseStorageAdapter databaseDirUrlWithBaseDir:SDSDatabaseStorage.baseDir - directoryMode:DirectoryModeRecovery] - .path; - NSString *grdbBackupDirectoryPath = [GRDBDatabaseStorageAdapter databaseDirUrlWithBaseDir:SDSDatabaseStorage.baseDir - directoryMode:DirectoryModeBackup] - .path; + NSMutableSet *databaseFilePaths = [NSMutableSet new]; for (NSString *filePath in allOnDiskFilePaths) { if ([filePath hasPrefix:grdbPrimaryDirectoryPath]) { @@ -336,12 +330,6 @@ typedef void (^OrphanDataBlock)(OWSOrphanData *); } else if ([filePath hasPrefix:grdbHotswapDirectoryPath]) { OWSLogInfo(@"Protecting database hotswap file: %@", filePath); [databaseFilePaths addObject:filePath]; - } else if ([filePath hasPrefix:grdbRecoveryDirectoryPath]) { - OWSLogInfo(@"Protecting database recovery file: %@", filePath); - [databaseFilePaths addObject:filePath]; - } else if ([filePath hasPrefix:grdbBackupDirectoryPath]) { - OWSLogInfo(@"Protecting database backup file: %@", filePath); - [databaseFilePaths addObject:filePath]; } } [allOnDiskFilePaths minusSet:databaseFilePaths]; diff --git a/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift b/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift index 4b20c3d823..f51c8fc771 100644 --- a/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift +++ b/SignalServiceKit/src/Storage/Database/GRDBDatabaseStorageAdapter.swift @@ -14,15 +14,11 @@ public class GRDBDatabaseStorageAdapter: NSObject { @objc public enum DirectoryMode: Int { case primary - case recovery - case backup case hotswap var folderName: String { switch self { case .primary: return "grdb" - case .recovery: return "grdb-recovery" - case .backup: return "grdb-backup" case .hotswap: return "grdb-hotswap" } } diff --git a/SignalServiceKit/src/Storage/StorageCoordinator.swift b/SignalServiceKit/src/Storage/StorageCoordinator.swift deleted file mode 100644 index 305b323103..0000000000 --- a/SignalServiceKit/src/Storage/StorageCoordinator.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. -// - -import Foundation -import GRDB - -extension StorageCoordinator { - @objc - public static var hasDatabaseCorruption: Bool { - guard dataStoreForUI == .grdb else { return false } - return SSKPreferences.hasGrdbDatabaseCorruption() - } - - @objc - public static func attemptDatabaseRecovery() throws { - guard hasDatabaseCorruption else { return } - - let baseDir = SDSDatabaseStorage.baseDir - - let primaryDatabaseDirURL = SDSDatabaseStorage.grdbDatabaseDirUrl - let primaryDatabaseFileURL = SDSDatabaseStorage.grdbDatabaseFileUrl - - let backupDatabaseDirURL = GRDBDatabaseStorageAdapter.databaseDirUrl(baseDir: baseDir, directoryMode: .backup) - let backupDatabaseFileURL = GRDBDatabaseStorageAdapter.databaseFileUrl(baseDir: baseDir, directoryMode: .backup) - owsAssert(primaryDatabaseDirURL != backupDatabaseDirURL) - owsAssert(primaryDatabaseFileURL != backupDatabaseFileURL) - - let recoveryDatabaseDirURL = GRDBDatabaseStorageAdapter.databaseDirUrl(baseDir: baseDir, directoryMode: .recovery) - let recoveryDatabaseFileURL = GRDBDatabaseStorageAdapter.databaseFileUrl(baseDir: baseDir, directoryMode: .recovery) - owsAssert(primaryDatabaseDirURL != recoveryDatabaseDirURL) - owsAssert(primaryDatabaseFileURL != recoveryDatabaseFileURL) - - if !OWSFileSystem.fileOrFolderExists(url: primaryDatabaseFileURL) - && OWSFileSystem.fileOrFolderExists(url: backupDatabaseFileURL) - && OWSFileSystem.fileOrFolderExists(url: recoveryDatabaseFileURL) { - // If we have a backup *and* recovery file already, and the primary database no longer exists, - // we probably were terminated during some sensitive file operations. Lets just try and finish - // that work and not try and make a new recovery DB - Logger.info("Missing primary database, but a recovery DB is present. Attempting recovery.") - } else { - Logger.info("Attempting database recovery.") - - // If there is already a "recovery" database lingering, delete it. - try OWSFileSystem.deleteFileIfExists(url: recoveryDatabaseDirURL) - OWSFileSystem.ensureDirectoryExists(recoveryDatabaseDirURL.path) - - // Before we open the database, create a backup copy. This is just - // out of paranoia, we'll clean it up if later everything seems fine. - try OWSFileSystem.deleteFileIfExists(url: backupDatabaseDirURL) - try FileManager.default.copyItem(at: primaryDatabaseDirURL, to: backupDatabaseDirURL) - - // Initialize the GRDB storage for the primary DB. It is - // critical that this is the first time we access GRDB storage, - // but it is equally critical we close the connection and release - // it before the app continues on. - - try autoreleasepool { - let keyspec = GRDBDatabaseStorageAdapter.keyspec - let grdbStorage = GRDBDatabaseStorageAdapter(baseDir: baseDir) - defer { grdbStorage.pool.releaseMemory() } - - // Attempt to export a copy of the database. The theory is that, - // in export, some corrupted aspects of the database can be - // resolved in the newly exported database. - // TODO: we could eventually try and use sqlite's ".recover" - // command which supposedly does a more thorough job, but it - // provides complications with sqlcipher so for now we avoid it. - try grdbStorage.pool.writeWithoutTransaction { db in - // Attach a recovery database - try db.execute( - sql: "ATTACH DATABASE ? AS recovery_db", - arguments: [ - recoveryDatabaseFileURL.absoluteString - ] - ) - - // Setup sqlcipher for the attached database exactly - // matching the primary database. - try GRDBDatabaseStorageAdapter.prepareDatabase( - db: db, - keyspec: keyspec, - name: "recovery_db" - ) - - // Export our database to the recovery database - try db.execute( - sql: "SELECT sqlcipher_export('recovery_db')" - ) - - // Detach the recovery database - try db.execute( - sql: "DETACH DATABASE recovery_db" - ) - } - - // Verify we can open the recovery database - var configuration = Configuration() - configuration.prepareDatabase = { db in - try GRDBDatabaseStorageAdapter.prepareDatabase(db: db, keyspec: keyspec) - } - let recoveryPool = try DatabasePool(path: recoveryDatabaseFileURL.path, configuration: configuration) - recoveryPool.releaseMemory() - } - } - - // Finally, we attempt to replace the primary database with the - // recovery database. This is dangerous, but the user is already - // in a corrupted state so some risk is worthwhile. In theory, - // there should be no permanent data loss that doesn't already - // exist since we've established a backup database. - try OWSFileSystem.deleteFileIfExists(url: primaryDatabaseDirURL) - - // Move the recovery database into place. - try FileManager.default.moveItem(at: recoveryDatabaseDirURL, to: primaryDatabaseDirURL) - - databaseStorage.reopenGRDBStorage() - - Logger.info("Database recovery complete, attempting to continue with recovered database.") - - // Mark DB as no longer corrupted - SSKPreferences.setHasGrdbDatabaseCorruption(false) - SSKPreferences.setHasGrdbEverRecoveredCorruptedDatabase(true) - - // TODO: At some point, we must delete the backup database. - // I'm not doing that while we can (hopefully) verify success - // of this method with a user currently encountering corruption - // since it leaves us a channel of recovery if things go horribly - // wrong. Before this makes it into a non-beta build we must do - // that cleanup. - } -} diff --git a/SignalServiceKit/src/Util/AppVersion.m b/SignalServiceKit/src/Util/AppVersion.m index 6d1c987a4b..0309f46356 100755 --- a/SignalServiceKit/src/Util/AppVersion.m +++ b/SignalServiceKit/src/Util/AppVersion.m @@ -107,8 +107,6 @@ NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_NSE OWSLogInfo(@"firstAppVersion: %@", self.firstAppVersion); OWSLogInfo(@"lastAppVersion: %@", self.lastAppVersion); OWSLogInfo(@"currentAppVersion: %@ (%@)", self.currentAppVersion, longVersionString); - OWSLogInfo(@"hasGrdbEverRecoveredCorruptedDatabase: %@", - [SSKPreferences hasGrdbEverRecoveredCorruptedDatabase] ? @"YES" : @"NO"); OWSLogInfo(@"lastCompletedLaunchAppVersion: %@", self.lastCompletedLaunchAppVersion); OWSLogInfo(@"lastCompletedLaunchMainAppVersion: %@", self.lastCompletedLaunchMainAppVersion); diff --git a/SignalServiceKit/src/Util/SSKPreferences.swift b/SignalServiceKit/src/Util/SSKPreferences.swift index 5c1f24c4c6..920e379c93 100644 --- a/SignalServiceKit/src/Util/SSKPreferences.swift +++ b/SignalServiceKit/src/Util/SSKPreferences.swift @@ -284,21 +284,4 @@ public class SSKPreferences: NSObject { appUserDefaults.set(value, forKey: hasGrdbDatabaseCorruptionKey) appUserDefaults.synchronize() } - - private static let hasGrdbEverRecoveredCorruptedDatabaseKey = "hasGrdbEverRecoveredCorruptedDatabase" - @objc - public static func hasGrdbEverRecoveredCorruptedDatabase() -> Bool { - let appUserDefaults = CurrentAppContext().appUserDefaults() - guard let preference = appUserDefaults.object(forKey: hasGrdbEverRecoveredCorruptedDatabaseKey) as? NSNumber else { - return false - } - return preference.boolValue - } - - @objc - public static func setHasGrdbEverRecoveredCorruptedDatabase(_ value: Bool) { - let appUserDefaults = CurrentAppContext().appUserDefaults() - appUserDefaults.set(value, forKey: hasGrdbEverRecoveredCorruptedDatabaseKey) - appUserDefaults.synchronize() - } } From 292be6f4a367d62966f04511b3b1a5c51d49e418 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Thu, 1 Apr 2021 12:56:39 -0700 Subject: [PATCH 6/6] PR Feedback --- .../DeviceTransferService+Restore.swift | 78 +++++++------------ 1 file changed, 30 insertions(+), 48 deletions(-) diff --git a/Signal/src/util/Device Transfer/DeviceTransferService+Restore.swift b/Signal/src/util/Device Transfer/DeviceTransferService+Restore.swift index 89153c8243..82de3ac128 100644 --- a/Signal/src/util/Device Transfer/DeviceTransferService+Restore.swift +++ b/Signal/src/util/Device Transfer/DeviceTransferService+Restore.swift @@ -157,9 +157,15 @@ extension DeviceTransferService { // path will be later overriden with the hotswap path. let newFilePath: String if DeviceTransferService.databaseIdentifier == file.identifier { - newFilePath = databasePrimaryPath + newFilePath = GRDBDatabaseStorageAdapter.databaseFileUrl( + baseDir: SDSDatabaseStorage.baseDir, + directoryMode: .primary + ).path } else if DeviceTransferService.databaseWALIdentifier == file.identifier { - newFilePath = databaseWalPrimaryPath + newFilePath = GRDBDatabaseStorageAdapter.databaseWalUrl( + baseDir: SDSDatabaseStorage.baseDir, + directoryMode: .primary + ).path } else { newFilePath = URL( fileURLWithPath: file.relativePath, @@ -174,9 +180,15 @@ extension DeviceTransferService { // tries to perform a write. var hotswapFilePath: String? if DeviceTransferService.databaseIdentifier == file.identifier { - hotswapFilePath = databaseHotswapPath + hotswapFilePath = GRDBDatabaseStorageAdapter.databaseFileUrl( + baseDir: SDSDatabaseStorage.baseDir, + directoryMode: .hotswap + ).path } else if DeviceTransferService.databaseWALIdentifier == file.identifier { - hotswapFilePath = databaseWalHotswapPath + hotswapFilePath = GRDBDatabaseStorageAdapter.databaseWalUrl( + baseDir: SDSDatabaseStorage.baseDir, + directoryMode: .hotswap + ).path } let fileIsAwaitingRestoration = OWSFileSystem.fileOrFolderExists(atPath: pendingFilePath) @@ -291,59 +303,29 @@ extension DeviceTransferService { return true } - private var databasePrimaryPath: String { - GRDBDatabaseStorageAdapter.databaseFileUrl( - baseDir: SDSDatabaseStorage.baseDir, - directoryMode: .primary - ).path - } - private var databaseWalPrimaryPath: String { - GRDBDatabaseStorageAdapter.databaseWalUrl( - baseDir: SDSDatabaseStorage.baseDir, - directoryMode: .primary - ).path - } - - private var databaseHotswapPath: String { - GRDBDatabaseStorageAdapter.databaseFileUrl( - baseDir: SDSDatabaseStorage.baseDir, - directoryMode: .hotswap - ).path - } - private var databaseWalHotswapPath: String { - GRDBDatabaseStorageAdapter.databaseWalUrl( - baseDir: SDSDatabaseStorage.baseDir, - directoryMode: .hotswap - ).path - } - private func promoteTransferDatabaseToPrimaryDatabase() -> Bool { Logger.info("Promoting the hotswap database to the primary database") - if OWSFileSystem.fileOrFolderExists(atPath: databaseHotswapPath) { - guard move(pendingFilePath: databaseHotswapPath, to: databasePrimaryPath) else { + 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: databasePrimaryPath) else { - owsFailDebug("Missing primary and hotswap database files.") + guard OWSFileSystem.fileOrFolderExists(atPath: primaryDatabaseDirectoryPath) else { + owsFailDebug("Missing primary and hotswap database directories.") return false } - Logger.info("Missing hotswap database, we may have partially restored. Assuming primary database is correct.") - } - - if OWSFileSystem.fileOrFolderExists(atPath: databaseWalHotswapPath) { - guard move(pendingFilePath: databaseWalHotswapPath, to: databaseWalPrimaryPath) else { - owsFailDebug("Failed to promote hotswap database wal to primary database wal.") - return false - } - } else { - guard OWSFileSystem.fileOrFolderExists(atPath: databaseWalPrimaryPath) else { - owsFailDebug("Missing primary and hotswap database wal files.") - return false - } - Logger.info("Missing hotswap database wal, we may have partially restored. Assuming primary database wal is correct.") + Logger.info("Missing hotswap database, we may have previously restored. Assuming primary database is correct.") } pendingPromotionFromHotSwapToPrimaryDatabase = false