Merge branch 'nt/device-transfer-corruption'

This commit is contained in:
Nora Trapp 2021-04-01 13:01:55 -07:00
commit 95b681eab3
15 changed files with 351 additions and 111 deletions

View File

@ -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()
}
)

View File

@ -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;

View File

@ -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.")
}

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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<NSString *> *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);

View File

@ -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

View File

@ -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

View File

@ -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] {

View File

@ -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)")
}
}

View File

@ -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)")
}
}

View File

@ -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"

View File

@ -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()
}
}

View File

@ -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()