Merge branch 'charlesmchen/orphanDataCleanerV2'

pull/1/head
Matthew Chen 6 years ago
commit cace335d45

@ -1 +1 @@
Subproject commit d5ef8e9a1a7dce57232dd534af17eddbca938427
Subproject commit e680d157ee32f4079cc2e126068773cd8280f118

@ -81,6 +81,7 @@
34480B641FD0A98800BC14EF /* UIView+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34480B601FD0A98800BC14EF /* UIView+OWS.m */; };
34480B671FD0AA9400BC14EF /* UIFont+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34480B651FD0AA9400BC14EF /* UIFont+OWS.m */; };
34480B681FD0AA9400BC14EF /* UIFont+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = 34480B661FD0AA9400BC14EF /* UIFont+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; };
344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */ = {isa = PBXBuildFile; fileRef = 344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */; };
344D6CEA20069E070042AF96 /* SelectRecipientViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 344D6CE620069E060042AF96 /* SelectRecipientViewController.h */; };
344D6CEB20069E070042AF96 /* SelectRecipientViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 344D6CE720069E060042AF96 /* SelectRecipientViewController.m */; };
344D6CEC20069E070042AF96 /* NewNonContactConversationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 344D6CE820069E070042AF96 /* NewNonContactConversationViewController.m */; };
@ -712,6 +713,8 @@
34480B601FD0A98800BC14EF /* UIView+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+OWS.m"; sourceTree = "<group>"; };
34480B651FD0AA9400BC14EF /* UIFont+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIFont+OWS.m"; sourceTree = "<group>"; };
34480B661FD0AA9400BC14EF /* UIFont+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIFont+OWS.h"; sourceTree = "<group>"; };
344825C4211390C700DB4BD8 /* OWSOrphanDataCleaner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOrphanDataCleaner.h; sourceTree = "<group>"; };
344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOrphanDataCleaner.m; sourceTree = "<group>"; };
34491FC11FB0F78500B3E5A3 /* my */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = my; path = translations/my.lproj/Localizable.strings; sourceTree = "<group>"; };
344D6CE620069E060042AF96 /* SelectRecipientViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SelectRecipientViewController.h; path = SignalMessaging/contacts/SelectRecipientViewController.h; sourceTree = SOURCE_ROOT; };
344D6CE720069E060042AF96 /* SelectRecipientViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SelectRecipientViewController.m; path = SignalMessaging/contacts/SelectRecipientViewController.m; sourceTree = SOURCE_ROOT; };
@ -2203,6 +2206,8 @@
340FC8CB20518C76007AEB0F /* OWSBackupJob.h */,
340FC8CC20518C76007AEB0F /* OWSBackupJob.m */,
34D2CCD120618B2F00CB1A14 /* OWSBackupLazyRestoreJob.swift */,
344825C4211390C700DB4BD8 /* OWSOrphanDataCleaner.h */,
344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */,
34D2CCD82062E7D000CB1A14 /* OWSScreenLockUI.h */,
34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */,
4579431C1E7C8CE9008ED0C0 /* Pastelog.h */,
@ -3357,6 +3362,7 @@
34D1F0881F8678AA0066283D /* ConversationViewLayout.m in Sources */,
452314A01F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift in Sources */,
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */,
344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */,
45D2AC02204885170033C692 /* OWS2FAReminderViewController.swift in Sources */,
4542DF54208D40AC007B4E76 /* LoadingViewController.swift in Sources */,
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */,

@ -12,6 +12,7 @@
#import "NotificationsManager.h"
#import "OWS2FASettingsViewController.h"
#import "OWSBackup.h"
#import "OWSOrphanDataCleaner.h"
#import "OWSScreenLockUI.h"
#import "Pastelog.h"
#import "PushManager.h"
@ -43,7 +44,6 @@
#import <SignalServiceKit/OWSIncompleteCallsJob.h>
#import <SignalServiceKit/OWSMessageManager.h>
#import <SignalServiceKit/OWSMessageSender.h>
#import <SignalServiceKit/OWSOrphanedDataCleaner.h>
#import <SignalServiceKit/OWSPrimaryStorage+Calling.h>
#import <SignalServiceKit/OWSReadReceiptManager.h>
#import <SignalServiceKit/TSAccountManager.h>
@ -149,7 +149,7 @@ static NSTimeInterval launchStartedAt;
return YES;
}
[AppVersion instance];
[AppVersion sharedInstance];
[self startupLogging];
@ -314,6 +314,7 @@ static NSTimeInterval launchStartedAt;
return NO;
}
OWSAssert(backgroundTask);
backgroundTask = nil;
return YES;
@ -325,7 +326,7 @@ static NSTimeInterval launchStartedAt;
self.didAppLaunchFail = YES;
// We perform a subset of the [application:didFinishLaunchingWithOptions:].
[AppVersion instance];
[AppVersion sharedInstance];
[self startupLogging];
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
@ -1061,7 +1062,7 @@ static NSTimeInterval launchStartedAt;
[DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self];
[AppVersion.instance mainAppLaunchDidComplete];
[AppVersion.sharedInstance mainAppLaunchDidComplete];
[Environment.current.contactsManager loadSignalAccountsFromCache];
[Environment.current.contactsManager startObserving];
@ -1090,7 +1091,7 @@ static NSTimeInterval launchStartedAt;
// TODO: Orphan cleanup is somewhat expensive - not least in doing a bunch
// of disk access. We might want to only run it "once per version"
// or something like that in production.
[OWSOrphanedDataCleaner auditAndCleanupAsync:nil];
[OWSOrphanDataCleaner auditOnLaunchIfNecessary];
#endif
[OWSProfileManager.sharedManager fetchLocalUsersProfile];

