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 years ago
parent 0d80678a77
commit 44e7a2dfa4

@ -6830,7 +6830,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 356; CURRENT_PROJECT_VERSION = 357;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -6902,7 +6902,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 356; CURRENT_PROJECT_VERSION = 357;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",

@ -54,14 +54,25 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate, OWSVid
galleryItem.attachment.thumbnail( galleryItem.attachment.thumbnail(
size: .large, size: .large,
success: { [weak self] image, _ in success: { [weak self] image, _ in
self?.image = image
// Only reload the content if the view has already loaded (if it // Only reload the content if the view has already loaded (if it
// hasn't then it'll load with the image immediately) // hasn't then it'll load with the image immediately)
if self?.isViewLoaded == true { let updateUICallback = {
self?.updateContents() self?.image = image
self?.updateMinZoomScale()
if self?.isViewLoaded == true {
self?.updateContents()
self?.updateMinZoomScale()
}
}
guard Thread.isMainThread else {
DispatchQueue.main.async {
updateUICallback()
}
return
} }
updateUICallback()
}, },
failure: { failure: {
SNLog("Could not load media.") SNLog("Could not load media.")

@ -7,13 +7,9 @@ import SessionSnodeKit
import SessionMessagingKit import SessionMessagingKit
import SessionUtilitiesKit import SessionUtilitiesKit
@objc(LKBackgroundPoller) public final class BackgroundPoller {
public final class BackgroundPoller: NSObject {
private static var promises: [Promise<Void>] = [] private static var promises: [Promise<Void>] = []
private override init() { }
@objc(pollWithCompletionHandler:)
public static func poll(completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { public static func poll(completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
promises = [] promises = []
.appending(pollForMessages()) .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) when(resolved: promises)
.done { _ in .done { _ in
cancelTimer.invalidate()
completionHandler(.newData) completionHandler(.newData)
} }
.catch { error in .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)") SNLog("Background poll failed due to error: \(error)")
cancelTimer.invalidate()
completionHandler(.failed) completionHandler(.failed)
} }
} }
@ -74,7 +87,7 @@ public final class BackgroundPoller: NSObject {
ClosedGroupPoller.poll( ClosedGroupPoller.poll(
groupPublicKey, groupPublicKey,
on: DispatchQueue.main, on: DispatchQueue.main,
maxRetryCount: 4, maxRetryCount: 0,
isBackgroundPoll: true isBackgroundPoll: true
) )
} }
@ -85,78 +98,76 @@ public final class BackgroundPoller: NSObject {
.then(on: DispatchQueue.main) { swarm -> Promise<Void> in .then(on: DispatchQueue.main) { swarm -> Promise<Void> in
guard let snode = swarm.randomElement() else { throw SnodeAPIError.generic } guard let snode = swarm.randomElement() else { throw SnodeAPIError.generic }
return attempt(maxRetryCount: 4, recoveringOn: DispatchQueue.main) { return SnodeAPI.getMessages(from: snode, associatedWith: publicKey)
return SnodeAPI.getMessages(from: snode, associatedWith: publicKey) .then(on: DispatchQueue.main) { messages -> Promise<Void> in
.then(on: DispatchQueue.main) { messages -> Promise<Void> in guard !messages.isEmpty else { return Promise.value(()) }
guard !messages.isEmpty else { return Promise.value(()) }
var jobsToRun: [Job] = []
var jobsToRun: [Job] = []
Storage.shared.write { db in
var threadMessages: [String: [MessageReceiveJob.Details.MessageInfo]] = [:]
Storage.shared.write { db in messages.forEach { message in
var threadMessages: [String: [MessageReceiveJob.Details.MessageInfo]] = [:] do {
let processedMessage: ProcessedMessage? = try Message.processRawReceivedMessage(db, rawMessage: message)
messages.forEach { message in let key: String = (processedMessage?.threadId ?? Message.nonThreadMessageId)
do {
let processedMessage: ProcessedMessage? = try Message.processRawReceivedMessage(db, rawMessage: message) threadMessages[key] = (threadMessages[key] ?? [])
let key: String = (processedMessage?.threadId ?? Message.nonThreadMessageId) .appending(processedMessage?.messageInfo)
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).")
}
}
} }
catch {
threadMessages switch error {
.forEach { threadId, threadMessages in // Ignore duplicate & selfSend message errors (and don't bother logging
let maybeJob: Job? = Job( // them as there will be a lot since we each service node duplicates messages)
variant: .messageReceive, case DatabaseError.SQLITE_CONSTRAINT_UNIQUE,
behaviour: .runOnce, MessageReceiverError.duplicateMessage,
threadId: threadId, MessageReceiverError.duplicateControlMessage,
details: MessageReceiveJob.Details( MessageReceiverError.selfSend:
messages: threadMessages, break
isBackgroundPoll: true
)
)
guard let job: Job = maybeJob else { return }
// Add to the JobRunner so they are persistent and will retry on default: SNLog("Failed to deserialize envelope due to error: \(error).")
// 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 threadMessages
let (promise, seal) = Promise<Void>.pending() .forEach { threadId, threadMessages in
let maybeJob: Job? = Job(
// Note: In the background we just want jobs to fail silently variant: .messageReceive,
MessageReceiveJob.run( behaviour: .runOnce,
job, threadId: threadId,
queue: DispatchQueue.main, details: MessageReceiveJob.Details(
success: { _, _ in seal.fulfill(()) }, messages: threadMessages,
failure: { _, _, _ in seal.fulfill(()) }, isBackgroundPoll: true
deferred: { _ in seal.fulfill(()) } )
) )
return promise 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)
}
} }
} }
} }

@ -212,13 +212,13 @@ public enum OnionRequestAPI: OnionRequestAPIType {
} }
// randomElement() uses the system's default random generator, which is cryptographically secure // randomElement() uses the system's default random generator, which is cryptographically secure
if paths.count >= targetPathCount { if
if let snode: Snode = snode { paths.count >= targetPathCount,
return Promise { $0.fulfill(paths.filter { !$0.contains(snode) }.randomElement()!) } let targetPath: [Snode] = paths
} .filter({ snode == nil || !$0.contains(snode!) })
else { .randomElement()
return Promise { $0.fulfill(paths.randomElement()!) } {
} return Promise { $0.fulfill(targetPath) }
} }
else if !paths.isEmpty { else if !paths.isEmpty {
if let snode = snode { if let snode = snode {
@ -228,13 +228,22 @@ public enum OnionRequestAPI: OnionRequestAPIType {
} }
else { else {
return buildPaths(reusing: paths).map2 { paths in 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 { else {
buildPaths(reusing: paths) // Re-build paths in the background 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 { else {
@ -247,7 +256,11 @@ public enum OnionRequestAPI: OnionRequestAPIType {
throw OnionRequestAPIError.insufficientSnodes throw OnionRequestAPIError.insufficientSnodes
} }
return paths.randomElement()! guard let path: [Snode] = paths.randomElement() else {
throw OnionRequestAPIError.insufficientSnodes
}
return path
} }
} }
} }

Loading…
Cancel
Save