Browse Source

Added defensive coding to prevent some crashes

Added some defensive coding to prevent path selection from being able to crash due to being empty
Fixed a crash where the MediaDetailViewController could access UI on a non-main thread
Updated the BackgroundPoller to no longer retry the users or closed group swarms and to "cancel" and return immediately if we hit 25 seconds of run time (OS will kill the process if we hit 30 seconds)
pull/612/head
Morgan Pretty 2 months ago
parent
commit
44e7a2dfa4
  1. 4
      Session.xcodeproj/project.pbxproj
  2. 21
      Session/Media Viewing & Editing/MediaDetailViewController.swift
  3. 151
      Session/Utilities/BackgroundPoller.swift
  4. 33
      SessionSnodeKit/OnionRequestAPI.swift

4
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)",

21
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.")

151
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<Void>] = []
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<Void> 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<Void> in
guard !messages.isEmpty else { return Promise.value(()) }
var jobsToRun: [Job] = []
return SnodeAPI.getMessages(from: snode, associatedWith: publicKey)
.then(on: DispatchQueue.main) { messages -> Promise<Void> 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<Void>] = jobsToRun.map { job -> Promise<Void> in
let (promise, seal) = Promise<Void>.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<Void>] = jobsToRun.map { job -> Promise<Void> in
let (promise, seal) = Promise<Void>.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)
}
}
}
}

33
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
}
}
}

Loading…
Cancel
Save