@ -53,7 +53,7 @@ class SyncPushTokensJob: NSObject {
shouldUploadTokens = true
}
if AppVersion.instance().lastAppVersion != AppVersion.instance().currentAppVersion {
if AppVersion.sharedInstance().lastAppVersion != AppVersion.sharedInstance().currentAppVersion {
Logger.info("\(self.TAG) Uploading due to fresh install or app upgrade.")
shouldUploadTokens = true
}
@ -79,7 +79,7 @@ class SyncPushTokensJob: NSObject {
return runPromise
}
// MARK - objc wrappers, since objc can't use swift parameterized types
// MARK: - objc wrappers, since objc can't use swift parameterized types
@objc class func run(accountManager: AccountManager, preferences: OWSPreferences) -> AnyPromise {
let promise: Promise<Void> = self.run(accountManager: accountManager, preferences: preferences)

@ -3,10 +3,10 @@
//
#import "DebugUIDiskUsage.h"
#import "OWSOrphanDataCleaner.h"
#import "OWSTableViewController.h"
#import "Signal-Swift.h"
#import <SignalServiceKit/NSDate+OWS.h>
#import <SignalServiceKit/OWSOrphanedDataCleaner.h>
#import <SignalServiceKit/OWSPrimaryStorage.h>
#import <SignalServiceKit/TSDatabaseView.h>
#import <SignalServiceKit/TSInteraction.h>
@ -28,11 +28,11 @@ NS_ASSUME_NONNULL_BEGIN
items:@[
[OWSTableItem itemWithTitle:@"Audit & Log"
actionBlock:^{
[OWSOrphanedDataCleaner auditAsync];
[OWSOrphanDataCleaner auditAndCleanup:NO];
}],
[OWSTableItem itemWithTitle:@"Audit & Clean Up"
actionBlock:^{
[OWSOrphanedDataCleaner auditAndCleanupAsync:nil];
[OWSOrphanDataCleaner auditAndCleanup:YES];
}],
[OWSTableItem itemWithTitle:@"Save All Attachments"
actionBlock:^{

@ -8,6 +8,7 @@
#import <SignalMessaging/OWSProfileManager.h>
#import <SignalMessaging/SignalMessaging-Swift.h>
#import <SignalServiceKit/OWSIdentityManager.h>
#import <SignalServiceKit/Threading.h>
NS_ASSUME_NONNULL_BEGIN
@ -15,6 +16,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (atomic) UIApplicationState reportedApplicationState;
@property (nonatomic, nullable) NSMutableArray<AppActiveBlock> *appActiveBlocks;
@end
#pragma mark -
@ -54,6 +57,10 @@ NS_ASSUME_NONNULL_BEGIN
name:UIApplicationWillTerminateNotification
object:nil];
// We can't use OWSSingletonAssert() since it uses the app context.
self.appActiveBlocks = [NSMutableArray new];
return self;
}
@ -108,6 +115,8 @@ NS_ASSUME_NONNULL_BEGIN
DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
[NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationDidBecomeActiveNotification object:nil];
[self runAppActiveBlocks];
}
- (void)applicationWillTerminate:(NSNotification *)notification
@ -228,6 +237,49 @@ NS_ASSUME_NONNULL_BEGIN
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:value];
}
#pragma mark -
- (void)runNowOrWhenMainAppIsActive:(AppActiveBlock)block
{
OWSAssert(block);
DispatchMainThreadSafe(^{
if (self.isMainAppAndActive) {
// App active blocks typically will be used to safely access the
// shared data container, so use a background task to protect this
// work.
OWSBackgroundTask *_Nullable backgroundTask =
[OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
block();
OWSAssert(backgroundTask);
backgroundTask = nil;
return;
}
[self.appActiveBlocks addObject:block];
});
}
- (void)runAppActiveBlocks
{
OWSAssertIsOnMainThread();
OWSAssert(self.isMainAppAndActive);
// App active blocks typically will be used to safely access the
// shared data container, so use a background task to protect this
// work.
OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
NSArray<AppActiveBlock> *appActiveBlocks = [self.appActiveBlocks copy];
[self.appActiveBlocks removeAllObjects];
for (AppActiveBlock block in appActiveBlocks) {
block();
}
OWSAssert(backgroundTask);
backgroundTask = nil;
}
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,21 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
// Notes:
//
// * On disk, we only bother cleaning up files, not directories.
@interface OWSOrphanDataCleaner : NSObject
- (instancetype)init NS_UNAVAILABLE;
// This is exposed for the debug UI.
+ (void)auditAndCleanup:(BOOL)shouldCleanup;
+ (void)auditOnLaunchIfNecessary;
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,705 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSOrphanDataCleaner.h"
#import "DateUtil.h"
#import <SignalMessaging/OWSProfileManager.h>
#import <SignalMessaging/OWSUserProfile.h>
#import <SignalServiceKit/AppReadiness.h>
#import <SignalServiceKit/AppVersion.h>
#import <SignalServiceKit/NSDate+OWS.h>
#import <SignalServiceKit/OWSContact.h>
#import <SignalServiceKit/OWSFileSystem.h>
#import <SignalServiceKit/OWSPrimaryStorage.h>
#import <SignalServiceKit/TSAttachmentStream.h>
#import <SignalServiceKit/TSInteraction.h>
#import <SignalServiceKit/TSMessage.h>
#import <SignalServiceKit/TSQuotedMessage.h>
#import <SignalServiceKit/TSThread.h>
#import <SignalServiceKit/YapDatabaseTransaction+OWS.h>
#import <YapDatabase/YapDatabase.h>
NS_ASSUME_NONNULL_BEGIN
NSString *const OWSOrphanDataCleaner_Collection = @"OWSOrphanDataCleaner_Collection";
NSString *const OWSOrphanDataCleaner_LastCleaningVersionKey = @"OWSOrphanDataCleaner_LastCleaningVersionKey";
NSString *const OWSOrphanDataCleaner_LastCleaningDateKey = @"OWSOrphanDataCleaner_LastCleaningDateKey";
@interface OWSOrphanData : NSObject
@property (nonatomic) NSSet<NSString *> *interactionIds;
@property (nonatomic) NSSet<NSString *> *attachmentIds;
@property (nonatomic) NSSet<NSString *> *filePaths;
@end
#pragma mark -
@implementation OWSOrphanData
@end
#pragma mark -
typedef void (^OrphanDataBlock)(OWSOrphanData *);
@implementation OWSOrphanDataCleaner
// Unlike CurrentAppContext().isMainAppAndActive, this method can be safely
// invoked off the main thread.
+ (BOOL)isMainAppAndActive
{
return CurrentAppContext().reportedApplicationState == UIApplicationStateActive;
}
+ (void)printPaths:(NSArray<NSString *> *)paths label:(NSString *)label
{
for (NSString *path in [paths sortedArrayUsingSelector:@selector(compare:)]) {
DDLogDebug(@"%@ %@: %@", self.logTag, label, path);
}
}
+ (long long)fileSizeOfFilePath:(NSString *)filePath
{
NSError *error;
NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error][NSFileSize];
if (error) {
if ([error.domain isEqualToString:NSCocoaErrorDomain] && error.code == 260) {
DDLogWarn(@"%@ can't find size of missing file: %@", self.logTag, filePath);
} else {
OWSProdLogAndFail(@"%@ attributesOfItemAtPath: %@ error: %@", self.logTag, filePath, error);
}
return 0;
}
return fileSize.longLongValue;
}
+ (nullable NSNumber *)fileSizeOfFilePathsSafe:(NSArray<NSString *> *)filePaths
{
long long result = 0;
for (NSString *filePath in filePaths) {
if (!self.isMainAppAndActive) {
return nil;
}
result += [self fileSizeOfFilePath:filePath];
}
return @(result);
}
+ (nullable NSSet<NSString *> *)filePathsInDirectorySafe:(NSString *)dirPath
{
NSMutableSet *filePaths = [NSMutableSet new];
if (![[NSFileManager defaultManager] fileExistsAtPath:dirPath]) {
return filePaths;
}
NSError *error;
NSArray<NSString *> *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:&error];
if (error) {
OWSProdLogAndFail(@"%@ contentsOfDirectoryAtPath error: %@", self.logTag, error);
return [NSSet new];
}
for (NSString *fileName in fileNames) {
if (!self.isMainAppAndActive) {
return nil;
}
NSString *filePath = [dirPath stringByAppendingPathComponent:fileName];
BOOL isDirectory;
[[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory];
if (isDirectory) {
NSSet<NSString *> *_Nullable dirPaths = [self filePathsInDirectorySafe:filePath];
if (!dirPaths) {
return nil;
}
[filePaths unionSet:dirPaths];
} else {
[filePaths addObject:filePath];
}
}
return filePaths;
}
// This method finds (but does not delete):
//
// * Orphan TSInteractions (with no thread).
// * Orphan TSAttachments (with no message).
// * Orphan attachment files (with no corresponding TSAttachment).
// * Orphan profile avatars.
// * Temporary files (all).
//
// It also finds (we don't clean these up).
//
// * Missing attachment files (cannot be cleaned up).
// These are attachments which have no file on disk. They should be extremely rare -
// the only cases I have seen are probably due to debugging.
// They can't be cleaned up - we don't want to delete the TSAttachmentStream or
// its corresponding message. Better that the broken message shows up in the
// conversation view.
+ (void)findOrphanDataWithRetries:(NSInteger)remainingRetries
databaseConnection:(YapDatabaseConnection *)databaseConnection
success:(OrphanDataBlock)success
failure:(dispatch_block_t)failure
{
OWSAssert(databaseConnection);
if (remainingRetries < 1) {
DDLogInfo(@"%@ Aborting orphan data search.", self.logTag);
dispatch_async(self.workQueue, ^{
failure();
});
return;
}
// Wait until the app is active...
[CurrentAppContext() runNowOrWhenMainAppIsActive:^{
// ...but perform the work off the main thread.
dispatch_async(self.workQueue, ^{
OWSOrphanData *_Nullable orphanData = [self findOrphanDataSync:databaseConnection];
if (orphanData) {
success(orphanData);
} else {
[self findOrphanDataWithRetries:remainingRetries - 1
databaseConnection:databaseConnection
success:success
failure:failure];
}
});
}];
}
// Returns nil on failure, usually indicating that the search
// aborted due to the app resigning active. This method is extremely careful to
// abort if the app resigns active, in order to avoid 0xdead10cc crashes.
+ (nullable OWSOrphanData *)findOrphanDataSync:(YapDatabaseConnection *)databaseConnection
{
OWSAssert(databaseConnection);
__block BOOL shouldAbort = NO;
// LOG_ALL_FILE_PATHS can be used to determine if there are other kinds of files
// that we're not cleaning up.
//#define LOG_ALL_FILE_PATHS
#ifdef LOG_ALL_FILE_PATHS
{
NSString *documentDirPath = [OWSFileSystem appDocumentDirectoryPath];
NSArray<NSString *> *_Nullable allDocumentFilePaths =
[self filePathsInDirectorySafe:documentDirPath].allObjects;
allDocumentFilePaths = [allDocumentFilePaths sortedArrayUsingSelector:@selector(compare:)];
NSString *attachmentsFolder = [TSAttachmentStream attachmentsFolder];
for (NSString *filePath in allDocumentFilePaths) {
if ([filePath hasPrefix:attachmentsFolder]) {
continue;
}
DDLogVerbose(@"%@ non-attachment file: %@", self.logTag, filePath);
}
}
{
NSString *documentDirPath = [OWSFileSystem appSharedDataDirectoryPath];
NSArray<NSString *> *_Nullable allDocumentFilePaths =
[self filePathsInDirectorySafe:documentDirPath].allObjects;
allDocumentFilePaths = [allDocumentFilePaths sortedArrayUsingSelector:@selector(compare:)];
NSString *attachmentsFolder = [TSAttachmentStream attachmentsFolder];
for (NSString *filePath in allDocumentFilePaths) {
if ([filePath hasPrefix:attachmentsFolder]) {
continue;
}
DDLogVerbose(@"%@ non-attachment file: %@", self.logTag, filePath);
}
}
#endif
// We treat _all_ temp files as orphan files. This is safe
// because temp files only need to be retained for the
// a single launch of the app. Since our "date threshold"
// for deletion is relative to the current launch time,
// all temp files currently in use should be safe.
NSString *temporaryDirectory = NSTemporaryDirectory();
NSArray<NSString *> *_Nullable tempFilePaths = [self filePathsInDirectorySafe:temporaryDirectory].allObjects;
if (!tempFilePaths || !self.isMainAppAndActive) {
return nil;
}
#ifdef LOG_ALL_FILE_PATHS
{
NSDateFormatter *dateFormatter = [NSDateFormatter new];
[dateFormatter setDateStyle:NSDateFormatterLongStyle];
[dateFormatter setTimeStyle:NSDateFormatterLongStyle];
tempFilePaths = [tempFilePaths sortedArrayUsingSelector:@selector(compare:)];
for (NSString *filePath in tempFilePaths) {
NSError *error;
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
if (!attributes || error) {
OWSProdLogAndFail(@"%@ Could not get attributes of file at: %@", self.logTag, filePath);
continue;
}
DDLogVerbose(@"%@ temp file: %@, %@",
self.logTag,
filePath,
[dateFormatter stringFromDate:attributes.fileModificationDate]);
}
}
#endif
NSString *legacyAttachmentsDirPath = TSAttachmentStream.legacyAttachmentsDirPath;
NSString *sharedDataAttachmentsDirPath = TSAttachmentStream.sharedDataAttachmentsDirPath;
NSSet<NSString *> *_Nullable legacyAttachmentFilePaths = [self filePathsInDirectorySafe:legacyAttachmentsDirPath];
if (!legacyAttachmentFilePaths || !self.isMainAppAndActive) {
return nil;
}
NSSet<NSString *> *_Nullable sharedDataAttachmentFilePaths =
[self filePathsInDirectorySafe:sharedDataAttachmentsDirPath];
if (!sharedDataAttachmentFilePaths || !self.isMainAppAndActive) {
return nil;
}
NSString *legacyProfileAvatarsDirPath = OWSUserProfile.legacyProfileAvatarsDirPath;
NSString *sharedDataProfileAvatarsDirPath = OWSUserProfile.sharedDataProfileAvatarsDirPath;
NSSet<NSString *> *_Nullable legacyProfileAvatarsFilePaths =
[self filePathsInDirectorySafe:legacyProfileAvatarsDirPath];
if (!legacyProfileAvatarsFilePaths || !self.isMainAppAndActive) {
return nil;
}
NSSet<NSString *> *_Nullable sharedDataProfileAvatarFilePaths =
[self filePathsInDirectorySafe:sharedDataProfileAvatarsDirPath];
if (!sharedDataProfileAvatarFilePaths || !self.isMainAppAndActive) {
return nil;
}
NSMutableSet<NSString *> *allOnDiskFilePaths = [NSMutableSet new];
[allOnDiskFilePaths unionSet:legacyAttachmentFilePaths];
[allOnDiskFilePaths unionSet:sharedDataAttachmentFilePaths];
[allOnDiskFilePaths unionSet:legacyProfileAvatarsFilePaths];
[allOnDiskFilePaths unionSet:sharedDataProfileAvatarFilePaths];
[allOnDiskFilePaths addObjectsFromArray:tempFilePaths];
NSSet<NSString *> *profileAvatarFilePaths = [OWSUserProfile allProfileAvatarFilePaths];
if (!self.isMainAppAndActive) {
return nil;
}
NSNumber *_Nullable totalFileSize = [self fileSizeOfFilePathsSafe:allOnDiskFilePaths.allObjects];
if (!totalFileSize || !self.isMainAppAndActive) {
return nil;
}
NSUInteger fileCount = allOnDiskFilePaths.count;
// Attachments
__block int attachmentStreamCount = 0;
NSMutableSet<NSString *> *allAttachmentFilePaths = [NSMutableSet new];
NSMutableSet<NSString *> *allAttachmentIds = [NSMutableSet new];
// Threads
__block NSSet *threadIds;
// Messages
NSMutableSet<NSString *> *orphanInteractionIds = [NSMutableSet new];
NSMutableSet<NSString *> *messageAttachmentIds = [NSMutableSet new];
NSMutableSet<NSString *> *quotedReplyThumbnailAttachmentIds = [NSMutableSet new];
NSMutableSet<NSString *> *contactShareAvatarAttachmentIds = [NSMutableSet new];
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[transaction
enumerateKeysAndObjectsInCollection:TSAttachmentStream.collection
usingBlock:^(NSString *key, TSAttachment *attachment, BOOL *stop) {
if (!self.isMainAppAndActive) {
shouldAbort = YES;
*stop = YES;
return;
}
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
return;
}
[allAttachmentIds addObject:attachment.uniqueId];
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
attachmentStreamCount++;
NSString *_Nullable filePath = [attachmentStream filePath];
if (filePath) {
[allAttachmentFilePaths addObject:filePath];
} else {
OWSProdLogAndFail(@"%@ attachment has no file path.", self.logTag);
}
NSString *_Nullable thumbnailPath = [attachmentStream thumbnailPath];
if (thumbnailPath.length > 0) {
[allAttachmentFilePaths addObject:thumbnailPath];
}
}];
if (shouldAbort) {
return;
}
threadIds = [NSSet setWithArray:[transaction allKeysInCollection:TSThread.collection]];
[transaction
enumerateKeysAndObjectsInCollection:TSMessage.collection
usingBlock:^(NSString *key, TSInteraction *interaction, BOOL *stop) {
if (!self.isMainAppAndActive) {
shouldAbort = YES;
*stop = YES;
return;
}
if (interaction.uniqueThreadId.length < 1
|| ![threadIds containsObject:interaction.uniqueThreadId]) {
[orphanInteractionIds addObject:interaction.uniqueId];
}
if (![interaction isKindOfClass:[TSMessage class]]) {
return;
}
TSMessage *message = (TSMessage *)interaction;
if (message.attachmentIds.count > 0) {
[messageAttachmentIds addObjectsFromArray:message.attachmentIds];
}
TSQuotedMessage *_Nullable quotedMessage = message.quotedMessage;
if (quotedMessage) {
[quotedReplyThumbnailAttachmentIds
addObjectsFromArray:quotedMessage.thumbnailAttachmentStreamIds];
}
OWSContact *_Nullable contactShare = message.contactShare;
if (contactShare && contactShare.avatarAttachmentId) {
[contactShareAvatarAttachmentIds
addObject:contactShare.avatarAttachmentId];
}
}];
}];
if (shouldAbort) {
return nil;
}
DDLogDebug(@"%@ fileCount: %zu", self.logTag, fileCount);
DDLogDebug(@"%@ totalFileSize: %lld", self.logTag, totalFileSize.longLongValue);
DDLogDebug(@"%@ attachmentStreams: %d", self.logTag, attachmentStreamCount);
DDLogDebug(@"%@ attachmentStreams with file paths: %zu", self.logTag, allAttachmentFilePaths.count);
NSMutableSet<NSString *> *orphanFilePaths = [allOnDiskFilePaths mutableCopy];
[orphanFilePaths minusSet:allAttachmentFilePaths];
[orphanFilePaths minusSet:profileAvatarFilePaths];
NSMutableSet<NSString *> *missingAttachmentFilePaths = [allAttachmentFilePaths mutableCopy];
[missingAttachmentFilePaths minusSet:allOnDiskFilePaths];
DDLogDebug(@"%@ orphan file paths: %zu", self.logTag, orphanFilePaths.count);
DDLogDebug(@"%@ missing attachment file paths: %zu", self.logTag, missingAttachmentFilePaths.count);
[self printPaths:orphanFilePaths.allObjects label:@"orphan file paths"];
[self printPaths:missingAttachmentFilePaths.allObjects label:@"missing attachment file paths"];
DDLogDebug(@"%@ attachmentIds: %zu", self.logTag, allAttachmentIds.count);
DDLogDebug(@"%@ messageAttachmentIds: %zu", self.logTag, messageAttachmentIds.count);
DDLogDebug(@"%@ quotedReplyThumbnailAttachmentIds: %zu", self.logTag, quotedReplyThumbnailAttachmentIds.count);
DDLogDebug(@"%@ contactShareAvatarAttachmentIds: %zu", self.logTag, contactShareAvatarAttachmentIds.count);
NSMutableSet<NSString *> *orphanAttachmentIds = [allAttachmentIds mutableCopy];
[orphanAttachmentIds minusSet:messageAttachmentIds];
[orphanAttachmentIds minusSet:quotedReplyThumbnailAttachmentIds];
[orphanAttachmentIds minusSet:contactShareAvatarAttachmentIds];
NSMutableSet<NSString *> *missingAttachmentIds = [messageAttachmentIds mutableCopy];
[missingAttachmentIds minusSet:allAttachmentIds];
DDLogDebug(@"%@ orphan attachmentIds: %zu", self.logTag, orphanAttachmentIds.count);
DDLogDebug(@"%@ missing attachmentIds: %zu", self.logTag, missingAttachmentIds.count);
DDLogDebug(@"%@ orphan interactions: %zu", self.logTag, orphanInteractionIds.count);
OWSOrphanData *result = [OWSOrphanData new];
result.interactionIds = [orphanInteractionIds copy];
result.attachmentIds = [orphanAttachmentIds copy];
result.filePaths = [orphanFilePaths copy];
return result;
}
+ (void)auditOnLaunchIfNecessary
{
OWSAssertIsOnMainThread();
// In production, do not audit or clean up.
#ifndef DEBUG
return;
#endif
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
YapDatabaseConnection *databaseConnection = primaryStorage.dbReadWriteConnection;
__block NSString *_Nullable lastCleaningVersion;
__block NSDate *_Nullable lastCleaningDate;
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
lastCleaningVersion = [transaction stringForKey:OWSOrphanDataCleaner_LastCleaningVersionKey
inCollection:OWSOrphanDataCleaner_Collection];
lastCleaningDate = [transaction dateForKey:OWSOrphanDataCleaner_LastCleaningDateKey
inCollection:OWSOrphanDataCleaner_Collection];
}];
// Only clean up once per app version.
NSString *currentAppVersion = AppVersion.sharedInstance.currentAppVersion;
if (lastCleaningVersion && [lastCleaningVersion isEqualToString:currentAppVersion]) {
DDLogVerbose(@"%@ skipping orphan data cleanup; already done on %@.", self.logTag, currentAppVersion);
return;
}
// Only clean up once per day.
if (lastCleaningDate && [DateUtil dateIsToday:lastCleaningDate]) {
DDLogVerbose(@"%@ skipping orphan data cleanup; already done today.", self.logTag);
return;
}
// If we want to be cautious, we can disable orphan deletion using
// flag - the cleanup will just be a dry run with logging.
BOOL shouldRemoveOrphans = NO;
[self auditAndCleanup:shouldRemoveOrphans databaseConnection:databaseConnection];
}
+ (void)auditAndCleanup:(BOOL)shouldRemoveOrphans
{
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
YapDatabaseConnection *databaseConnection = primaryStorage.dbReadWriteConnection;
[self auditAndCleanup:shouldRemoveOrphans databaseConnection:databaseConnection];
}
// We use the lowest priority possible.
+ (dispatch_queue_t)workQueue
{
return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
}
+ (void)auditAndCleanup:(BOOL)shouldRemoveOrphans databaseConnection:(YapDatabaseConnection *)databaseConnection
{
OWSAssertIsOnMainThread();
OWSAssert(databaseConnection);
if (!AppReadiness.isAppReady) {
OWSProdLogAndFail(@"%@ can't audit orphan data until app is ready.", self.logTag);
return;
}
if (!CurrentAppContext().isMainApp) {
OWSProdLogAndFail(@"%@ can't audit orphan data in app extensions.", self.logTag);
return;
}
// Orphan cleanup has two risks:
//
// * As a long-running process that involves access to the
// shared data container, it could cause 0xdead10cc.
// * It could accidentally delete data still in use,
// e.g. a profile avatar which has been saved to disk
// but whose OWSUserProfile hasn't been saved yet.
//
// To prevent 0xdead10cc, the cleaner continually checks
// whether the app has resigned active. If so, it aborts.
// Each phase (search, re-search, processing) retries N times,
// then gives up until the next app launch.
//
// To prevent accidental data deletion, we take the following
// measures:
//
// * Only cleanup data of the following types (which should
// include all relevant app data): profile avatar,
// attachment, temporary files (including temporary
// attachments).
// * We don't delete any data created more recently than N seconds
// _before_ when the app launched. This prevents any stray data
// currently in use by the app from being accidentally cleaned
// up.
const NSInteger kMaxRetries = 3;
[self findOrphanDataWithRetries:kMaxRetries
databaseConnection:databaseConnection
success:^(OWSOrphanData *orphanData) {
[self processOrphans:orphanData
remainingRetries:kMaxRetries
databaseConnection:databaseConnection
shouldRemoveOrphans:shouldRemoveOrphans
success:^{
DDLogInfo(@"%@ Completed orphan data cleanup.", self.logTag);
[databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction setObject:AppVersion.sharedInstance.currentAppVersion
forKey:OWSOrphanDataCleaner_LastCleaningVersionKey
inCollection:OWSOrphanDataCleaner_Collection];
[transaction setDate:[NSDate new]
forKey:OWSOrphanDataCleaner_LastCleaningDateKey
inCollection:OWSOrphanDataCleaner_Collection];
}];
}
failure:^{
DDLogInfo(@"%@ Aborting orphan data cleanup.", self.logTag);
}];
}
failure:^{
DDLogInfo(@"%@ Aborting orphan data cleanup.", self.logTag);
}];
}
// Returns NO on failure, usually indicating that orphan processing
// aborted due to the app resigning active. This method is extremely careful to
// abort if the app resigns active, in order to avoid 0xdead10cc crashes.
+ (void)processOrphans:(OWSOrphanData *)orphanData
remainingRetries:(NSInteger)remainingRetries
databaseConnection:(YapDatabaseConnection *)databaseConnection
shouldRemoveOrphans:(BOOL)shouldRemoveOrphans
success:(dispatch_block_t)success
failure:(dispatch_block_t)failure
{
OWSAssert(databaseConnection);
OWSAssert(orphanData);
if (remainingRetries < 1) {
DDLogInfo(@"%@ Aborting orphan data audit.", self.logTag);
dispatch_async(self.workQueue, ^{
failure();
});
return;
}
// Wait until the app is active...
[CurrentAppContext() runNowOrWhenMainAppIsActive:^{
// ...but perform the work off the main thread.
dispatch_async(self.workQueue, ^{
if ([self processOrphansSync:orphanData
databaseConnection:databaseConnection
shouldRemoveOrphans:shouldRemoveOrphans]) {
success();
return;
} else {
[self processOrphans:orphanData
remainingRetries:remainingRetries - 1
databaseConnection:databaseConnection
shouldRemoveOrphans:shouldRemoveOrphans
success:success
failure:failure];
}
});
}];
}
// Returns NO on failure, usually indicating that orphan processing
// aborted due to the app resigning active. This method is extremely careful to
// abort if the app resigns active, in order to avoid 0xdead10cc crashes.
+ (BOOL)processOrphansSync:(OWSOrphanData *)orphanData
databaseConnection:(YapDatabaseConnection *)databaseConnection
shouldRemoveOrphans:(BOOL)shouldRemoveOrphans
{
OWSAssert(databaseConnection);
OWSAssert(orphanData);
__block BOOL shouldAbort = NO;
// We need to avoid cleaning up new attachments and files that are still in the process of
// being created/written, so we don't clean up anything recent.
const NSTimeInterval kMinimumOrphanAgeSeconds = CurrentAppContext().isRunningTests ? 0.f : 15 * kMinuteInterval;
NSDate *appLaunchTime = AppVersion.sharedInstance.appLaunchTime;
NSTimeInterval thresholdTimestamp = appLaunchTime.timeIntervalSince1970 - kMinimumOrphanAgeSeconds;
NSDate *thresholdDate = [NSDate dateWithTimeIntervalSince1970:thresholdTimestamp];
[databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSUInteger interactionsRemoved = 0;
for (NSString *interactionId in orphanData.interactionIds) {
if (!self.isMainAppAndActive) {
shouldAbort = YES;
return;
}
TSInteraction *_Nullable interaction =
[TSInteraction fetchObjectWithUniqueID:interactionId transaction:transaction];
if (!interaction) {
// This could just be a race condition, but it should be very unlikely.
DDLogWarn(@"%@ Could not load interaction: %@", self.logTag, interactionId);
continue;
}
// Don't delete interactions which were created in the last N minutes.
NSDate *creationDate = [NSDate ows_dateWithMillisecondsSince1970:interaction.timestamp];
if ([creationDate isAfterDate:thresholdDate]) {
DDLogInfo(@"%@ Skipping orphan interaction due to age: %f",
self.logTag,
fabs(creationDate.timeIntervalSinceNow));
continue;
}
DDLogInfo(@"%@ Removing orphan message: %@", self.logTag, interaction.uniqueId);
interactionsRemoved++;
if (!shouldRemoveOrphans) {
continue;
}
[interaction removeWithTransaction:transaction];
}
DDLogInfo(@"%@ Deleted orphan interactions: %zu", self.logTag, interactionsRemoved);
NSUInteger attachmentsRemoved = 0;
for (NSString *attachmentId in orphanData.attachmentIds) {
if (!self.isMainAppAndActive) {
shouldAbort = YES;
return;
}
TSAttachment *_Nullable attachment =
[TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
if (!attachment) {
// This can happen on launch since we sync contacts/groups, especially if you have a lot of attachments
// to churn through, it's likely it's been deleted since starting this job.
DDLogWarn(@"%@ Could not load attachment: %@", self.logTag, attachmentId);
continue;
}
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
continue;
}
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
// Don't delete attachments which were created in the last N minutes.
NSDate *creationDate = attachmentStream.creationTimestamp;
if ([creationDate isAfterDate:thresholdDate]) {
DDLogInfo(@"%@ Skipping orphan attachment due to age: %f",
self.logTag,
fabs(creationDate.timeIntervalSinceNow));
continue;
}
DDLogInfo(@"%@ Removing orphan attachmentStream: %@", self.logTag, attachmentStream.uniqueId);
attachmentsRemoved++;
if (!shouldRemoveOrphans) {
continue;
}
[attachmentStream removeWithTransaction:transaction];
}
DDLogInfo(@"%@ Deleted orphan attachments: %zu", self.logTag, attachmentsRemoved);
}];
if (shouldAbort) {
return nil;
}
NSUInteger filesRemoved = 0;
NSArray<NSString *> *filePaths = [orphanData.filePaths.allObjects sortedArrayUsingSelector:@selector(compare:)];
for (NSString *filePath in filePaths) {
if (!self.isMainAppAndActive) {
return nil;
}
NSError *error;
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
if (!attributes || error) {
OWSProdLogAndFail(@"%@ Could not get attributes of file at: %@", self.logTag, filePath);
continue;
}
// Don't delete files which were created in the last N minutes.
NSDate *creationDate = attributes.fileModificationDate;
if ([creationDate isAfterDate:thresholdDate]) {
DDLogInfo(@"%@ Skipping orphan attachment file due to age: %f",
self.logTag,
fabs([creationDate timeIntervalSinceNow]));
continue;
}
DDLogInfo(@"%@ Deleting orphan attachment file: %@", self.logTag, filePath);
filesRemoved++;
if (!shouldRemoveOrphans) {
continue;
}
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
if (error) {
OWSProdLogAndFail(@"%@ Could not remove orphan file at: %@", self.logTag, filePath);
}
}
DDLogInfo(@"%@ Deleted orphan attachment files: %zu", self.logTag, filesRemoved);
return YES;
}
@end
NS_ASSUME_NONNULL_END

