Add internal-only UI to display verbose backup errors

This commit is contained in:
Harry 2024-10-30 11:21:53 -07:00 committed by GitHub
parent a6fd1765f9
commit 9ba009a1e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 566 additions and 97 deletions

View File

@ -874,6 +874,8 @@
6646573F2AC3B9190099DE1C /* MockRegistrationStateChangeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6646573E2AC3B9190099DE1C /* MockRegistrationStateChangeManager.swift */; };
664657412AC4FB720099DE1C /* NotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664657402AC4FB720099DE1C /* NotificationPresenter.swift */; };
664657472ACB66630099DE1C /* TSAccountManagerObjcBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664657462ACB66630099DE1C /* TSAccountManagerObjcBridge.swift */; };
66485EB02CCC515A00B8613F /* MessageBackupInternalErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66485EAF2CCC50FA00B8613F /* MessageBackupInternalErrorViewController.swift */; };
66485EB32CD03F6400B8613F /* MessageBackupErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66485EB22CD03F5D00B8613F /* MessageBackupErrorPresenter.swift */; };
6649651C2BDC6EAD00E2DE98 /* AVAsset+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6649651B2BDC6EAD00E2DE98 /* AVAsset+Attachment.swift */; };
6649651E2BDF169F00E2DE98 /* UIImage+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6649651D2BDF169F00E2DE98 /* UIImage+Attachment.swift */; };
664BA8452BB5CE12005638E0 /* PreparedOutgoingMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664BA8442BB5CE12005638E0 /* PreparedOutgoingMessage.swift */; };
@ -4569,6 +4571,8 @@
6646573E2AC3B9190099DE1C /* MockRegistrationStateChangeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRegistrationStateChangeManager.swift; sourceTree = "<group>"; };
664657402AC4FB720099DE1C /* NotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPresenter.swift; sourceTree = "<group>"; };
664657462ACB66630099DE1C /* TSAccountManagerObjcBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSAccountManagerObjcBridge.swift; sourceTree = "<group>"; };
66485EAF2CCC50FA00B8613F /* MessageBackupInternalErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupInternalErrorViewController.swift; sourceTree = "<group>"; };
66485EB22CD03F5D00B8613F /* MessageBackupErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupErrorPresenter.swift; sourceTree = "<group>"; };
6649651B2BDC6EAD00E2DE98 /* AVAsset+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAsset+Attachment.swift"; sourceTree = "<group>"; };
6649651D2BDF169F00E2DE98 /* UIImage+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Attachment.swift"; sourceTree = "<group>"; };
664BA8442BB5CE12005638E0 /* PreparedOutgoingMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreparedOutgoingMessage.swift; sourceTree = "<group>"; };
@ -8109,6 +8113,7 @@
34BECE2C1F7ABCE000D7438D /* GifPicker */,
34386A4C207D0C01009F5D9C /* HomeView */,
4C4F360E2284516F00A8DF48 /* MediaGallery */,
66485EB12CD03F3300B8613F /* MessageBackup */,
34995F122411838C00C70546 /* NewGroupView */,
3497971D25DAA86100E99FA4 /* Payments */,
34969558219B605E00DCFE74 /* Photos */,
@ -9071,6 +9076,14 @@
path = RegistrationStateChangeManager;
sourceTree = "<group>";
};
66485EB12CD03F3300B8613F /* MessageBackup */ = {
isa = PBXGroup;
children = (
66485EAF2CCC50FA00B8613F /* MessageBackupInternalErrorViewController.swift */,
);
path = MessageBackup;
sourceTree = "<group>";
};
6649651A2BDC6E8D00E2DE98 /* Playback */ = {
isa = PBXGroup;
children = (
@ -9183,6 +9196,7 @@
66CD25902B0EC20800139E17 /* MessageBackupConstants.swift */,
66FFDADB2C823C270079C0E7 /* MessageBackupContexts.swift */,
D90D4D832BBB61680097C573 /* MessageBackupEmptyFrameId.swift */,
66485EB22CD03F5D00B8613F /* MessageBackupErrorPresenter.swift */,
66232AE02CC0271F00AE6A76 /* MessageBackupFullTextSearchIndexer.swift */,
C1A0F79C2B9F57340009DC0D /* MessageBackupKeyMaterial.swift */,
C1A0F79E2B9F59920009DC0D /* MessageBackupKeyMaterialImpl.swift */,
@ -16304,6 +16318,7 @@
346EAA14250199A400E8AB6F /* MemberRequestView.swift in Sources */,
4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */,
4CB5F26720F6E1E2004D1B42 /* MessageActionsToolbar.swift in Sources */,
66485EB02CCC515A00B8613F /* MessageBackupInternalErrorViewController.swift in Sources */,
45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */,
66CDB7522AF9D117009A36EC /* MessageFetchBGRefreshTask.swift in Sources */,
34DE9C02256575300080E4AF /* MessageLoader.swift in Sources */,
@ -17085,6 +17100,7 @@
66FFDADC2C823C270079C0E7 /* MessageBackupContexts.swift in Sources */,
C1CA5F8E2BE2F21C00D733CA /* MessageBackupDistributionListRecipientArchiver.swift in Sources */,
D90D4D842BBB61680097C573 /* MessageBackupEmptyFrameId.swift in Sources */,
66485EB32CD03F6400B8613F /* MessageBackupErrorPresenter.swift in Sources */,
662590D12B5B525E001FDCDD /* MessageBackupErrors.swift in Sources */,
D91D9C8C2C3F06400009E4F7 /* MessageBackupExpirationTimerChatUpdateArchiver.swift in Sources */,
66232AE12CC0272900AE6A76 /* MessageBackupFullTextSearchIndexer.swift in Sources */,

View File

@ -389,7 +389,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
callMessageHandler: WebRTCCallMessageHandler(),
currentCallProvider: currentCall,
notificationPresenter: NotificationPresenterImpl(),
incrementalTSAttachmentMigrator: launchContext.incrementalMessageTSAttachmentMigrator
incrementalTSAttachmentMigrator: launchContext.incrementalMessageTSAttachmentMigrator,
messageBackupErrorPresenterFactory: MessageBackupErrorPresenterFactoryInternal()
)
setupNSEInteroperation()
SUIEnvironment.shared.setUp(

View File

@ -17,6 +17,7 @@ public struct RegistrationCoordinatorDependencies {
public let featureFlags: RegistrationCoordinatorImpl.Shims.FeatureFlags
public let keyValueStoreFactory: KeyValueStoreFactory
public let localUsernameManager: LocalUsernameManager
public let messageBackupErrorPresenter: MessageBackupErrorPresenter
public let messageBackupManager: MessageBackupManager
public let messagePipelineSupervisor: RegistrationCoordinatorImpl.Shims.MessagePipelineSupervisor
public let messageProcessor: RegistrationCoordinatorImpl.Shims.MessageProcessor
@ -50,6 +51,7 @@ public struct RegistrationCoordinatorDependencies {
featureFlags: RegistrationCoordinatorImpl.Wrappers.FeatureFlags(),
keyValueStoreFactory: DependenciesBridge.shared.keyValueStoreFactory,
localUsernameManager: DependenciesBridge.shared.localUsernameManager,
messageBackupErrorPresenter: DependenciesBridge.shared.messageBackupErrorPresenter,
messageBackupManager: DependenciesBridge.shared.messageBackupManager,
messagePipelineSupervisor: RegistrationCoordinatorImpl.Wrappers.MessagePipelineSupervisor(SSKEnvironment.shared.messagePipelineSupervisorRef),
messageProcessor: RegistrationCoordinatorImpl.Wrappers.MessageProcessor(SSKEnvironment.shared.messageProcessorRef),

View File

@ -471,8 +471,12 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
)
self.inMemoryState.hasRestoredFromLocalMessageBackup = true
Logger.info("Finished restore")
}.recover { error in
owsFailDebug("Failed restore")
}.recover(on: schedulers.main) { error in
let (guarantee, future) = Guarantee<Void>.pending()
self.deps.messageBackupErrorPresenter.forcePresentDuringRegistration {
future.resolve()
}
return guarantee
}.then { [weak self] () -> Guarantee<RegistrationStep> in
guard let self else {
return unretainedSelfError()

View File

@ -156,6 +156,27 @@ class ForwardMessageViewController: InteractiveSheetViewController {
present(content: .single(item: builder.build()), from: fromViewController, delegate: delegate)
}
public class func present(
forMessageBody messageBody: MessageBody,
from fromViewController: UIViewController,
delegate: ForwardMessageDelegate
) {
present(
content: .single(item: ForwardMessageItem.Item(
interaction: nil,
attachments: nil,
contactShare: nil,
messageBody: messageBody,
linkPreviewDraft: nil,
stickerMetadata: nil,
stickerAttachment: nil,
textAttachment: nil
)),
from: fromViewController,
delegate: delegate
)
}
private class func present(content: Content,
from fromViewController: UIViewController,
delegate: ForwardMessageDelegate) {

View File

@ -0,0 +1,289 @@
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import SignalUI
import UIKit
/// For internal (nightly) use only. Produces MessageBackupErrorPresenterInternal.
class MessageBackupErrorPresenterFactoryInternal: MessageBackupErrorPresenterFactory {
func build(
appReadiness: AppReadiness,
db: any DB,
keyValueStoreFactory: KeyValueStoreFactory,
tsAccountManager: TSAccountManager
) -> MessageBackupErrorPresenter {
return MessageBackupErrorPresenterInternal(
appReadiness: appReadiness,
db: db,
keyValueStoreFactory: keyValueStoreFactory,
tsAccountManager: tsAccountManager
)
}
}
/// For internal (nightly) use only. Presents MessageBackupInternalErrorViewController when backups emits errors.
class MessageBackupErrorPresenterInternal: MessageBackupErrorPresenter {
private let appReadiness: AppReadiness
private let db: any DB
private let tsAccountManager: TSAccountManager
private let kvStore: KeyValueStore
private static let stringifiedErrorsKey = "stringifiedErrors"
private static let hasBeenDisplayedKey = "hasBeenDisplayed"
init(
appReadiness: AppReadiness,
db: any DB,
keyValueStoreFactory: KeyValueStoreFactory,
tsAccountManager: TSAccountManager
) {
self.appReadiness = appReadiness
self.db = db
self.tsAccountManager = tsAccountManager
self.kvStore = keyValueStoreFactory.keyValueStore(collection: "MessageBackupErrorPresenterImpl")
NotificationCenter.default.addObserver(
self,
selector: #selector(presentErrorsIfNeededWithDelay),
name: .registrationStateDidChange,
object: nil
)
appReadiness.runNowOrWhenUIDidBecomeReadySync { [weak self] in
self?.presentErrorsIfNeededWithDelay()
}
}
func persistErrors(_ errors: [SignalServiceKit.MessageBackup.CollapsedErrorLog], tx outerTx: DBWriteTransaction) {
guard FeatureFlags.messageBackupErrorDisplay else {
return
}
if errors.isEmpty {
return
}
let stringified = errors
.map {
var text = ($0.typeLogString) + "\n"
+ "Repeated \($0.errorCount) times, from: \($0.idLogStrings)\n"
+ "Example callsite: \($0.exampleCallsiteString)"
if let exampleProtoFrameJson = $0.exampleProtoFrameJson {
text.append("\nProto:\n\(exampleProtoFrameJson)")
}
return text
}
.joined(separator: "\n-------------------\n")
// The outer transaction might get rolled back because of these very errors.
// At the risk of losing these errors in a crash (this is internal only, its fine)
// do the actual write in a separate transaction (that happens synchronously)
// so it is never rolled back.
outerTx.addAsyncCompletion(on: DispatchQueue.global()) { [weak self] in
guard let self else { return }
self.db.write { innerTx in
self.kvStore.setString(stringified, key: Self.stringifiedErrorsKey, transaction: innerTx)
self.kvStore.setBool(false, key: Self.hasBeenDisplayedKey, transaction: innerTx)
innerTx.addAsyncCompletion(on: DispatchQueue.main) { [weak self] in
self?.presentErrorsIfNeeded()
}
}
}
}
private var forceDuringRegistration = false
func forcePresentDuringRegistration(completion: @escaping () -> Void) {
self.forceDuringRegistration = true
self.presentErrorsIfNeeded(completion: completion)
}
@objc
private func presentErrorsIfNeededWithDelay() {
// Introduce a small delay to get the UI set up.
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
self.presentErrorsIfNeeded()
}
}
private func presentErrorsIfNeeded(completion: (() -> Void)? = nil) {
defer { self.forceDuringRegistration = false }
guard FeatureFlags.messageBackupErrorDisplay else {
completion?()
return
}
guard forceDuringRegistration || appReadiness.isUIReady else {
completion?()
return
}
let isRegistered = tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered
guard forceDuringRegistration || isRegistered else {
completion?()
return
}
let errorString: String? = db.write { tx in
if kvStore.getBool(Self.hasBeenDisplayedKey, defaultValue: false, transaction: tx) {
return nil
}
let errorString = kvStore.getString(Self.stringifiedErrorsKey, transaction: tx)
kvStore.setBool(true, key: Self.hasBeenDisplayedKey, transaction: tx)
return errorString
}
guard let errorString else {
completion?()
return
}
let vc = MessageBackupInternalErrorViewController(
errorString: errorString,
isRegistered: isRegistered,
completion: completion
)
let navVc = OWSNavigationController(rootViewController: vc)
UIApplication.shared.frontmostViewController?.present(navVc, animated: true)
}
}
private class MessageBackupInternalErrorViewController: OWSViewController {
// MARK: - Properties
private let originalText: String
private let completion: (() -> Void)?
var textView: UITextView!
let isRegistered: Bool
let footer = UIToolbar.clear()
// MARK: Initializers
fileprivate init(
errorString: String,
isRegistered: Bool,
completion: (() -> Void)?
) {
self.originalText = errorString
self.isRegistered = isRegistered
self.completion = completion
super.init()
}
// MARK: View Lifecycle
public override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Backup errors"
createViews()
self.textView.contentOffset = CGPoint(x: 0, y: self.textView.contentInset.top)
}
public override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
completion?()
}
public override func themeDidChange() {
super.themeDidChange()
loadContent()
}
public func loadContent() {
view.backgroundColor = Theme.backgroundColor
textView.backgroundColor = Theme.backgroundColor
textView.textColor = Theme.primaryTextColor
footer.tintColor = Theme.primaryIconColor
}
// MARK: - Create Views
private func createViews() {
view.backgroundColor = Theme.backgroundColor
let textView = OWSTextView()
self.textView = textView
textView.font = UIFont.dynamicTypeBody
textView.backgroundColor = Theme.backgroundColor
textView.isOpaque = true
textView.isEditable = true
textView.isSelectable = true
textView.isScrollEnabled = true
textView.showsHorizontalScrollIndicator = false
textView.showsVerticalScrollIndicator = true
textView.isUserInteractionEnabled = true
textView.textColor = Theme.primaryTextColor
textView.text = originalText
view.addSubview(textView)
textView.autoPinEdge(toSuperviewEdge: .top)
textView.autoPinEdge(toSuperviewEdge: .leading)
textView.autoPinEdge(toSuperviewEdge: .trailing)
textView.textContainerInset = UIEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
view.addSubview(footer)
footer.autoPinWidthToSuperview()
footer.autoPinEdge(.top, to: .bottom, of: textView)
footer.autoPin(toBottomLayoutGuideOf: self, withInset: 0)
footer.tintColor = Theme.primaryIconColor
var footerItems = [
UIBarButtonItem(
image: Theme.iconImage(.buttonShare),
style: .plain,
target: self,
action: #selector(shareButtonPressed)
),
.flexibleSpace()
]
if isRegistered {
footerItems.append(.button(icon: .buttonForward, style: .plain) { [weak self] in
self?.sendAsMessage()
})
}
footer.items = footerItems
loadContent()
}
// MARK: - Actions
@objc
private func shareButtonPressed(_ sender: UIBarButtonItem) {
AttachmentSharing.showShareUI(for: textView.text, sender: sender)
}
private func sendAsMessage() {
ForwardMessageViewController.present(
forMessageBody: .init(text: textView.text, ranges: .empty),
from: self,
delegate: self
)
}
}
extension MessageBackupInternalErrorViewController: ForwardMessageDelegate {
public func forwardMessageFlowDidComplete(items: [ForwardMessageItem], recipientThreads: [TSThread]) {
dismiss(animated: true) {
ForwardMessageViewController.finalizeForward(
items: items,
recipientThreads: recipientThreads,
fromViewController: self
)
}
}
public func forwardMessageFlowDidCancel() {
dismiss(animated: true)
}
}

View File

@ -114,6 +114,7 @@ public class RegistrationCoordinatorTest: XCTestCase {
featureFlags: featureFlags,
keyValueStoreFactory: InMemoryKeyValueStoreFactory(),
localUsernameManager: localUsernameManagerMock,
messageBackupErrorPresenter: NoOpMessageBackupErrorPresenter(),
messageBackupManager: MessageBackupManagerMock(),
messagePipelineSupervisor: mockMessagePipelineSupervisor,
messageProcessor: mockMessageProcessor,

View File

@ -147,7 +147,8 @@ class NSEEnvironment {
callMessageHandler: NSECallMessageHandler(),
currentCallProvider: CurrentCallNoOpProvider(),
notificationPresenter: NotificationPresenterImpl(),
incrementalTSAttachmentMigrator: NoOpIncrementalMessageTSAttachmentMigrator()
incrementalTSAttachmentMigrator: NoOpIncrementalMessageTSAttachmentMigrator(),
messageBackupErrorPresenterFactory: NoOpMessageBackupErrorPresenterFactory()
)
databaseContinuation.prepareDatabase().done(on: DispatchQueue.main) { finalSetupContinuation in

View File

@ -104,6 +104,7 @@ public class DependenciesBridge {
public let masterKeySyncManager: MasterKeySyncManager
public let mediaBandwidthPreferenceStore: MediaBandwidthPreferenceStore
public let mediaGalleryResourceManager: MediaGalleryResourceManager
public let messageBackupErrorPresenter: MessageBackupErrorPresenter
public let messageBackupManager: MessageBackupManager
public let messageStickerManager: MessageStickerManager
public let mrbkStore: MediaRootBackupKeyStore
@ -224,6 +225,7 @@ public class DependenciesBridge {
masterKeySyncManager: MasterKeySyncManager,
mediaBandwidthPreferenceStore: MediaBandwidthPreferenceStore,
mediaGalleryResourceManager: MediaGalleryResourceManager,
messageBackupErrorPresenter: MessageBackupErrorPresenter,
messageBackupManager: MessageBackupManager,
messageStickerManager: MessageStickerManager,
mrbkStore: MediaRootBackupKeyStore,
@ -341,6 +343,7 @@ public class DependenciesBridge {
self.masterKeySyncManager = masterKeySyncManager
self.mediaBandwidthPreferenceStore = mediaBandwidthPreferenceStore
self.mediaGalleryResourceManager = mediaGalleryResourceManager
self.messageBackupErrorPresenter = messageBackupErrorPresenter
self.messageBackupManager = messageBackupManager
self.messageStickerManager = messageStickerManager
self.mrbkStore = mrbkStore

View File

@ -106,6 +106,7 @@ public class AppSetup {
currentCallProvider: any CurrentCallProvider,
notificationPresenter: any NotificationPresenter,
incrementalTSAttachmentMigrator: IncrementalMessageTSAttachmentMigrator,
messageBackupErrorPresenterFactory: MessageBackupErrorPresenterFactory,
testDependencies: TestDependencies = TestDependencies()
) -> AppSetup.DatabaseContinuation {
configureUnsatisfiableConstraintLogging()
@ -1027,6 +1028,13 @@ public class AppSetup {
let backupStoryStore = MessageBackupStoryStore(storyStore: storyStore)
let mrbkStore = MediaRootBackupKeyStore(keyValueStoreFactory: keyValueStoreFactory)
let messageBackupErrorPresenter = messageBackupErrorPresenterFactory.build(
appReadiness: appReadiness,
db: db,
keyValueStoreFactory: keyValueStoreFactory,
tsAccountManager: tsAccountManager
)
let messageBackupManager = MessageBackupManagerImpl(
accountDataArchiver: MessageBackupAccountDataArchiverImpl(
chatStyleArchiver: messageBackupChatStyleArchiver,
@ -1099,6 +1107,7 @@ public class AppSetup {
encryptedStreamProvider: MessageBackupEncryptedProtoStreamProviderImpl(
backupKeyMaterial: messageBackupKeyMaterial
),
errorPresenter: messageBackupErrorPresenter,
fullTextSearchIndexer: MessageBackupFullTextSearchIndexerImpl(
appReadiness: appReadiness,
dateProvider: dateProvider,
@ -1271,6 +1280,7 @@ public class AppSetup {
masterKeySyncManager: masterKeySyncManager,
mediaBandwidthPreferenceStore: mediaBandwidthPreferenceStore,
mediaGalleryResourceManager: mediaGalleryResourceManager,
messageBackupErrorPresenter: messageBackupErrorPresenter,
messageBackupManager: messageBackupManager,
messageStickerManager: messageStickerManager,
mrbkStore: mrbkStore,

View File

@ -86,7 +86,11 @@ public class MessageBackupChatStyleArchiver: MessageBackupProtoArchiver {
if !partialErrors.isEmpty {
// Just log these errors, but count as success and proceed.
MessageBackup.log(partialErrors)
MessageBackup
.collapse(partialErrors
.map { MessageBackup.LoggableErrorAndProto(error: $0) }
)
.forEach { $0.log() }
}
return .success(protos)
@ -469,10 +473,10 @@ public class MessageBackupChatStyleArchiver: MessageBackupProtoArchiver {
} catch {
// Just log these errors, but count as success and proceed.
// The wallpaper just won't upload.
MessageBackup.log([MessageBackup.ArchiveFrameError<IDType>.archiveFrameError(
MessageBackup.collapse([.init(error: MessageBackup.ArchiveFrameError<IDType>.archiveFrameError(
.failedToEnqueueAttachmentForUpload,
errorId
)])
))]).forEach { $0.log() }
}
}

View File

@ -3,6 +3,8 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import SwiftProtobuf
extension MessageBackup {
public typealias RawError = Swift.Error
@ -280,6 +282,10 @@ extension MessageBackup {
return nil
}
}
public var shouldLog: Bool {
return true
}
}
/// Error archiving an entire category of frames; not attributable to a
@ -345,6 +351,10 @@ extension MessageBackup {
// Log each of these as we see them.
return nil
}
public var shouldLog: Bool {
return true
}
}
/// Error restoring a frame.
@ -809,6 +819,15 @@ extension MessageBackup {
return callsiteLogString
}
}
public var shouldLog: Bool {
switch type {
case .unimplemented:
return false
default:
return true
}
}
}
}
@ -825,60 +844,87 @@ internal protocol MessageBackupLoggableError {
/// Instead we collapse these similar logs together, keep a count, and log that.
/// If this is non-nil, we do that collapsing, otherwise we log as-is.
var collapseKey: String? { get }
var shouldLog: Bool { get }
}
extension MessageBackup {
internal static func log<T: MessageBackupLoggableError>(_ errors: [T]) {
var logAsIs = [String]()
internal struct LoggableErrorAndProto {
let error: any MessageBackupLoggableError
/// Nil for archiving, if we fail to even parse the proto on restore,
/// or if the feature flag is disabled such that this would be unused.
let protoJson: String?
init(
error: any MessageBackupLoggableError,
protoFrame: SwiftProtobuf.Message? = nil
) {
self.error = error
// Don't serialize proto frames if we aren't displaying errors.
if let protoFrame, FeatureFlags.messageBackupErrorDisplay {
do {
self.protoJson = try protoFrame.jsonString()
} catch let jsonError {
self.protoJson = "Unable to json encode proto: \(jsonError)"
}
} else {
self.protoJson = nil
}
}
}
internal static func collapse(_ errors: [LoggableErrorAndProto]) -> [CollapsedErrorLog] {
var collapsedLogs = OrderedDictionary<String, CollapsedErrorLog>()
for error in errors {
guard let collapseKey = error.collapseKey else {
logAsIs.append(
error.typeLogString + " "
+ error.idLogString + " "
+ error.callsiteLogString
)
guard error.error.shouldLog else {
continue
}
let collapseKey = error.error.collapseKey ?? UUID().uuidString
if var existingLog = collapsedLogs[collapseKey] {
existingLog.collapse(error)
collapsedLogs.replace(key: collapseKey, value: existingLog)
} else {
var newLog = CollapsedErrorLog()
newLog.collapse(error)
var newLog = CollapsedErrorLog(error)
collapsedLogs.append(key: collapseKey, value: newLog)
}
}
logAsIs.forEach { Logger.error($0) }
collapsedLogs.orderedValues.forEach { $0.log() }
return Array(collapsedLogs.orderedValues)
}
fileprivate static let maxCollapsedIdLogCount = 10
fileprivate struct CollapsedErrorLog {
var typeLogString: String?
var exampleCallsiteString: String?
var errorCount: UInt = 0
var idLogStrings: [String] = []
public struct CollapsedErrorLog {
public private(set) var typeLogString: String
public private(set) var exampleCallsiteString: String
public private(set) var exampleProtoFrameJson: String?
public private(set) var errorCount: UInt = 0
public private(set) var idLogStrings: [String] = []
mutating func collapse(_ error: MessageBackupLoggableError) {
init(_ error: LoggableErrorAndProto) {
self.typeLogString = error.error.typeLogString
self.exampleCallsiteString = error.error.callsiteLogString
self.exampleProtoFrameJson = error.protoJson
self.collapse(error)
}
mutating func collapse(_ error: LoggableErrorAndProto) {
self.errorCount += 1
self.typeLogString = self.typeLogString ?? error.typeLogString
self.exampleCallsiteString = self.exampleCallsiteString ?? error.callsiteLogString
if exampleProtoFrameJson == nil, let protoJson = error.protoJson {
self.exampleProtoFrameJson = protoJson
}
if idLogStrings.count < MessageBackup.maxCollapsedIdLogCount {
idLogStrings.append(error.idLogString)
idLogStrings.append(error.error.idLogString)
}
}
func log() {
internal func log() {
Logger.error(
(typeLogString ?? "") + " "
(typeLogString) + " "
+ "Repeated \(errorCount) times. "
+ "from: \(idLogStrings) "
+ "example callsite: \(exampleCallsiteString ?? "none")"
+ "example callsite: \(exampleCallsiteString)"
)
}
}

View File

@ -0,0 +1,53 @@
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
public protocol MessageBackupErrorPresenterFactory {
func build(
appReadiness: AppReadiness,
db: any DB,
keyValueStoreFactory: KeyValueStoreFactory,
tsAccountManager: TSAccountManager
) -> MessageBackupErrorPresenter
}
public protocol MessageBackupErrorPresenter {
/// Persist a set of errors for future display.
/// We persist because display may be deferred until certain UI actions occur (finishing registration)
/// during which time the app may be interrupted.
/// We only care to hold onto the latest set of backup errors.
func persistErrors(_ errors: [MessageBackup.CollapsedErrorLog], tx: DBWriteTransaction)
/// Force presentation during registration; calls completion when presentation has finished.
func forcePresentDuringRegistration(completion: @escaping () -> Void)
}
public class NoOpMessageBackupErrorPresenterFactory: MessageBackupErrorPresenterFactory {
public init() {}
public func build(
appReadiness: AppReadiness,
db: any DB,
keyValueStoreFactory: KeyValueStoreFactory,
tsAccountManager: TSAccountManager
) -> MessageBackupErrorPresenter {
return NoOpMessageBackupErrorPresenter()
}
}
public class NoOpMessageBackupErrorPresenter: MessageBackupErrorPresenter {
public init() {}
public func persistErrors(_ errors: [MessageBackup.CollapsedErrorLog], tx: any DBWriteTransaction) {
// do nothing
}
public func forcePresentDuringRegistration(completion: @escaping () -> Void) {
// do nothing
}
}

View File

@ -22,6 +22,7 @@ public class MessageBackupManagerImpl: MessageBackupManager {
private class NotImplementedError: Error {}
private class BackupError: Error {}
private typealias LoggableErrorAndProto = MessageBackup.LoggableErrorAndProto
private let accountDataArchiver: MessageBackupAccountDataArchiver
private let attachmentDownloadManager: AttachmentDownloadManager
@ -39,6 +40,7 @@ public class MessageBackupManagerImpl: MessageBackupManager {
private let disappearingMessagesJob: OWSDisappearingMessagesJob
private let distributionListRecipientArchiver: MessageBackupDistributionListRecipientArchiver
private let encryptedStreamProvider: MessageBackupEncryptedProtoStreamProvider
private let errorPresenter: MessageBackupErrorPresenter
private let fullTextSearchIndexer: MessageBackupFullTextSearchIndexer
private let groupRecipientArchiver: MessageBackupGroupRecipientArchiver
private let incrementalTSAttachmentMigrator: IncrementalMessageTSAttachmentMigrator
@ -68,6 +70,7 @@ public class MessageBackupManagerImpl: MessageBackupManager {
disappearingMessagesJob: OWSDisappearingMessagesJob,
distributionListRecipientArchiver: MessageBackupDistributionListRecipientArchiver,
encryptedStreamProvider: MessageBackupEncryptedProtoStreamProvider,
errorPresenter: MessageBackupErrorPresenter,
fullTextSearchIndexer: MessageBackupFullTextSearchIndexer,
groupRecipientArchiver: MessageBackupGroupRecipientArchiver,
incrementalTSAttachmentMigrator: IncrementalMessageTSAttachmentMigrator,
@ -96,6 +99,7 @@ public class MessageBackupManagerImpl: MessageBackupManager {
self.disappearingMessagesJob = disappearingMessagesJob
self.distributionListRecipientArchiver = distributionListRecipientArchiver
self.encryptedStreamProvider = encryptedStreamProvider
self.errorPresenter = errorPresenter
self.fullTextSearchIndexer = fullTextSearchIndexer
self.groupRecipientArchiver = groupRecipientArchiver
self.incrementalTSAttachmentMigrator = incrementalTSAttachmentMigrator
@ -236,6 +240,10 @@ public class MessageBackupManagerImpl: MessageBackupManager {
tx: DBWriteTransaction
) throws {
let startTimeMs = Date().ows_millisecondsSince1970
var errors = [LoggableErrorAndProto]()
defer {
self.processErrors(errors: errors, tx: tx)
}
try writeHeader(stream: stream, tx: tx)
@ -259,7 +267,7 @@ public class MessageBackupManagerImpl: MessageBackupManager {
case .success:
break
case .failure(let error):
MessageBackup.log([error])
errors.append(LoggableErrorAndProto(error: error))
throw OWSAssertionError("Failed to archive account data")
}
@ -271,7 +279,7 @@ public class MessageBackupManagerImpl: MessageBackupManager {
case .success(let success):
localRecipientId = success
case .failure(let error):
MessageBackup.log([error])
errors.append(LoggableErrorAndProto(error: error))
throw OWSAssertionError("Failed to archive local recipient!")
}
@ -290,7 +298,7 @@ public class MessageBackupManagerImpl: MessageBackupManager {
case .success:
break
case .failure(let error):
MessageBackup.log([error])
errors.append(LoggableErrorAndProto(error: error))
throw OWSAssertionError("Failed to archive release notes channel!")
}
@ -301,9 +309,10 @@ public class MessageBackupManagerImpl: MessageBackupManager {
case .success:
break
case .partialSuccess(let partialFailures):
try processArchiveFrameErrors(errors: partialFailures)
errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0) })
case .completeFailure(let error):
try processFatalArchivingError(error: error)
errors.append(LoggableErrorAndProto(error: error))
throw BackupError()
}
switch groupRecipientArchiver.archiveAllGroupRecipients(
@ -313,9 +322,10 @@ public class MessageBackupManagerImpl: MessageBackupManager {
case .success:
break
case .partialSuccess(let partialFailures):
try processArchiveFrameErrors(errors: partialFailures)
errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0) })
case .completeFailure(let error):
try processFatalArchivingError(error: error)
errors.append(LoggableErrorAndProto(error: error))
throw BackupError()
}
switch distributionListRecipientArchiver.archiveAllDistributionListRecipients(
@ -325,9 +335,10 @@ public class MessageBackupManagerImpl: MessageBackupManager {
case .success:
break
case .partialSuccess(let partialFailures):
try processArchiveFrameErrors(errors: partialFailures)
errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0) })
case .completeFailure(let error):
try processFatalArchivingError(error: error)
errors.append(LoggableErrorAndProto(error: error))
throw BackupError()
}
// TODO: [Backups] Archive call link recipients.
@ -347,9 +358,10 @@ public class MessageBackupManagerImpl: MessageBackupManager {
case .success:
break
case .partialSuccess(let partialFailures):
try processArchiveFrameErrors(errors: partialFailures)
errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0) })
case .completeFailure(let error):
try processFatalArchivingError(error: error)
errors.append(LoggableErrorAndProto(error: error))
throw BackupError()
}
let chatItemArchiveResult = chatItemArchiver.archiveInteractions(
@ -360,9 +372,10 @@ public class MessageBackupManagerImpl: MessageBackupManager {
case .success:
break
case .partialSuccess(let partialFailures):
try processArchiveFrameErrors(errors: partialFailures)
errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0) })
case .completeFailure(let error):
try processFatalArchivingError(error: error)
errors.append(LoggableErrorAndProto(error: error))
throw BackupError()
}
let archivingContext = MessageBackup.ArchivingContext(
@ -378,9 +391,10 @@ public class MessageBackupManagerImpl: MessageBackupManager {
case .success:
break
case .partialSuccess(let partialFailures):
try processArchiveFrameErrors(errors: partialFailures)
errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0) })
case .completeFailure(let error):
try processFatalArchivingError(error: error)
errors.append(LoggableErrorAndProto(error: error))
throw BackupError()
}
try stream.closeFileStream()
@ -412,23 +426,6 @@ public class MessageBackupManagerImpl: MessageBackupManager {
}
}
private func processArchiveFrameErrors<IdType>(
errors: [MessageBackup.ArchiveFrameError<IdType>]
) throws {
MessageBackup.log(errors)
// At time of writing, we want to fail for every single error.
if errors.isEmpty.negated {
throw BackupError()
}
}
private func processFatalArchivingError(
error: MessageBackup.FatalArchivingError
) throws {
MessageBackup.log([error])
throw BackupError()
}
// MARK: - Import
public func importEncryptedBackup(
@ -514,6 +511,11 @@ public class MessageBackupManagerImpl: MessageBackupManager {
) throws {
let startTimeMs = Date().ows_millisecondsSince1970
var frameErrors = [LoggableErrorAndProto]()
defer {
self.processErrors(errors: frameErrors, tx: tx)
}
let backupInfo: BackupProto_BackupInfo
var hasMoreFrames = false
switch stream.readHeader() {
@ -526,29 +528,35 @@ public class MessageBackupManagerImpl: MessageBackupManager {
throw OWSAssertionError("invalid empty header frame")
case .protoDeserializationError(let error):
// Fail if we fail to deserialize the header.
try processRestoreFrameErrors(errors: [.restoreFrameError(
frameErrors.append(LoggableErrorAndProto(error: MessageBackup.RestoreFrameError.restoreFrameError(
.invalidProtoData(.missingBackupInfoHeader),
MessageBackup.BackupInfoId()
)])
)))
throw error
}
Logger.info("Reading backup with version: \(backupInfo.version) backed up at \(backupInfo.backupTimeMs)")
guard backupInfo.version == Constants.supportedBackupVersion else {
try processRestoreFrameErrors(errors: [.restoreFrameError(
.invalidProtoData(.unsupportedBackupInfoVersion),
MessageBackup.BackupInfoId()
)])
frameErrors.append(LoggableErrorAndProto(
error: MessageBackup.RestoreFrameError.restoreFrameError(
.invalidProtoData(.unsupportedBackupInfoVersion),
MessageBackup.BackupInfoId()
),
protoFrame: backupInfo
))
throw BackupError()
}
do {
try mrbkStore.setMediaRootBackupKey(fromRestoredBackup: backupInfo, tx: tx)
} catch {
try processRestoreFrameErrors(errors: [.restoreFrameError(
.invalidProtoData(.invalidMediaRootBackupKey),
MessageBackup.BackupInfoId()
)])
frameErrors.append(LoggableErrorAndProto(
error: MessageBackup.RestoreFrameError.restoreFrameError(
.invalidProtoData(.invalidMediaRootBackupKey),
MessageBackup.BackupInfoId()
),
protoFrame: backupInfo
))
throw error
}
@ -648,9 +656,10 @@ public class MessageBackupManagerImpl: MessageBackupManager {
case .success:
continue
case .partialRestore(let errors):
try processRestoreFrameErrors(errors: errors)
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, protoFrame: recipient) })
case .failure(let errors):
try processRestoreFrameErrors(errors: errors)
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, protoFrame: recipient) })
throw BackupError()
}
case .chat(let chat):
let chatResult = chatArchiver.restore(
@ -661,9 +670,10 @@ public class MessageBackupManagerImpl: MessageBackupManager {
case .success:
continue
case .partialRestore(let errors):
try processRestoreFrameErrors(errors: errors)
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, protoFrame: chat) })
case .failure(let errors):
try processRestoreFrameErrors(errors: errors)
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, protoFrame: chat) })
throw BackupError()
}
case .chatItem(let chatItem):
let chatItemResult = chatItemArchiver.restore(
@ -674,9 +684,10 @@ public class MessageBackupManagerImpl: MessageBackupManager {
case .success:
continue
case .partialRestore(let errors):
try processRestoreFrameErrors(errors: errors)
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, protoFrame: chatItem) })
case .failure(let errors):
try processRestoreFrameErrors(errors: errors)
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, protoFrame: chatItem) })
throw BackupError()
}
case .account(let backupProtoAccountData):
let accountDataResult = accountDataArchiver.restore(
@ -688,9 +699,10 @@ public class MessageBackupManagerImpl: MessageBackupManager {
case .success:
continue
case .partialRestore(let errors):
try processRestoreFrameErrors(errors: errors)
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, protoFrame: backupProtoAccountData) })
case .failure(let errors):
try processRestoreFrameErrors(errors: errors)
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, protoFrame: backupProtoAccountData) })
throw BackupError()
}
case .stickerPack(let backupProtoStickerPack):
let stickerPackResult = stickerPackArchiver.restore(
@ -701,26 +713,27 @@ public class MessageBackupManagerImpl: MessageBackupManager {
case .success:
continue
case .partialRestore(let errors):
try processRestoreFrameErrors(errors: errors)
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, protoFrame: backupProtoStickerPack) })
case .failure(let errors):
try processRestoreFrameErrors(errors: errors)
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, protoFrame: backupProtoStickerPack) })
throw BackupError()
}
case .adHocCall(let backupProtoAdHocCall):
// TODO: [Backups] Restore ad-hoc calls.
try processRestoreFrameErrors(errors: [.restoreFrameError(
frameErrors.append(LoggableErrorAndProto(error: MessageBackup.RestoreFrameError.restoreFrameError(
.unimplemented,
MessageBackup.AdHocCallId(
backupProtoAdHocCall.callID,
recipientId: backupProtoAdHocCall.recipientID
)
)])
)))
case nil:
if hasMoreFrames {
owsFailDebug("Frame missing item!")
try processRestoreFrameErrors(errors: [.restoreFrameError(
frameErrors.append(LoggableErrorAndProto(error: MessageBackup.RestoreFrameError.restoreFrameError(
.invalidProtoData(.frameMissingItem),
MessageBackup.EmptyFrameId.shared
)])
)))
}
}
}
@ -752,16 +765,17 @@ public class MessageBackupManagerImpl: MessageBackupManager {
Logger.info("Imported \(stream.numberOfReadFrames) in \(endTimeMs - startTimeMs)ms")
}
private func processRestoreFrameErrors<IdType>(errors: [MessageBackup.RestoreFrameError<IdType>]) throws {
MessageBackup.log(errors)
// At time of writing, we want to fail for every single error.
if errors.isEmpty.negated {
throw BackupError()
}
}
// MARK: -
private func processErrors(
errors: [LoggableErrorAndProto],
tx: DBWriteTransaction
) {
let collapsedErrors = MessageBackup.collapse(errors)
collapsedErrors.forEach { $0.log() }
errorPresenter.persistErrors(collapsedErrors, tx: tx)
}
/// TSAttachments must be migrated to v2 Attachments before we can create or restore backups.
/// Normally this migration happens in the background; force it to run and finish now.
private func migrateAttachmentsBeforeBackup() async {

View File

@ -31,6 +31,7 @@ public class MockSSKEnvironment: NSObject {
currentCallProvider: CurrentCallNoOpProvider(),
notificationPresenter: NoopNotificationPresenterImpl(),
incrementalTSAttachmentMigrator: IncrementalMessageTSAttachmentMigratorMock(),
messageBackupErrorPresenterFactory: NoOpMessageBackupErrorPresenterFactory(),
testDependencies: AppSetup.TestDependencies(
accountServiceClient: FakeAccountServiceClient(),
contactManager: FakeContactsManager(),

View File

@ -42,6 +42,7 @@ public enum FeatureFlags {
public static let doNotSendGroupChangeMessagesOnProfileKeyRotation = false
public static let messageBackupErrorDisplay = build.includes(.internal)
public static let messageBackupFileAlpha = build.includes(.dev)
public static let messageBackupFileAlphaRegistrationFlow = build.includes(.dev)
public static let linkAndSync = build.includes(.dev)

View File

@ -452,6 +452,7 @@ class MessageBackupIntegrationTests: XCTestCase {
currentCallProvider: CrashyMocks.MockCurrentCallThreadProvider(),
notificationPresenter: CrashyMocks.MockNotificationPresenter(),
incrementalTSAttachmentMigrator: NoOpIncrementalMessageTSAttachmentMigrator(),
messageBackupErrorPresenterFactory: NoOpMessageBackupErrorPresenterFactory(),
testDependencies: AppSetup.TestDependencies(
backupAttachmentDownloadManager: BackupAttachmentDownloadManagerMock(),
dateProvider: dateProvider,

View File

@ -94,7 +94,8 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
callMessageHandler: NoopCallMessageHandler(),
currentCallProvider: CurrentCallNoOpProvider(),
notificationPresenter: NoopNotificationPresenterImpl(),
incrementalTSAttachmentMigrator: NoOpIncrementalMessageTSAttachmentMigrator()
incrementalTSAttachmentMigrator: NoOpIncrementalMessageTSAttachmentMigrator(),
messageBackupErrorPresenterFactory: NoOpMessageBackupErrorPresenterFactory()
)
// Configure the rest of the globals before preparing the database.