Merge branch 'nt/device-transfer-corruption'
This commit is contained in:
commit
95b681eab3
@ -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()
|
||||
}
|
||||
)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.")
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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] {
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user