@ -58,6 +58,7 @@ NS_ASSUME_NONNULL_BEGIN
migrationCompletion();
OWSAssert(backgroundTask);
backgroundTask = nil;
}];
}];

@ -1,5 +1,5 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "Environment.h"

@ -40,8 +40,8 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert([Environment current]);
OWSAssert(completion);
NSString *previousVersion = AppVersion.instance.lastAppVersion;
NSString *currentVersion = AppVersion.instance.currentAppVersion;
NSString *previousVersion = AppVersion.sharedInstance.lastAppVersion;
NSString *currentVersion = AppVersion.sharedInstance.currentAppVersion;
DDLogInfo(@"%@ Checking migrations. currentVersion: %@, lastRanVersion: %@",
self.logTag,

@ -69,8 +69,11 @@ extern NSString *const kLocalProfileUniqueId;
+ (NSString *)profileAvatarFilepathWithFilename:(NSString *)filename;
+ (nullable NSError *)migrateToSharedData;
+ (NSString *)legacyProfileAvatarsDirPath;
+ (NSString *)sharedDataProfileAvatarsDirPath;
+ (NSString *)profileAvatarsDirPath;
+ (void)resetProfileStorage;
+ (NSSet<NSString *> *)allProfileAvatarFilePaths;
@end

@ -12,6 +12,7 @@
#import <SignalServiceKit/TSAccountManager.h>
#import <YapDatabase/YapDatabaseConnection.h>
#import <YapDatabase/YapDatabaseTransaction.h>
#import <SignalServiceKit/OWSPrimaryStorage.h>
NS_ASSUME_NONNULL_BEGIN
@ -437,6 +438,33 @@ NSString *const kLocalProfileUniqueId = @"kLocalProfileUniqueId";
}
}
+ (NSSet<NSString *> *)allProfileAvatarFilePaths
{
NSString *profileAvatarsDirPath = self.profileAvatarsDirPath;
NSMutableSet<NSString *> *profileAvatarFilePaths = [NSMutableSet new];
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[OWSUserProfile
enumerateCollectionObjectsWithTransaction:transaction
usingBlock:^(id object, BOOL *stop) {
if (![object isKindOfClass:[OWSUserProfile class]]) {
OWSProdLogAndFail(@"%@ unexpected object in user profiles: %@",
self.logTag,
[object class]);
return;
}
OWSUserProfile *userProfile = object;
if (!userProfile.avatarFileName) {
return;
}
NSString *filePath = [profileAvatarsDirPath
stringByAppendingPathComponent:userProfile.avatarFileName];
[profileAvatarFilePaths addObject:filePath];
}];
}];
return [profileAvatarFilePaths copy];
}
@end
NS_ASSUME_NONNULL_END

