// // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSPrimaryStorage.h" #import "AppContext.h" #import "OWSAnalytics.h" #import "OWSBatchMessageProcessor.h" #import "OWSDisappearingMessagesFinder.h" #import "OWSFailedAttachmentDownloadsJob.h" #import "OWSFailedMessagesJob.h" #import "OWSFileSystem.h" #import "OWSIncomingMessageFinder.h" #import "OWSMediaGalleryFinder.h" #import "OWSMessageReceiver.h" #import "OWSStorage+Subclass.h" #import "TSDatabaseSecondaryIndexes.h" #import "TSDatabaseView.h" NS_ASSUME_NONNULL_BEGIN NSString *const OWSPrimaryStorageExceptionName_CouldNotCreateDatabaseDirectory = @"TSStorageManagerExceptionName_CouldNotCreateDatabaseDirectory"; void runSyncRegistrationsForStorage(OWSStorage *storage) { OWSCAssert(storage); // Synchronously register extensions which are essential for views. [TSDatabaseView registerCrossProcessNotifier:storage]; } void runAsyncRegistrationsForStorage(OWSStorage *storage) { OWSCAssert(storage); // Asynchronously register other extensions. // // All sync registrations must be done before all async registrations, // or the sync registrations will block on the async registrations. [TSDatabaseView asyncRegisterThreadInteractionsDatabaseView:storage]; [TSDatabaseView asyncRegisterThreadDatabaseView:storage]; [TSDatabaseView asyncRegisterUnreadDatabaseView:storage]; [storage asyncRegisterExtension:[TSDatabaseSecondaryIndexes registerTimeStampIndex] withName:@"idx"]; [OWSMessageReceiver asyncRegisterDatabaseExtension:storage]; [OWSBatchMessageProcessor asyncRegisterDatabaseExtension:storage]; [TSDatabaseView asyncRegisterUnseenDatabaseView:storage]; [TSDatabaseView asyncRegisterThreadOutgoingMessagesDatabaseView:storage]; [TSDatabaseView asyncRegisterThreadSpecialMessagesDatabaseView:storage]; // Register extensions which aren't essential for rendering threads async. [OWSIncomingMessageFinder asyncRegisterExtensionWithPrimaryStorage:storage]; [TSDatabaseView asyncRegisterSecondaryDevicesDatabaseView:storage]; [OWSDisappearingMessagesFinder asyncRegisterDatabaseExtensions:storage]; [OWSFailedMessagesJob asyncRegisterDatabaseExtensionsWithPrimaryStorage:storage]; [OWSFailedAttachmentDownloadsJob asyncRegisterDatabaseExtensionsWithPrimaryStorage:storage]; [OWSMediaGalleryFinder asyncRegisterDatabaseExtensionsWithPrimaryStorage:storage]; [TSDatabaseView asyncRegisterLazyRestoreAttachmentsDatabaseView:storage]; } #pragma mark - @interface OWSPrimaryStorage () @property (nonatomic, readonly, nullable) YapDatabaseConnection *dbReadConnection; @property (nonatomic, readonly, nullable) YapDatabaseConnection *dbReadWriteConnection; @property (atomic) BOOL areAsyncRegistrationsComplete; @property (atomic) BOOL areSyncRegistrationsComplete; @end #pragma mark - @implementation OWSPrimaryStorage + (instancetype)sharedManager { static OWSPrimaryStorage *sharedManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedManager = [[self alloc] initStorage]; #if TARGET_OS_IPHONE [OWSPrimaryStorage protectFiles]; #endif }); return sharedManager; } - (instancetype)initStorage { self = [super initStorage]; if (self) { [self loadDatabase]; _dbReadConnection = self.newDatabaseConnection; _dbReadWriteConnection = self.newDatabaseConnection; OWSSingletonAssert(); } return self; } - (void)resetStorage { _dbReadConnection = nil; _dbReadWriteConnection = nil; [super resetStorage]; } - (void)runSyncRegistrations { runSyncRegistrationsForStorage(self); // See comments on OWSDatabaseConnection. // // In the absence of finding documentation that can shed light on the issue we've been // seeing, this issue only seems to affect sync and not async registrations. We've always // been opening write transactions before the async registrations complete without negative // consequences. OWSAssert(!self.areSyncRegistrationsComplete); self.areSyncRegistrationsComplete = YES; } - (void)runAsyncRegistrationsWithCompletion:(void (^_Nonnull)(void))completion { OWSAssert(completion); [((OWSDatabase *)self.database)collectRegistrationConnections]; runAsyncRegistrationsForStorage(self); DDLogVerbose(@"%@ async registrations enqueued.", self.logTag); // Block until all async registrations are complete. // // NOTE: This has to happen on the "registration connections" for this // database. NSMutableSet *pendingRegistrationConnectionSet = [[((OWSDatabase *)self.database)clearCollectedRegistrationConnections] mutableCopy]; DDLogVerbose(@"%@ flushing registration connections: %zd.", self.logTag, pendingRegistrationConnectionSet.count); dispatch_async(dispatch_get_main_queue(), ^{ for (YapDatabaseConnection *dbConnection in pendingRegistrationConnectionSet) { [dbConnection flushTransactionsWithCompletionQueue:dispatch_get_main_queue() completionBlock:^{ OWSAssertIsOnMainThread(); OWSAssert(!self.areAsyncRegistrationsComplete); [pendingRegistrationConnectionSet removeObject:dbConnection]; if (pendingRegistrationConnectionSet.count > 0) { DDLogVerbose(@"%@ registration connection flushed.", self.logTag); return; } DDLogVerbose(@"%@ async registrations complete.", self.logTag); self.areAsyncRegistrationsComplete = YES; completion(); }]; } }); } + (void)protectFiles { DDLogInfo( @"%@ Database file size: %@", self.logTag, [OWSFileSystem fileSizeOfPath:self.sharedDataDatabaseFilePath]); DDLogInfo( @"%@ \t SHM file size: %@", self.logTag, [OWSFileSystem fileSizeOfPath:self.sharedDataDatabaseFilePath_SHM]); DDLogInfo( @"%@ \t WAL file size: %@", self.logTag, [OWSFileSystem fileSizeOfPath:self.sharedDataDatabaseFilePath_WAL]); // Protect the entire new database directory. [OWSFileSystem protectFileOrFolderAtPath:self.sharedDataDatabaseDirPath]; } + (NSString *)legacyDatabaseDirPath { return [OWSFileSystem appDocumentDirectoryPath]; } + (NSString *)sharedDataDatabaseDirPath { NSString *databaseDirPath = [[OWSFileSystem appSharedDataDirectoryPath] stringByAppendingPathComponent:@"database"]; if (![OWSFileSystem ensureDirectoryExists:databaseDirPath]) { OWSRaiseException( OWSPrimaryStorageExceptionName_CouldNotCreateDatabaseDirectory, @"Could not create new database directory"); } return databaseDirPath; } + (NSString *)databaseFilename { return @"Signal.sqlite"; } + (NSString *)databaseFilename_SHM { return [self.databaseFilename stringByAppendingString:@"-shm"]; } + (NSString *)databaseFilename_WAL { return [self.databaseFilename stringByAppendingString:@"-wal"]; } + (NSString *)legacyDatabaseFilePath { return [self.legacyDatabaseDirPath stringByAppendingPathComponent:self.databaseFilename]; } + (NSString *)legacyDatabaseFilePath_SHM { return [self.legacyDatabaseDirPath stringByAppendingPathComponent:self.databaseFilename_SHM]; } + (NSString *)legacyDatabaseFilePath_WAL { return [self.legacyDatabaseDirPath stringByAppendingPathComponent:self.databaseFilename_WAL]; } + (NSString *)sharedDataDatabaseFilePath { return [self.sharedDataDatabaseDirPath stringByAppendingPathComponent:self.databaseFilename]; } + (NSString *)sharedDataDatabaseFilePath_SHM { return [self.sharedDataDatabaseDirPath stringByAppendingPathComponent:self.databaseFilename_SHM]; } + (NSString *)sharedDataDatabaseFilePath_WAL { return [self.sharedDataDatabaseDirPath stringByAppendingPathComponent:self.databaseFilename_WAL]; } + (nullable NSError *)migrateToSharedData { DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); // Given how sensitive this migration is, we verbosely // log the contents of all involved paths before and after. NSArray *paths = @[ self.legacyDatabaseFilePath, self.legacyDatabaseFilePath_SHM, self.legacyDatabaseFilePath_WAL, self.sharedDataDatabaseFilePath, self.sharedDataDatabaseFilePath_SHM, self.sharedDataDatabaseFilePath_WAL, ]; NSFileManager *fileManager = [NSFileManager defaultManager]; for (NSString *path in paths) { if ([fileManager fileExistsAtPath:path]) { DDLogInfo(@"%@ before migrateToSharedData: %@, %@", self.logTag, path, [OWSFileSystem fileSizeOfPath:path]); } } // We protect the db files here, which is somewhat redundant with what will happen in // `moveAppFilePath:` which also ensures file protection. // However that method dispatches async, since it can take a while with large attachment directories. // // Since we only have three files here it'll be quick to do it sync, and we want to make // sure it happens as part of the migration. // // FileProtection attributes move with the file, so we do it on the legacy files before moving // them. [OWSFileSystem protectFileOrFolderAtPath:self.legacyDatabaseFilePath]; [OWSFileSystem protectFileOrFolderAtPath:self.legacyDatabaseFilePath_SHM]; [OWSFileSystem protectFileOrFolderAtPath:self.legacyDatabaseFilePath_WAL]; NSError *_Nullable error = nil; if ([fileManager fileExistsAtPath:self.legacyDatabaseFilePath] && [fileManager fileExistsAtPath:self.sharedDataDatabaseFilePath]) { // In the case that we have a "database conflict" (i.e. database files // in the src and dst locations), ensure database integrity by renaming // all of the dst database files. for (NSString *filePath in @[ self.sharedDataDatabaseFilePath, self.sharedDataDatabaseFilePath_SHM, self.sharedDataDatabaseFilePath_WAL, ]) { error = [OWSFileSystem renameFilePathUsingRandomExtension:filePath]; if (error) { return error; } } } error = [OWSFileSystem moveAppFilePath:self.legacyDatabaseFilePath sharedDataFilePath:self.sharedDataDatabaseFilePath]; if (error) { return error; } error = [OWSFileSystem moveAppFilePath:self.legacyDatabaseFilePath_SHM sharedDataFilePath:self.sharedDataDatabaseFilePath_SHM]; if (error) { return error; } error = [OWSFileSystem moveAppFilePath:self.legacyDatabaseFilePath_WAL sharedDataFilePath:self.sharedDataDatabaseFilePath_WAL]; if (error) { return error; } for (NSString *path in paths) { if ([fileManager fileExistsAtPath:path]) { DDLogInfo(@"%@ after migrateToSharedData: %@, %@", self.logTag, path, [OWSFileSystem fileSizeOfPath:path]); } } return nil; } + (NSString *)databaseFilePath { DDLogVerbose(@"%@ databasePath: %@", self.logTag, OWSPrimaryStorage.sharedDataDatabaseFilePath); return self.sharedDataDatabaseFilePath; } + (NSString *)databaseFilePath_SHM { return self.sharedDataDatabaseFilePath_SHM; } + (NSString *)databaseFilePath_WAL { return self.sharedDataDatabaseFilePath_WAL; } - (NSString *)databaseFilePath { return OWSPrimaryStorage.databaseFilePath; } - (NSString *)databaseFilePath_SHM { return OWSPrimaryStorage.databaseFilePath_SHM; } - (NSString *)databaseFilePath_WAL { return OWSPrimaryStorage.databaseFilePath_WAL; } - (NSString *)databaseFilename_SHM { return OWSPrimaryStorage.databaseFilename_SHM; } - (NSString *)databaseFilename_WAL { return OWSPrimaryStorage.databaseFilename_WAL; } + (YapDatabaseConnection *)dbReadConnection { return OWSPrimaryStorage.sharedManager.dbReadConnection; } + (YapDatabaseConnection *)dbReadWriteConnection { return OWSPrimaryStorage.sharedManager.dbReadWriteConnection; } @end NS_ASSUME_NONNULL_END