diff --git a/SignalMessaging/environment/LaunchJobs.swift b/SignalMessaging/environment/LaunchJobs.swift index 3334d5467c..d1944aed32 100644 --- a/SignalMessaging/environment/LaunchJobs.swift +++ b/SignalMessaging/environment/LaunchJobs.swift @@ -3,6 +3,7 @@ // import Foundation +import SignalServiceKit @objc public class LaunchJobs: NSObject { @@ -54,16 +55,24 @@ public class LaunchJobs: NSObject { Logger.verbose("Starting.") - DispatchQueue.global().async { + // Getting this work done ASAP is super high priority, since we won't finish launching until + // the completion block is fired. + // + // Originally, I considered making this all synchronous on the main thread (there's no UI to + // interrupt anyway) but I figured that in the absolute *worst* case where this work takes ~15s + // we risk getting watchdogged by SpringBoard. + // + // So instead, we'll use a user interactive queue and hope that's good enough. + DispatchQueue.sharedUserInteractive.async { // Mark all "attempting out" messages as "unsent", i.e. any messages that were not successfully // sent before the app exited should be marked as failures. - OWSFailedMessagesJob().runSync() + FailedMessagesJob().runSync() // Mark all "incomplete" calls as missed, e.g. any incoming or outgoing calls that were not // connected, failed or hung up before the app existed should be marked as missed. - OWSIncompleteCallsJob().runSync() + IncompleteCallsJob().runSync() // Mark all "downloading" attachments as "failed", i.e. any incoming attachments that were not // successfully downloaded before the app exited should be marked as failures. - OWSFailedAttachmentDownloadsJob().runSync() + FailedAttachmentDownloadsJob().runSync() // Kick off a low priority trim of the MSL // This will reschedule itself on a background queue ~24h or so diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentPointer.h b/SignalServiceKit/src/Messages/Attachments/TSAttachmentPointer.h index 34bff12a2d..9db00181db 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentPointer.h +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentPointer.h @@ -18,7 +18,7 @@ typedef NS_ENUM(NSUInteger, TSAttachmentPointerType) { TSAttachmentPointerTypeRestoring = 2, }; -typedef NS_ENUM(NSUInteger, TSAttachmentPointerState) { +typedef NS_CLOSED_ENUM(NSUInteger, TSAttachmentPointerState) { TSAttachmentPointerStateEnqueued = 0, TSAttachmentPointerStateDownloading = 1, TSAttachmentPointerStateFailed = 2, diff --git a/SignalServiceKit/src/Messages/FailedAttachmentDownloadsJob.swift b/SignalServiceKit/src/Messages/FailedAttachmentDownloadsJob.swift new file mode 100644 index 0000000000..6e3abdce20 --- /dev/null +++ b/SignalServiceKit/src/Messages/FailedAttachmentDownloadsJob.swift @@ -0,0 +1,60 @@ +// +// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// + +import Foundation + +public class FailedAttachmentDownloadsJob: Dependencies { + /// Used for logging the total number of attachments modified + private var count: UInt = 0 + public init() {} + + public func runSync() { + databaseStorage.write { writeTx in + AttachmentFinder.unfailedAttachmentPointerIds(transaction: writeTx).forEach { attachmentId in + // Since we can't directly mutate the enumerated attachments, we store only their ids in hopes + // of saving a little memory and then enumerate the (larger) TSAttachment objects one at a time. + autoreleasepool { + updateAttachmentPointerIfNecessary(attachmentId, transaction: writeTx) + } + } + } + Logger.info("Finished job. Marked \(count) in-progress attachments as failed") + } + + public func updateAttachmentPointerIfNecessary(_ uniqueId: String, transaction writeTx: SDSAnyWriteTransaction) { + // Preconditions: Must be a valid attachment pointer that hasn't failed + guard let attachment = TSAttachmentPointer.anyFetchAttachmentPointer( + uniqueId: uniqueId, + transaction: writeTx + ) else { + owsFailDebug("Missing attachment with id: \(uniqueId)") + return + } + + switch attachment.state { + case .enqueued, .downloading: + attachment.updateAttachmentPointerState(.failed, transaction: writeTx) + count += 1 + + switch count { + case ...3: + Logger.info("marked attachment pointer as failed: \(attachment.uniqueId)") + case 4: + Logger.info("eliding logs for further attachment pointers. final count will be reported once complete.") + default: + break + } + case .pendingMessageRequest: + // Do nothing. We don't want to mark this attachment as failed. + // It will be updated when the message request is resolved. + break + case .pendingManualDownload: + // Do nothing. We don't want to mark this attachment as failed. + break + case .failed: + // This should not have been returned from `unfailedAttachmentPointerIds` + owsFailDebug("Attachment has unexpected state \(attachment.uniqueId)."); + } + } +} diff --git a/SignalServiceKit/src/Messages/FailedMessagesJob.swift b/SignalServiceKit/src/Messages/FailedMessagesJob.swift new file mode 100644 index 0000000000..6e17bb2326 --- /dev/null +++ b/SignalServiceKit/src/Messages/FailedMessagesJob.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// + +import Foundation + +public class FailedMessagesJob: Dependencies { + + /// Used for logging the total number of messages modified + private var count: UInt = 0 + public init() {} + + public func runSync() { + databaseStorage.write { writeTx in + InteractionFinder.attemptingOutInteractionIds(transaction: writeTx).forEach { failedInteractionId in + // Since we can't directly mutate the enumerated "attempting out" expired messages, we store + // only their ids in hopes of saving a little memory and then enumerate the (larger) + // TSMessage objects one at a time. + autoreleasepool { + updateFailedMessageIfNecessary(failedInteractionId, transaction: writeTx) + } + } + } + } + + public func updateFailedMessageIfNecessary(_ uniqueId: String, transaction writeTx: SDSAnyWriteTransaction) { + // Preconditions + guard let message = TSOutgoingMessage.anyFetchOutgoingMessage(uniqueId: uniqueId, transaction: writeTx) else { + owsFailDebug("Missing interaction with id: \(uniqueId)") + return + } + guard message.messageState == .sending else { + owsFailDebug("Refusing to mark as unsent message \(message.timestamp) with state: \(message.messageState)") + return + } + + // Update + message.updateWithAllSendingRecipientsMarkedAsFailed(with: writeTx) + count += 1 + + // Log if appropriate + switch count { + case ...3: + Logger.info("marking message as unsent: \(message.uniqueId) \(message.timestamp)") + case 4: + Logger.info("eliding logs for further unsent messages. final update count will be reported once complete.") + default: + break + } + + // Postcondition + owsAssertDebug(message.messageState == .failed) + } +} diff --git a/SignalServiceKit/src/Messages/IncompleteCallsJob.swift b/SignalServiceKit/src/Messages/IncompleteCallsJob.swift new file mode 100644 index 0000000000..bf4c7abd73 --- /dev/null +++ b/SignalServiceKit/src/Messages/IncompleteCallsJob.swift @@ -0,0 +1,74 @@ +// +// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// + +import Foundation + +public class IncompleteCallsJob: Dependencies { + + private var count: UInt = 0 + private let cutoffTimestamp: UInt64 + + public init() { + if CurrentAppContext().appLaunchTime.ows_millisecondsSince1970 > 0 { + cutoffTimestamp = CurrentAppContext().appLaunchTime.ows_millisecondsSince1970 + } else { + owsFailDebug("Invalid app launch time") + cutoffTimestamp = Date.ows_millisecondTimestamp() + } + } + + public func runSync() { + databaseStorage.write { writeTx in + InteractionFinder.incompleteCallIds(transaction: writeTx).forEach { incompleteCallId in + // Since we can't directly mutate the enumerated "incomplete" calls, we store only their ids in hopes + // of saving a little memory and then enumerate the (larger) TSCall objects one at a time. + autoreleasepool { + updateIncompleteCallIfNecessary(incompleteCallId, transaction: writeTx) + } + } + } + Logger.info("Finished job. Updated \(count) incomplete calls") + } + + public func updateIncompleteCallIfNecessary(_ uniqueId: String, transaction writeTx: SDSAnyWriteTransaction) { + // Preconditions: Must be a valid call that started before the app launched. + guard let call = TSCall.anyFetchCall(uniqueId: uniqueId, transaction: writeTx) else { + owsFailDebug("Missing call with id: \(uniqueId)") + return + } + guard call.timestamp < cutoffTimestamp else { + Logger.info("Ignoring new call: \(call.uniqueId)") + return + } + + // Update + let targetCallType: RPRecentCallType + switch call.callType { + case .outgoingIncomplete: + targetCallType = .outgoingMissed + case .incomingIncomplete: + targetCallType = .incomingMissed + default: + owsFailDebug("Call has unexpected type: \(call.callType)") + return + } + + call.updateCallType(targetCallType, transaction: writeTx) + count += 1 + + // Log if appropriate + switch count { + case ...3: + Logger.info("marked call as missed: \(call.uniqueId) \(call.timestamp)") + case 4: + Logger.info("eliding logs for further incomplete calls. final update count will be reported once complete.") + default: + break + } + + // Postcondition: Should be some kind of missed call + let validFinalStates: [RPRecentCallType] = [.incomingMissed, .outgoingMissed] + owsAssertDebug(validFinalStates.contains(call.callType)) + } +} diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h index 5741d01d9c..a4597bdf13 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h @@ -268,7 +268,7 @@ NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp transaction:(SDSAnyWriteTransaction *)transaction; // On app launch, all "sending" recipients should be marked as "failed". -- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:(SDSAnyWriteTransaction *)transaction; +- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTransaction:(SDSAnyWriteTransaction *)transaction; - (BOOL)hasFailedRecipients; diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m index cb1a100a77..814f2ce2e3 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m @@ -619,7 +619,7 @@ NSUInteger const TSOutgoingMessageSchemaVersion = 1; }]; } -- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:(SDSAnyWriteTransaction *)transaction +- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTransaction:(SDSAnyWriteTransaction *)transaction { OWSAssertDebug(transaction); diff --git a/SignalServiceKit/src/Messages/OWSFailedAttachmentDownloadsJob.h b/SignalServiceKit/src/Messages/OWSFailedAttachmentDownloadsJob.h deleted file mode 100644 index 60378dffd3..0000000000 --- a/SignalServiceKit/src/Messages/OWSFailedAttachmentDownloadsJob.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. -// - -NS_ASSUME_NONNULL_BEGIN - -@interface OWSFailedAttachmentDownloadsJob : NSObject - -- (void)runSync; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/OWSFailedAttachmentDownloadsJob.m b/SignalServiceKit/src/Messages/OWSFailedAttachmentDownloadsJob.m deleted file mode 100644 index d9076ce6ad..0000000000 --- a/SignalServiceKit/src/Messages/OWSFailedAttachmentDownloadsJob.m +++ /dev/null @@ -1,73 +0,0 @@ -// -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. -// - -#import "OWSFailedAttachmentDownloadsJob.h" -#import "TSAttachmentPointer.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@implementation OWSFailedAttachmentDownloadsJob - -- (void)enumerateAttemptingOutAttachmentsWithBlock:(void (^_Nonnull)(TSAttachmentPointer *attachment))block - transaction:(SDSAnyReadTransaction *)transaction -{ - OWSAssertDebug(transaction); - - // Since we can't directly mutate the enumerated attachments, we store only their ids in hopes - // of saving a little memory and then enumerate the (larger) TSAttachment objects one at a time. - NSArray *attachmentIds = [AttachmentFinder unfailedAttachmentPointerIdsWithTransaction:transaction]; - for (NSString *attachmentId in attachmentIds) { - TSAttachmentPointer *_Nullable attachment = - [TSAttachmentPointer anyFetchAttachmentPointerWithUniqueId:attachmentId transaction:transaction]; - if (attachment == nil) { - OWSFailDebug(@"Missing attachment."); - continue; - } - block(attachment); - } -} - -- (void)runSync -{ - __block uint count = 0; - DatabaseStorageWrite( - self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) { - [self enumerateAttemptingOutAttachmentsWithBlock:^(TSAttachmentPointer *attachment) { - // sanity check - if (attachment.state == TSAttachmentPointerStateFailed) { - OWSFailDebug(@"Attachment has unexpected state."); - return; - } - - switch (attachment.state) { - case TSAttachmentPointerStateFailed: - OWSFailDebug(@"Attachment has unexpected state."); - break; - case TSAttachmentPointerStatePendingMessageRequest: - // Do nothing. We don't want to mark this attachment as failed. - // It will be updated when the message request is resolved. - break; - case TSAttachmentPointerStateEnqueued: - case TSAttachmentPointerStateDownloading: - [attachment updateWithAttachmentPointerState:TSAttachmentPointerStateFailed - transaction:transaction]; - count++; - return; - case TSAttachmentPointerStatePendingManualDownload: - // Do nothing. We don't want to mark this attachment as failed. - break; - } - } - transaction:transaction]; - }); - - if (count > 0) { - OWSLogDebug(@"Marked %u attachments as failed", count); - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/OWSFailedMessagesJob.h b/SignalServiceKit/src/Messages/OWSFailedMessagesJob.h deleted file mode 100644 index 07fb3d92e1..0000000000 --- a/SignalServiceKit/src/Messages/OWSFailedMessagesJob.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. -// - -NS_ASSUME_NONNULL_BEGIN - -@interface OWSFailedMessagesJob : NSObject - -- (void)runSync; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/OWSFailedMessagesJob.m b/SignalServiceKit/src/Messages/OWSFailedMessagesJob.m deleted file mode 100644 index 5cd403d40c..0000000000 --- a/SignalServiceKit/src/Messages/OWSFailedMessagesJob.m +++ /dev/null @@ -1,67 +0,0 @@ -// -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. -// - -#import "OWSFailedMessagesJob.h" -#import "TSMessage.h" -#import "TSOutgoingMessage.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@implementation OWSFailedMessagesJob - -- (NSArray *)fetchAttemptingOutMessageIdsWithTransaction:(SDSAnyReadTransaction *)transaction -{ - OWSAssertDebug(transaction); - - return [InteractionFinder attemptingOutInteractionIdsWithTransaction:transaction]; -} - -- (void)enumerateAttemptingOutMessagesWithBlock:(void (^_Nonnull)(TSOutgoingMessage *message))block - transaction:(SDSAnyReadTransaction *)transaction -{ - OWSAssertDebug(transaction); - - // Since we can't directly mutate the enumerated "attempting out" expired messages, we store only their ids in hopes - // of saving a little memory and then enumerate the (larger) TSMessage objects one at a time. - for (NSString *expiredMessageId in [self fetchAttemptingOutMessageIdsWithTransaction:transaction]) { - TSOutgoingMessage *_Nullable message = - [TSOutgoingMessage anyFetchOutgoingMessageWithUniqueId:expiredMessageId transaction:transaction]; - if (message == nil) { - OWSFailDebug(@"Missing interaction."); - continue; - } - block(message); - } -} - -- (void)runSync -{ - __block uint count = 0; - - DatabaseStorageWrite( - self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) { - [self enumerateAttemptingOutMessagesWithBlock:^(TSOutgoingMessage *message) { - // sanity check - OWSAssertDebug(message.messageState == TSOutgoingMessageStateSending); - if (message.messageState != TSOutgoingMessageStateSending) { - OWSLogError(@"Refusing to mark as unsent message with state: %d", (int)message.messageState); - return; - } - - OWSLogDebug(@"marking message as unsent: %@", message.uniqueId); - [message updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:transaction]; - OWSAssertDebug(message.messageState == TSOutgoingMessageStateFailed); - - count++; - } - transaction:transaction]; - }); - - OWSLogDebug(@"Marked %u messages as unsent", count); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/OWSIncompleteCallsJob.h b/SignalServiceKit/src/Messages/OWSIncompleteCallsJob.h deleted file mode 100644 index 829e3462cd..0000000000 --- a/SignalServiceKit/src/Messages/OWSIncompleteCallsJob.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. -// - -NS_ASSUME_NONNULL_BEGIN - -@interface OWSIncompleteCallsJob : NSObject - -- (void)runSync; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/OWSIncompleteCallsJob.m b/SignalServiceKit/src/Messages/OWSIncompleteCallsJob.m deleted file mode 100644 index f75adbc1fd..0000000000 --- a/SignalServiceKit/src/Messages/OWSIncompleteCallsJob.m +++ /dev/null @@ -1,76 +0,0 @@ -// -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. -// - -#import "OWSIncompleteCallsJob.h" -#import "AppContext.h" -#import "TSCall.h" -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@implementation OWSIncompleteCallsJob - -- (NSArray *)fetchIncompleteCallIdsWithTransaction:(SDSAnyWriteTransaction *)transaction -{ - OWSAssertDebug(transaction); - - return [InteractionFinder incompleteCallIdsWithTransaction:transaction]; -} - -- (void)enumerateIncompleteCallsWithBlock:(void (^)(TSCall *call))block - transaction:(SDSAnyWriteTransaction *)transaction -{ - OWSAssertDebug(transaction); - - // Since we can't directly mutate the enumerated "incomplete" calls, we store only their ids in hopes - // of saving a little memory and then enumerate the (larger) TSCall objects one at a time. - for (NSString *callId in [self fetchIncompleteCallIdsWithTransaction:transaction]) { - TSCall *_Nullable call = [TSCall anyFetchCallWithUniqueId:callId transaction:transaction]; - if (call == nil) { - OWSFailDebug(@"Missing call."); - continue; - } - block(call); - } -} - -- (void)runSync -{ - __block uint count = 0; - - OWSAssertDebug(CurrentAppContext().appLaunchTime); - uint64_t cutoffTimestamp = [NSDate ows_millisecondsSince1970ForDate:CurrentAppContext().appLaunchTime]; - - DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) { - [self - enumerateIncompleteCallsWithBlock:^(TSCall *call) { - if (call.timestamp > cutoffTimestamp) { - OWSLogInfo(@"ignoring new call: %@", call.uniqueId); - return; - } - - if (call.callType == RPRecentCallTypeOutgoingIncomplete) { - OWSLogDebug(@"marking call as missed: %@", call.uniqueId); - [call updateCallType:RPRecentCallTypeOutgoingMissed transaction:transaction]; - OWSAssertDebug(call.callType == RPRecentCallTypeOutgoingMissed); - } else if (call.callType == RPRecentCallTypeIncomingIncomplete) { - OWSLogDebug(@"marking call as missed: %@", call.uniqueId); - [call updateCallType:RPRecentCallTypeIncomingMissed transaction:transaction]; - OWSAssertDebug(call.callType == RPRecentCallTypeIncomingMissed); - } else { - OWSFailDebug(@"call has unexpected call type: %@", NSStringFromCallType(call.callType)); - return; - } - count++; - } - transaction:transaction]; - }); - - OWSLogInfo(@"Marked %u calls as missed", count); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalShareExtension/SharingThreadPickerViewController.swift b/SignalShareExtension/SharingThreadPickerViewController.swift index 4c0593f1b5..1c6cca874b 100644 --- a/SignalShareExtension/SharingThreadPickerViewController.swift +++ b/SignalShareExtension/SharingThreadPickerViewController.swift @@ -400,7 +400,7 @@ extension SharingThreadPickerViewController { self.databaseStorage.write { transaction in for message in self.outgoingMessages { // If we sent the message to anyone, mark it as failed - message.updateWithAllSendingRecipientsMarkedAsFailed(withTansaction: transaction) + message.updateWithAllSendingRecipientsMarkedAsFailed(with: transaction) } } self.shareViewDelegate?.shareViewWasCancelled()