@ -88,7 +88,7 @@ import Foundation
// Don't show the nag to users who have just launched
// the app for the first time.
guard AppVersion.instance().lastAppVersion != nil else {
guard AppVersion.sharedInstance().lastAppVersion != nil else {
return
}

@ -122,7 +122,8 @@ static const CGFloat kAttachmentDownloadProgressTheta = 0.001f;
{
OWSAssert(transaction);
__block OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
__block OWSBackgroundTask *_Nullable backgroundTask =
[OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
[self setAttachment:attachment isDownloadingInMessage:message transaction:transaction];
@ -132,6 +133,7 @@ static const CGFloat kAttachmentDownloadProgressTheta = 0.001f;
[self setAttachment:attachment didFailInMessage:message error:error];
failureHandler(error);
OWSAssert(backgroundTask);
backgroundTask = nil;
});
};
@ -144,6 +146,7 @@ static const CGFloat kAttachmentDownloadProgressTheta = 0.001f;
[message touch];
}
OWSAssert(backgroundTask);
backgroundTask = nil;
});
};

@ -62,7 +62,10 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable NSString *)readOversizeText;
+ (void)deleteAttachments;
+ (NSString *)attachmentsFolder;
+ (NSString *)legacyAttachmentsDirPath;
+ (NSString *)sharedDataAttachmentsDirPath;
- (BOOL)shouldHaveImageSize;
- (CGSize)imageSize;

