Add internal-only UI to display verbose backup errors
This commit is contained in:
parent
a6fd1765f9
commit
9ba009a1e3
@ -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 */,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -114,6 +114,7 @@ public class RegistrationCoordinatorTest: XCTestCase {
|
||||
featureFlags: featureFlags,
|
||||
keyValueStoreFactory: InMemoryKeyValueStoreFactory(),
|
||||
localUsernameManager: localUsernameManagerMock,
|
||||
messageBackupErrorPresenter: NoOpMessageBackupErrorPresenter(),
|
||||
messageBackupManager: MessageBackupManagerMock(),
|
||||
messagePipelineSupervisor: mockMessagePipelineSupervisor,
|
||||
messageProcessor: mockMessageProcessor,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -31,6 +31,7 @@ public class MockSSKEnvironment: NSObject {
|
||||
currentCallProvider: CurrentCallNoOpProvider(),
|
||||
notificationPresenter: NoopNotificationPresenterImpl(),
|
||||
incrementalTSAttachmentMigrator: IncrementalMessageTSAttachmentMigratorMock(),
|
||||
messageBackupErrorPresenterFactory: NoOpMessageBackupErrorPresenterFactory(),
|
||||
testDependencies: AppSetup.TestDependencies(
|
||||
accountServiceClient: FakeAccountServiceClient(),
|
||||
contactManager: FakeContactsManager(),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -452,6 +452,7 @@ class MessageBackupIntegrationTests: XCTestCase {
|
||||
currentCallProvider: CrashyMocks.MockCurrentCallThreadProvider(),
|
||||
notificationPresenter: CrashyMocks.MockNotificationPresenter(),
|
||||
incrementalTSAttachmentMigrator: NoOpIncrementalMessageTSAttachmentMigrator(),
|
||||
messageBackupErrorPresenterFactory: NoOpMessageBackupErrorPresenterFactory(),
|
||||
testDependencies: AppSetup.TestDependencies(
|
||||
backupAttachmentDownloadManager: BackupAttachmentDownloadManagerMock(),
|
||||
dateProvider: dateProvider,
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user