diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 4ed98f633..e6a50a7d4 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -6830,7 +6830,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 356; + CURRENT_PROJECT_VERSION = 357; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6902,7 +6902,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 356; + CURRENT_PROJECT_VERSION = 357; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/Session/Media Viewing & Editing/MediaDetailViewController.swift b/Session/Media Viewing & Editing/MediaDetailViewController.swift index 0ae1ff5a3..80538eb4a 100644 --- a/Session/Media Viewing & Editing/MediaDetailViewController.swift +++ b/Session/Media Viewing & Editing/MediaDetailViewController.swift @@ -54,14 +54,25 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate, OWSVid galleryItem.attachment.thumbnail( size: .large, success: { [weak self] image, _ in - self?.image = image - // Only reload the content if the view has already loaded (if it // hasn't then it'll load with the image immediately) - if self?.isViewLoaded == true { - self?.updateContents() - self?.updateMinZoomScale() + let updateUICallback = { + self?.image = image + + if self?.isViewLoaded == true { + self?.updateContents() + self?.updateMinZoomScale() + } + } + + guard Thread.isMainThread else { + DispatchQueue.main.async { + updateUICallback() + } + return } + + updateUICallback() }, failure: { SNLog("Could not load media.") diff --git a/Session/Utilities/BackgroundPoller.swift b/Session/Utilities/BackgroundPoller.swift index 8841286d3..9a430ec5e 100644 --- a/Session/Utilities/BackgroundPoller.swift +++ b/Session/Utilities/BackgroundPoller.swift @@ -7,13 +7,9 @@ import SessionSnodeKit import SessionMessagingKit import SessionUtilitiesKit -@objc(LKBackgroundPoller) -public final class BackgroundPoller: NSObject { +public final class BackgroundPoller { private static var promises: [Promise] = [] - private override init() { } - - @objc(pollWithCompletionHandler:) public static func poll(completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { promises = [] .appending(pollForMessages()) @@ -40,12 +36,29 @@ public final class BackgroundPoller: NSObject { } ) + // Background tasks will automatically be terminated after 30 seconds (which results in a crash + // and a prompt to appear for the user) we want to avoid this so we start a timer which expires + // after 25 seconds allowing us to cancel all pending promises + let cancelTimer: Timer = Timer.scheduledTimerOnMainThread(withTimeInterval: 25, repeats: false) { timer in + timer.invalidate() + + guard promises.contains(where: { !$0.isResolved }) else { return } + + SNLog("Background poll failed due to manual timeout") + completionHandler(.failed) + } + when(resolved: promises) .done { _ in + cancelTimer.invalidate() completionHandler(.newData) } .catch { error in + // If we have already invalidated the timer then do nothing (we essentially timed out) + guard cancelTimer.isValid else { return } + SNLog("Background poll failed due to error: \(error)") + cancelTimer.invalidate() completionHandler(.failed) } } @@ -74,7 +87,7 @@ public final class BackgroundPoller: NSObject { ClosedGroupPoller.poll( groupPublicKey, on: DispatchQueue.main, - maxRetryCount: 4, + maxRetryCount: 0, isBackgroundPoll: true ) } @@ -85,78 +98,76 @@ public final class BackgroundPoller: NSObject { .then(on: DispatchQueue.main) { swarm -> Promise in guard let snode = swarm.randomElement() else { throw SnodeAPIError.generic } - return attempt(maxRetryCount: 4, recoveringOn: DispatchQueue.main) { - return SnodeAPI.getMessages(from: snode, associatedWith: publicKey) - .then(on: DispatchQueue.main) { messages -> Promise in - guard !messages.isEmpty else { return Promise.value(()) } - - var jobsToRun: [Job] = [] + return SnodeAPI.getMessages(from: snode, associatedWith: publicKey) + .then(on: DispatchQueue.main) { messages -> Promise in + guard !messages.isEmpty else { return Promise.value(()) } + + var jobsToRun: [Job] = [] + + Storage.shared.write { db in + var threadMessages: [String: [MessageReceiveJob.Details.MessageInfo]] = [:] - Storage.shared.write { db in - var threadMessages: [String: [MessageReceiveJob.Details.MessageInfo]] = [:] - - messages.forEach { message in - do { - let processedMessage: ProcessedMessage? = try Message.processRawReceivedMessage(db, rawMessage: message) - let key: String = (processedMessage?.threadId ?? Message.nonThreadMessageId) - - threadMessages[key] = (threadMessages[key] ?? []) - .appending(processedMessage?.messageInfo) - } - catch { - switch error { - // Ignore duplicate & selfSend message errors (and don't bother logging - // them as there will be a lot since we each service node duplicates messages) - case DatabaseError.SQLITE_CONSTRAINT_UNIQUE, - MessageReceiverError.duplicateMessage, - MessageReceiverError.duplicateControlMessage, - MessageReceiverError.selfSend: - break - - default: SNLog("Failed to deserialize envelope due to error: \(error).") - } - } + messages.forEach { message in + do { + let processedMessage: ProcessedMessage? = try Message.processRawReceivedMessage(db, rawMessage: message) + let key: String = (processedMessage?.threadId ?? Message.nonThreadMessageId) + + threadMessages[key] = (threadMessages[key] ?? []) + .appending(processedMessage?.messageInfo) } - - threadMessages - .forEach { threadId, threadMessages in - let maybeJob: Job? = Job( - variant: .messageReceive, - behaviour: .runOnce, - threadId: threadId, - details: MessageReceiveJob.Details( - messages: threadMessages, - isBackgroundPoll: true - ) - ) - - guard let job: Job = maybeJob else { return } + catch { + switch error { + // Ignore duplicate & selfSend message errors (and don't bother logging + // them as there will be a lot since we each service node duplicates messages) + case DatabaseError.SQLITE_CONSTRAINT_UNIQUE, + MessageReceiverError.duplicateMessage, + MessageReceiverError.duplicateControlMessage, + MessageReceiverError.selfSend: + break - // Add to the JobRunner so they are persistent and will retry on - // the next app run if they fail - JobRunner.add(db, job: job, canStartJob: false) - jobsToRun.append(job) + default: SNLog("Failed to deserialize envelope due to error: \(error).") } + } } - let promises: [Promise] = jobsToRun.map { job -> Promise in - let (promise, seal) = Promise.pending() - - // Note: In the background we just want jobs to fail silently - MessageReceiveJob.run( - job, - queue: DispatchQueue.main, - success: { _, _ in seal.fulfill(()) }, - failure: { _, _, _ in seal.fulfill(()) }, - deferred: { _ in seal.fulfill(()) } - ) - - return promise - } + threadMessages + .forEach { threadId, threadMessages in + let maybeJob: Job? = Job( + variant: .messageReceive, + behaviour: .runOnce, + threadId: threadId, + details: MessageReceiveJob.Details( + messages: threadMessages, + isBackgroundPoll: true + ) + ) + + guard let job: Job = maybeJob else { return } + + // Add to the JobRunner so they are persistent and will retry on + // the next app run if they fail + JobRunner.add(db, job: job, canStartJob: false) + jobsToRun.append(job) + } + } + + let promises: [Promise] = jobsToRun.map { job -> Promise in + let (promise, seal) = Promise.pending() + + // Note: In the background we just want jobs to fail silently + MessageReceiveJob.run( + job, + queue: DispatchQueue.main, + success: { _, _ in seal.fulfill(()) }, + failure: { _, _, _ in seal.fulfill(()) }, + deferred: { _ in seal.fulfill(()) } + ) - return when(fulfilled: promises) + return promise } - } + + return when(fulfilled: promises) + } } } } diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index 0d69334d4..3efd5d4ba 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -212,13 +212,13 @@ public enum OnionRequestAPI: OnionRequestAPIType { } // randomElement() uses the system's default random generator, which is cryptographically secure - if paths.count >= targetPathCount { - if let snode: Snode = snode { - return Promise { $0.fulfill(paths.filter { !$0.contains(snode) }.randomElement()!) } - } - else { - return Promise { $0.fulfill(paths.randomElement()!) } - } + if + paths.count >= targetPathCount, + let targetPath: [Snode] = paths + .filter({ snode == nil || !$0.contains(snode!) }) + .randomElement() + { + return Promise { $0.fulfill(targetPath) } } else if !paths.isEmpty { if let snode = snode { @@ -228,13 +228,22 @@ public enum OnionRequestAPI: OnionRequestAPIType { } else { return buildPaths(reusing: paths).map2 { paths in - return paths.filter { !$0.contains(snode) }.randomElement()! + guard let path: [Snode] = paths.filter({ !$0.contains(snode) }).randomElement() else { + throw OnionRequestAPIError.insufficientSnodes + } + + return path } } } else { buildPaths(reusing: paths) // Re-build paths in the background - return Promise { $0.fulfill(paths.randomElement()!) } + + guard let path: [Snode] = paths.randomElement() else { + return Promise(error: OnionRequestAPIError.insufficientSnodes) + } + + return Promise { $0.fulfill(path) } } } else { @@ -247,7 +256,11 @@ public enum OnionRequestAPI: OnionRequestAPIType { throw OnionRequestAPIError.insufficientSnodes } - return paths.randomElement()! + guard let path: [Snode] = paths.randomElement() else { + throw OnionRequestAPIError.insufficientSnodes + } + + return path } } }