@ -110,10 +110,6 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value)
#pragma mark Date operations
- (uint64_t)millisecondsTimestamp {
return self.timestamp;
}
- (NSDate *)dateForSorting
{
return [NSDate ows_dateWithMillisecondsSince1970:self.timestampForSorting];

@ -355,12 +355,13 @@ NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJo
return;
}
OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
NSArray<OWSMessageContentJob *> *processedJobs = [self processJobs:batchJobs];
[self.finder removeJobsWithIds:processedJobs.uniqueIds];
OWSAssert(backgroundTask);
backgroundTask = nil;
DDLogVerbose(@"%@ completed %lu/%lu jobs. %lu jobs left.",

@ -137,6 +137,7 @@ void AssertIsOnDisappearingMessagesQueue()
DDLogDebug(@"%@ Removed %lu expired messages", self.logTag, (unsigned long)expirationCount);
OWSAssert(backgroundTask);
backgroundTask = nil;
return expirationCount;
}
@ -223,8 +224,8 @@ void AssertIsOnDisappearingMessagesQueue()
OWSAssert(timestampForSorting > 0);
OWSAssert(transaction);
__block OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
// Become eventually consistent in the case that the remote changed their settings at the same time.
// Also in case remote doesn't support expiring messages
OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration =
@ -256,6 +257,7 @@ void AssertIsOnDisappearingMessagesQueue()
createdInExistingGroup:createdInExistingGroup];
[infoMessage saveWithTransaction:transaction];
OWSAssert(backgroundTask);
backgroundTask = nil;
}

@ -307,7 +307,8 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
return;
}
__block OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
__block OWSBackgroundTask *_Nullable backgroundTask =
[OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
[self processJob:job
completion:^(BOOL success) {
@ -317,6 +318,7 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
success ? @"decrypted" : @"failed to decrypt",
(unsigned long)[OWSMessageDecryptJob numberOfKeysInCollection]);
[self drainQueueWorkStep];
OWSAssert(backgroundTask);
backgroundTask = nil;
}];
}

@ -732,7 +732,8 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_
if ([message.path isEqualToString:@"/api/v1/message"] && [message.verb isEqualToString:@"PUT"]) {
__block OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
__block OWSBackgroundTask *_Nullable backgroundTask =
[OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@try {
@ -743,6 +744,7 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_
if (!decryptedPayload) {
DDLogWarn(@"%@ Failed to decrypt incoming payload or bad HMAC", self.logTag);
[self sendWebSocketMessageAcknowledgement:message];
OWSAssert(backgroundTask);
backgroundTask = nil;
return;
}
@ -762,6 +764,7 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_
dispatch_async(dispatch_get_main_queue(), ^{
[self sendWebSocketMessageAcknowledgement:message];
OWSAssert(backgroundTask);
backgroundTask = nil;
});
});

@ -52,7 +52,7 @@ NSString *const OWSIncomingMessageFinderColumnSourceDeviceId = @"OWSIncomingMess
- (YapDatabaseConnection *)dbConnection
{
@synchronized (self) {
@synchronized(self) {
if (!_dbConnection) {
_dbConnection = [self.primaryStorage newDatabaseConnection];
}

@ -1,30 +0,0 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
// Notes:
//
// * On disk, we only bother cleaning up files, not directories.
// * For code simplicity, we don't guarantee that everything is
// cleaned up in a single pass. If an interaction is cleaned up,
// it's attachments might not be cleaned up until the next pass.
// If an attachment is cleaned up, it's file on disk might not
// be cleaned up until the next pass.
@interface OWSOrphanedDataCleaner : NSObject
- (instancetype)init NS_UNAVAILABLE;
+ (void)auditAsync;
// completion, if present, will be invoked on the main thread.
+ (void)auditAndCleanupAsync:(void (^_Nullable)(void))completion;
+ (NSSet<NSString *> *)filePathsInAttachmentsFolder;
+ (long long)fileSizeOfFilePaths:(NSArray<NSString *> *)filePaths;
@end
NS_ASSUME_NONNULL_END

@ -1,286 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSOrphanedDataCleaner.h"
#import "NSDate+OWS.h"
#import "OWSContact.h"
#import "OWSPrimaryStorage.h"
#import "TSAttachmentStream.h"
#import "TSInteraction.h"
#import "TSMessage.h"
#import "TSQuotedMessage.h"
#import "TSThread.h"
#import <YapDatabase/YapDatabase.h>
NS_ASSUME_NONNULL_BEGIN
@implementation OWSOrphanedDataCleaner
+ (void)auditAsync
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[OWSOrphanedDataCleaner auditAndCleanup:NO completion:nil];
});
}
+ (void)auditAndCleanupAsync:(void (^_Nullable)(void))completion
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[OWSOrphanedDataCleaner auditAndCleanup:YES completion:completion];
});
}
// This method finds and optionally cleans up:
//
// * Orphan messages (with no thread).
// * Orphan attachments (with no message).
// * Orphan attachment files (with no attachment).
// * Missing attachment files (cannot be cleaned up).
// These are attachments which have no file on disk. They should be extremely rare -
// the only cases I have seen are probably due to debugging.
// They can't be cleaned up - we don't want to delete the TSAttachmentStream or
// its corresponding message. Better that the broken message shows up in the
// conversation view.
+ (void)auditAndCleanup:(BOOL)shouldCleanup completion:(void (^_Nullable)(void))completion
{
NSSet<NSString *> *diskFilePaths = [self filePathsInAttachmentsFolder];
long long totalFileSize = [self fileSizeOfFilePaths:diskFilePaths.allObjects];
NSUInteger fileCount = diskFilePaths.count;
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
YapDatabaseConnection *databaseConnection = primaryStorage.newDatabaseConnection;
__block int attachmentStreamCount = 0;
NSMutableSet<NSString *> *attachmentFilePaths = [NSMutableSet new];
NSMutableSet<NSString *> *attachmentIds = [NSMutableSet new];
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
[transaction enumerateKeysAndObjectsInCollection:TSAttachmentStream.collection
usingBlock:^(NSString *key, TSAttachment *attachment, BOOL *stop) {
[attachmentIds addObject:attachment.uniqueId];
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
return;
}
TSAttachmentStream *attachmentStream
= (TSAttachmentStream *)attachment;
attachmentStreamCount++;
NSString *_Nullable filePath = [attachmentStream filePath];
OWSAssert(filePath);
[attachmentFilePaths addObject:filePath];
NSString *_Nullable thumbnailPath = [attachmentStream thumbnailPath];
if (thumbnailPath.length > 0) {
[attachmentFilePaths addObject:thumbnailPath];
}
}];
}];
DDLogDebug(@"%@ fileCount: %lu", self.logTag, (unsigned long)fileCount);
DDLogDebug(@"%@ totalFileSize: %lld", self.logTag, totalFileSize);
DDLogDebug(@"%@ attachmentStreams: %d", self.logTag, attachmentStreamCount);
DDLogDebug(@"%@ attachmentStreams with file paths: %lu", self.logTag, (unsigned long)attachmentFilePaths.count);
NSMutableSet<NSString *> *orphanDiskFilePaths = [diskFilePaths mutableCopy];
[orphanDiskFilePaths minusSet:attachmentFilePaths];
NSMutableSet<NSString *> *missingAttachmentFilePaths = [attachmentFilePaths mutableCopy];
[missingAttachmentFilePaths minusSet:diskFilePaths];
DDLogDebug(@"%@ orphan disk file paths: %lu", self.logTag, (unsigned long)orphanDiskFilePaths.count);
DDLogDebug(@"%@ missing attachment file paths: %lu", self.logTag, (unsigned long)missingAttachmentFilePaths.count);
[self printPaths:orphanDiskFilePaths.allObjects label:@"orphan disk file paths"];
[self printPaths:missingAttachmentFilePaths.allObjects label:@"missing attachment file paths"];
__block NSMutableSet *threadIds;
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
threadIds = [[NSMutableSet alloc] initWithArray:[transaction allKeysInCollection:TSThread.collection]];
}];
NSMutableSet<NSString *> *orphanInteractionIds = [NSMutableSet new];
NSMutableSet<NSString *> *messageAttachmentIds = [NSMutableSet new];
NSMutableSet<NSString *> *quotedReplyThumbnailAttachmentIds = [NSMutableSet new];
NSMutableSet<NSString *> *contactShareAvatarAttachmentIds = [NSMutableSet new];
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
[transaction enumerateKeysAndObjectsInCollection:TSMessage.collection
usingBlock:^(NSString *key, TSInteraction *interaction, BOOL *stop) {
if (![threadIds containsObject:interaction.uniqueThreadId]) {
[orphanInteractionIds addObject:interaction.uniqueId];
}
if (![interaction isKindOfClass:[TSMessage class]]) {
return;
}
TSMessage *message = (TSMessage *)interaction;
if (message.attachmentIds.count > 0) {
[messageAttachmentIds addObjectsFromArray:message.attachmentIds];
}
TSQuotedMessage *_Nullable quotedMessage = message.quotedMessage;
if (quotedMessage) {
[quotedReplyThumbnailAttachmentIds
addObjectsFromArray:quotedMessage
.thumbnailAttachmentStreamIds];
}
OWSContact *_Nullable contactShare = message.contactShare;
if (contactShare && contactShare.avatarAttachmentId) {
[contactShareAvatarAttachmentIds
addObject:contactShare.avatarAttachmentId];
}
}];
}];
DDLogDebug(@"%@ attachmentIds: %lu", self.logTag, (unsigned long)attachmentIds.count);
DDLogDebug(@"%@ messageAttachmentIds: %lu", self.logTag, (unsigned long)messageAttachmentIds.count);
DDLogDebug(@"%@ quotedReplyThumbnailAttachmentIds: %lu",
self.logTag,
(unsigned long)quotedReplyThumbnailAttachmentIds.count);
DDLogDebug(
@"%@ contactShareAvatarAttachmentIds: %lu", self.logTag, (unsigned long)contactShareAvatarAttachmentIds.count);
NSMutableSet<NSString *> *orphanAttachmentIds = [attachmentIds mutableCopy];
[orphanAttachmentIds minusSet:messageAttachmentIds];
[orphanAttachmentIds minusSet:quotedReplyThumbnailAttachmentIds];
[orphanAttachmentIds minusSet:contactShareAvatarAttachmentIds];
NSMutableSet<NSString *> *missingAttachmentIds = [messageAttachmentIds mutableCopy];
[missingAttachmentIds minusSet:attachmentIds];
DDLogDebug(@"%@ orphan attachmentIds: %lu", self.logTag, (unsigned long)orphanAttachmentIds.count);
DDLogDebug(@"%@ missing attachmentIds: %lu", self.logTag, (unsigned long)missingAttachmentIds.count);
DDLogDebug(@"%@ orphan interactions: %lu", self.logTag, (unsigned long)orphanInteractionIds.count);
// We need to avoid cleaning up new attachments and files that are still in the process of
// being created/written, so we don't clean up anything recent.
const NSTimeInterval kMinimumOrphanAge = CurrentAppContext().isRunningTests ? 0.f : 15 * kMinuteInterval;
if (!shouldCleanup) {
return;
}
[databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
for (NSString *interactionId in orphanInteractionIds) {
TSInteraction *interaction = [TSInteraction fetchObjectWithUniqueID:interactionId transaction:transaction];
if (!interaction) {
// This could just be a race condition, but it should be very unlikely.
OWSFail(@"%@ Could not load interaction: %@", self.logTag, interactionId);
continue;
}
DDLogInfo(@"%@ Removing orphan message: %@", self.logTag, interaction.uniqueId);
[interaction removeWithTransaction:transaction];
}
for (NSString *attachmentId in orphanAttachmentIds) {
TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
if (!attachment) {
// This can happen on launch since we sync contacts/groups, especially if you have a lot of attachments
// to churn through, it's likely it's been deleted since starting this job.
DDLogWarn(@"%@ Could not load attachment: %@", self.logTag, attachmentId);
continue;
}
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
continue;
}
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
// Don't delete attachments which were created in the last N minutes.
if (fabs([attachmentStream.creationTimestamp timeIntervalSinceNow]) < kMinimumOrphanAge) {
DDLogInfo(@"%@ Skipping orphan attachment due to age: %f",
self.logTag,
fabs([attachmentStream.creationTimestamp timeIntervalSinceNow]));
continue;
}
DDLogInfo(@"%@ Removing orphan attachmentStream from DB: %@", self.logTag, attachmentStream.uniqueId);
[attachmentStream removeWithTransaction:transaction];
}
}];
for (NSString *filePath in orphanDiskFilePaths) {
NSError *error;
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
if (!attributes || error) {
OWSFail(@"%@ Could not get attributes of file at: %@", self.logTag, filePath);
continue;
}
// Don't delete files which were created in the last N minutes.
if (fabs([attributes.fileModificationDate timeIntervalSinceNow]) < kMinimumOrphanAge) {
DDLogInfo(@"%@ Skipping orphan attachment file due to age: %f",
self.logTag,
fabs([attributes.fileModificationDate timeIntervalSinceNow]));
continue;
}
DDLogInfo(@"%@ Deleting orphan attachment file: %@", self.logTag, filePath);
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
if (error) {
OWSFail(@"%@ Could not remove orphan file at: %@", self.logTag, filePath);
}
}
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
}
+ (void)printPaths:(NSArray<NSString *> *)paths label:(NSString *)label
{
for (NSString *path in [paths sortedArrayUsingSelector:@selector(compare:)]) {
DDLogDebug(@"%@ %@: %@", self.logTag, label, path);
}
}
+ (NSSet<NSString *> *)filePathsInAttachmentsFolder
{
NSString *attachmentsFolder = [TSAttachmentStream attachmentsFolder];
DDLogDebug(@"%@ attachmentsFolder: %@", self.logTag, attachmentsFolder);
return [self filePathsInDirectory:attachmentsFolder];
}
+ (NSSet<NSString *> *)filePathsInDirectory:(NSString *)dirPath
{
NSMutableSet *filePaths = [NSMutableSet new];
NSError *error;
NSArray<NSString *> *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:&error];
if (error) {
OWSFail(@"%@ contentsOfDirectoryAtPath error: %@", self.logTag, error);
return [NSSet new];
}
for (NSString *fileName in fileNames) {
NSString *filePath = [dirPath stringByAppendingPathComponent:fileName];
BOOL isDirectory;
[[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory];
if (isDirectory) {
[filePaths addObjectsFromArray:[self filePathsInDirectory:filePath].allObjects];
} else {
[filePaths addObject:filePath];
}
}
return filePaths;
}
+ (long long)fileSizeOfFilePath:(NSString *)filePath
{
NSError *error;
NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error][NSFileSize];
if (error) {
OWSFail(@"%@ attributesOfItemAtPath: %@ error: %@", self.logTag, filePath, error);
return 0;
}
return fileSize.longLongValue;
}
+ (long long)fileSizeOfFilePaths:(NSArray<NSString *> *)filePaths
{
long long result = 0;
for (NSString *filePath in filePaths) {
result += [self fileSizeOfFilePath:filePath];
}
return result;
}
@end
NS_ASSUME_NONNULL_END

@ -16,6 +16,7 @@ extern NSString *const OWSApplicationWillResignActiveNotification;
extern NSString *const OWSApplicationDidBecomeActiveNotification;
typedef void (^BackgroundTaskExpirationHandler)(void);
typedef void (^AppActiveBlock)(void);
NSString *NSStringForUIApplicationState(UIApplicationState value);
@ -91,6 +92,8 @@ NSString *NSStringForUIApplicationState(UIApplicationState value);
// Should be a NOOP if isMainApp is NO.
- (void)setNetworkActivityIndicatorVisible:(BOOL)value;
- (void)runNowOrWhenMainAppIsActive:(AppActiveBlock)block;
@end
id<AppContext> CurrentAppContext(void);

@ -11,7 +11,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (atomic) BOOL isAppReady;
@property (nonatomic, nullable) NSMutableArray<AppReadyBlock> *appReadyBlocks;
@property (nonatomic) NSMutableArray<AppReadyBlock> *appReadyBlocks;
@end
@ -39,6 +39,8 @@ NS_ASSUME_NONNULL_BEGIN
OWSSingletonAssert();
self.appReadyBlocks = [NSMutableArray new];
return self;
}
@ -64,9 +66,6 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
if (!self.appReadyBlocks) {
self.appReadyBlocks = [NSMutableArray new];
}
[self.appReadyBlocks addObject:block];
}
@ -92,10 +91,11 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssertIsOnMainThread();
OWSAssert(self.isAppReady);
for (AppReadyBlock block in self.appReadyBlocks) {
NSArray<AppReadyBlock> *appReadyBlocks = [self.appReadyBlocks copy];
[self.appReadyBlocks removeAllObjects];
for (AppReadyBlock block in appReadyBlocks) {
block();
}
self.appReadyBlocks = nil;
}
@end

@ -4,17 +4,21 @@
@interface AppVersion : NSObject
@property (nonatomic, readonly) NSString *firstAppVersion;
@property (nonatomic, readonly) NSString *lastAppVersion;
@property (nonatomic, readonly) NSString *currentAppVersion;
// The properties are updated immediately after launch.
@property (atomic, readonly) NSString *firstAppVersion;
@property (atomic, readonly) NSString *lastAppVersion;
@property (atomic, readonly) NSString *currentAppVersion;
// Unlike lastAppVersion, this property isn't updated until
// appLaunchDidComplete is called.
@property (nonatomic, readonly) NSString *lastCompletedLaunchAppVersion;
@property (nonatomic, readonly) NSString *lastCompletedLaunchMainAppVersion;
@property (nonatomic, readonly) NSString *lastCompletedLaunchSAEAppVersion;
// There properties aren't updated until appLaunchDidComplete is called.
@property (atomic, readonly) NSString *lastCompletedLaunchAppVersion;
@property (atomic, readonly) NSString *lastCompletedLaunchMainAppVersion;
@property (atomic, readonly) NSString *lastCompletedLaunchSAEAppVersion;
+ (instancetype)instance;
@property (atomic, readonly) NSDate *appLaunchTime;
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)sharedInstance;
- (void)mainAppLaunchDidComplete;
- (void)saeLaunchDidComplete;

@ -15,12 +15,13 @@ NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_SAE
@interface AppVersion ()
@property (nonatomic) NSString *firstAppVersion;
@property (nonatomic) NSString *lastAppVersion;
@property (nonatomic) NSString *currentAppVersion;
@property (nonatomic) NSString *lastCompletedLaunchAppVersion;
@property (nonatomic) NSString *lastCompletedLaunchMainAppVersion;
@property (nonatomic) NSString *lastCompletedLaunchSAEAppVersion;
@property (atomic) NSString *firstAppVersion;
@property (atomic) NSString *lastAppVersion;
@property (atomic) NSString *currentAppVersion;
@property (atomic) NSString *lastCompletedLaunchAppVersion;
@property (atomic) NSString *lastCompletedLaunchMainAppVersion;
@property (atomic) NSString *lastCompletedLaunchSAEAppVersion;
@end
@ -28,7 +29,8 @@ NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_SAE
@implementation AppVersion
+ (instancetype)instance {
+ (instancetype)sharedInstance
{
static AppVersion *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@ -39,6 +41,10 @@ NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_SAE
}
- (void)configure {
OWSAssertIsOnMainThread();
_appLaunchTime = [NSDate new];
self.currentAppVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
// The version of the app when it was first launched.
@ -78,6 +84,8 @@ NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_SAE
- (void)appLaunchDidComplete
{
OWSAssertIsOnMainThread();
DDLogInfo(@"%@ appLaunchDidComplete", self.logTag);
self.lastCompletedLaunchAppVersion = self.currentAppVersion;
@ -90,6 +98,8 @@ NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_SAE
- (void)mainAppLaunchDidComplete
{
OWSAssertIsOnMainThread();
self.lastCompletedLaunchMainAppVersion = self.currentAppVersion;
[[NSUserDefaults appUserDefaults] setObject:self.currentAppVersion
forKey:kNSUserDefaults_LastCompletedLaunchAppVersion_MainApp];
@ -99,6 +109,8 @@ NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_SAE
- (void)saeLaunchDidComplete
{
OWSAssertIsOnMainThread();
self.lastCompletedLaunchSAEAppVersion = self.currentAppVersion;
[[NSUserDefaults appUserDefaults] setObject:self.currentAppVersion
forKey:kNSUserDefaults_LastCompletedLaunchAppVersion_SAE];

@ -44,7 +44,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
Logger.info("\(self.logTag) \(#function)")
_ = AppVersion()
_ = AppVersion.sharedInstance()
startupLogging()
@ -265,7 +265,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
// We don't need to use DeviceSleepManager in the SAE.
AppVersion.instance().saeLaunchDidComplete()
AppVersion.sharedInstance().saeLaunchDidComplete()
Environment.current().contactsManager.loadSignalAccountsFromCache()
Environment.current().contactsManager.startObserving()
@ -277,7 +277,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
OWSProfileManager.shared().ensureLocalProfileCached()
// We don't need to use OWSOrphanedDataCleaner in the SAE.
// We don't need to use OWSOrphanDataCleaner in the SAE.
// We don't need to fetch the local profile in the SAE

@ -205,6 +205,11 @@ NS_ASSUME_NONNULL_BEGIN
OWSFail(@"%@ called %s.", self.logTag, __PRETTY_FUNCTION__);
}
- (void)runNowOrWhenMainAppIsActive:(AppActiveBlock)block
{
OWSProdLogAndFail(@"%@ cannot run main app active blocks in share extension.", self.logTag);
}
@end
NS_ASSUME_NONNULL_END

Loading…
Cancel
Save