diff --git a/.drone.jsonnet b/.drone.jsonnet index 99cfbe705..99934153f 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -227,7 +227,7 @@ local sim_delete_cmd = 'if [ -f build/artifacts/sim_uuid ]; then rm -f /Users/$U name: 'Build', commands: [ 'mkdir build', - 'NSUnbufferedIO=YES set -o pipefail && xcodebuild archive -workspace Session.xcworkspace -scheme Session -derivedDataPath ./build/derivedData -parallelizeTargets -configuration "App Store Release" -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator" | xcbeautify --is-ci', + 'NSUnbufferedIO=YES set -o pipefail && xcodebuild archive -workspace Session.xcworkspace -scheme Session -derivedDataPath ./build/derivedData -parallelizeTargets -configuration "App_Store_Release" -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator" | xcbeautify --is-ci', ], depends_on: [ 'Install CocoaPods', diff --git a/LibSession-Util b/LibSession-Util index 714230c0c..fdfd9dbb6 160000 --- a/LibSession-Util +++ b/LibSession-Util @@ -1 +1 @@ -Subproject commit 714230c0c8867ac8662603fd6debb5b4c4369ef1 +Subproject commit fdfd9dbb698e4ce6e6e08dd1b85eec428ab2077e diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 8536b6560..f151af370 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -870,6 +870,8 @@ FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; }; FDDC08F229A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */; }; + FDDD554C2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDD554B2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift */; }; + FDDD554E2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */; }; FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */; }; FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */; }; FDE125232A837E4E002DA685 /* MainAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE125222A837E4E002DA685 /* MainAppContext.swift */; }; @@ -2065,6 +2067,8 @@ FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = ""; }; FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessResult.swift; sourceTree = ""; }; FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionTypeConversionUtilitiesSpec.swift; sourceTree = ""; }; + FDDD554B2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForAppUpdatesJob.swift; sourceTree = ""; }; + FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _019_ScheduleAppUpdateCheckJob.swift; sourceTree = ""; }; FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequest+Utilities.swift"; sourceTree = ""; }; FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerSpec.swift; sourceTree = ""; }; FDE125222A837E4E002DA685 /* MainAppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAppContext.swift; sourceTree = ""; }; @@ -3755,6 +3759,7 @@ FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */, FD428B222B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift */, 7B5233C5290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift */, + FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */, ); path = Migrations; sourceTree = ""; @@ -4454,6 +4459,7 @@ FD2B4AFE2946C93200AB4848 /* ConfigurationSyncJob.swift */, 7B7E5B512A4D024C00A8208E /* ExpirationUpdateJob.swift */, 7B7AD41E2A5512CA00469FB1 /* GetExpirationJob.swift */, + FDDD554B2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift */, ); path = Types; sourceTree = ""; @@ -6177,6 +6183,7 @@ 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */, 7B7E5B522A4D024C00A8208E /* ExpirationUpdateJob.swift in Sources */, FD09799727FFA84A00936362 /* RecipientState.swift in Sources */, + FDDD554E2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift in Sources */, FDA8EB00280E8D58002B68E5 /* FailedAttachmentDownloadsJob.swift in Sources */, FD09798927FD1C5A00936362 /* OpenGroup.swift in Sources */, FD848B9628422A2A000E298B /* MessageViewModel.swift in Sources */, @@ -6279,6 +6286,7 @@ C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */, FD716E682850318E00C96BF4 /* CallMode.swift in Sources */, FD09799527FE7B8E00936362 /* Interaction.swift in Sources */, + FDDD554C2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift in Sources */, FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */, 7BAA7B6628D2DE4700AE1489 /* _009_OpenGroupPermission.swift in Sources */, FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */, @@ -8034,7 +8042,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 449; + CURRENT_PROJECT_VERSION = 452; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8071,7 +8079,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.6.1; + MARKETING_VERSION = 2.6.2; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-fobjc-arc-exceptions", @@ -8112,7 +8120,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 449; + CURRENT_PROJECT_VERSION = 452; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -8144,7 +8152,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.6.1; + MARKETING_VERSION = 2.6.2; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 443568155..12b244c7c 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -298,7 +298,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD guard BackgroundPoller.isValid else { return } - Log.info("Background poll failed due to manual timeout") + Log.info("Background poll failed due to manual timeout.") BackgroundPoller.isValid = false if Singleton.hasAppContext && Singleton.appContext.isInBackground { @@ -321,7 +321,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // the app after this closure is registered but before it's actually triggered - this can // result in the `BackgroundPoller` incorrectly getting called in the foreground, this check // is here to prevent that - guard Singleton.hasAppContext && Singleton.appContext.isInBackground else { return } + guard Singleton.hasAppContext && Singleton.appContext.isInBackground else { + BackgroundPoller.isValid = false + return + } BackgroundPoller.poll { result in guard BackgroundPoller.isValid else { return } diff --git a/Session/Utilities/BackgroundPoller.swift b/Session/Utilities/BackgroundPoller.swift index 782650752..9f32adc3c 100644 --- a/Session/Utilities/BackgroundPoller.swift +++ b/Session/Utilities/BackgroundPoller.swift @@ -15,46 +15,50 @@ public final class BackgroundPoller { completionHandler: @escaping (UIBackgroundFetchResult) -> Void, using dependencies: Dependencies = Dependencies() ) { + let (groupIds, servers): (Set, Set) = Storage.shared.read { db in + ( + try ClosedGroup + .select(.threadId) + .joining( + required: ClosedGroup.members + .filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db)) + ) + .asRequest(of: String.self) + .fetchSet(db), + /// The default room promise creates an OpenGroup with an empty `roomToken` value, we + /// don't want to start a poller for this as the user hasn't actually joined a room + /// + /// We also want to exclude any rooms which have failed to poll too many times in a row from + /// the background poll as they are likely to fail again + try OpenGroup + .select(.server) + .filter( + OpenGroup.Columns.roomToken != "" && + OpenGroup.Columns.isActive && + OpenGroup.Columns.pollFailureCount < OpenGroupAPI.Poller.maxRoomFailureCountForBackgroundPoll + ) + .distinct() + .asRequest(of: String.self) + .fetchSet(db) + ) + } + .defaulting(to: ([], [])) + + Log.info("[BackgroundPoller] Fetching 1 User, \(groupIds.count) \("group", number: groupIds.count), \(servers.count) \("communit", number: servers.count, singular: "y", plural: "ies").") Publishers .MergeMany( [pollForMessages(using: dependencies)] - .appending(contentsOf: pollForClosedGroupMessages(using: dependencies)) - .appending( - contentsOf: Storage.shared - .read { db in - /// The default room promise creates an OpenGroup with an empty `roomToken` value, we - /// don't want to start a poller for this as the user hasn't actually joined a room - /// - /// We also want to exclude any rooms which have failed to poll too many times in a row from - /// the background poll as they are likely to fail again - try OpenGroup - .select(.server) - .filter( - OpenGroup.Columns.roomToken != "" && - OpenGroup.Columns.isActive && - OpenGroup.Columns.pollFailureCount < OpenGroupAPI.Poller.maxRoomFailureCountForBackgroundPoll - ) - .distinct() - .asRequest(of: String.self) - .fetchSet(db) - } - .defaulting(to: []) - .map { server -> AnyPublisher in - let poller: OpenGroupAPI.Poller = OpenGroupAPI.Poller(for: server) - poller.stop() - - return poller.poll( - calledFromBackgroundPoller: true, - isBackgroundPollerValid: { BackgroundPoller.isValid }, - isPostCapabilitiesRetry: false, - using: dependencies - ) - } - ) + .appending(contentsOf: pollForClosedGroupMessages(groupIds: groupIds, using: dependencies)) + .appending(contentsOf: pollForCommunityMessages(servers: servers, using: dependencies)) ) .subscribe(on: DispatchQueue.global(qos: .background), using: dependencies) .receive(on: DispatchQueue.main, using: dependencies) .collect() + .handleEvents( + receiveOutput: { _ in + Log.info("[BackgroundPoller] Finished polling.") + } + ) .sinkUntilComplete( receiveCompletion: { result in // If we have already invalidated the timer then do nothing (we essentially timed out) @@ -63,7 +67,7 @@ public final class BackgroundPoller { switch result { case .finished: completionHandler(.newData) case .failure(let error): - SNLog("Background poll failed due to error: \(error)") + Log.error("[BackgroundPoller] Failed due to error: \(error).") completionHandler(.failed) } } @@ -83,39 +87,55 @@ public final class BackgroundPoller { drainBehaviour: .alwaysRandom, using: dependencies ) + .handleEvents( + receiveOutput: { _, _, validMessageCount, _ in + Log.info("[BackgroundPoller] Received \(validMessageCount) valid \("message", number: validMessageCount).") + } + ) .map { _ in () } .eraseToAnyPublisher() } private static func pollForClosedGroupMessages( + groupIds: Set, using dependencies: Dependencies ) -> [AnyPublisher] { // Fetch all closed groups (excluding any don't contain the current user as a // GroupMemeber as the user is no longer a member of those) - return Storage.shared - .read { db in - try ClosedGroup - .select(.threadId) - .joining( - required: ClosedGroup.members - .filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db)) - ) - .asRequest(of: String.self) - .fetchAll(db) - } - .defaulting(to: []) - .map { groupPublicKey in - return ClosedGroupPoller() - .poll( - namespaces: ClosedGroupPoller.namespaces, - for: groupPublicKey, - calledFromBackgroundPoller: true, - isBackgroundPollValid: { BackgroundPoller.isValid }, - drainBehaviour: .alwaysRandom, - using: dependencies - ) - .map { _ in () } - .eraseToAnyPublisher() - } + return groupIds.map { groupPublicKey in + return ClosedGroupPoller() + .poll( + namespaces: ClosedGroupPoller.namespaces, + for: groupPublicKey, + calledFromBackgroundPoller: true, + isBackgroundPollValid: { BackgroundPoller.isValid }, + drainBehaviour: .alwaysRandom, + using: dependencies + ) + .handleEvents( + receiveOutput: { _, _, validMessageCount, _ in + Log.info("[BackgroundPoller] Received \(validMessageCount) valid \("message", number: validMessageCount) for group: \(groupPublicKey).") + } + ) + .map { _ in () } + .eraseToAnyPublisher() + } + } + + private static func pollForCommunityMessages( + servers: Set, + using dependencies: Dependencies + ) -> [AnyPublisher] { + return servers.map { server -> AnyPublisher in + let poller: OpenGroupAPI.Poller = OpenGroupAPI.Poller(for: server) + poller.stop() + + return poller.poll( + calledFromBackgroundPoller: true, + isBackgroundPollerValid: { BackgroundPoller.isValid }, + isPostCapabilitiesRetry: false, + using: dependencies + ) + } } } diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 83d167986..db31e6d27 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -35,7 +35,8 @@ public enum SNMessagingKit: MigratableTarget { // Just to make the external API _015_BlockCommunityMessageRequests.self, _016_MakeBrokenProfileTimestampsNullable.self, _017_RebuildFTSIfNeeded_2_4_5.self, - _018_DisappearingMessagesConfiguration.self + _018_DisappearingMessagesConfiguration.self, + _019_ScheduleAppUpdateCheckJob.self ] ] ) @@ -60,5 +61,6 @@ public enum SNMessagingKit: MigratableTarget { // Just to make the external API JobRunner.setExecutor(ConfigMessageReceiveJob.self, for: .configMessageReceive) JobRunner.setExecutor(ExpirationUpdateJob.self, for: .expirationUpdate) JobRunner.setExecutor(GetExpirationJob.self, for: .getExpiration) + JobRunner.setExecutor(CheckForAppUpdatesJob.self, for: .checkForAppUpdates) } } diff --git a/SessionMessagingKit/Database/Migrations/_019_ScheduleAppUpdateCheckJob.swift b/SessionMessagingKit/Database/Migrations/_019_ScheduleAppUpdateCheckJob.swift new file mode 100644 index 000000000..504a2d87d --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_019_ScheduleAppUpdateCheckJob.swift @@ -0,0 +1,25 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +enum _019_ScheduleAppUpdateCheckJob: Migration { + static let target: TargetMigrations.Identifier = .messagingKit + static let identifier: String = "ScheduleAppUpdateCheckJob" // stringlint:disable + static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.1 + static var requirements: [MigrationRequirement] = [.libSessionStateLoaded] + static let fetchedTables: [(TableRecord & FetchableRecord).Type] = [] + static let createdOrAlteredTables: [(TableRecord & FetchableRecord).Type] = [] + static let droppedTables: [(TableRecord & FetchableRecord).Type] = [] + + static func migrate(_ db: GRDB.Database) throws { + _ = try Job( + variant: .checkForAppUpdates, + behaviour: .recurring + ).migrationSafeInserted(db) + + Storage.update(progress: 1, for: self, in: target) // In case this is the last migration + } +} diff --git a/SessionMessagingKit/Jobs/Types/CheckForAppUpdatesJob.swift b/SessionMessagingKit/Jobs/Types/CheckForAppUpdatesJob.swift new file mode 100644 index 000000000..b5f96a2a0 --- /dev/null +++ b/SessionMessagingKit/Jobs/Types/CheckForAppUpdatesJob.swift @@ -0,0 +1,56 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Combine +import SessionUtilitiesKit +import SessionSnodeKit + +public enum CheckForAppUpdatesJob: JobExecutor { + private static let updateCheckFrequency: TimeInterval = (4 * 60 * 60) // Max every 4 hours + public static var maxFailureCount: Int = -1 + public static var requiresThreadId: Bool = false + public static let requiresInteractionId: Bool = false + + public static func run( + _ job: Job, + queue: DispatchQueue, + success: @escaping (Job, Bool, Dependencies) -> (), + failure: @escaping (Job, Error?, Bool, Dependencies) -> (), + deferred: @escaping (Job, Dependencies) -> (), + using dependencies: Dependencies + ) { + dependencies.storage + .readPublisher(using: dependencies) { db -> [UInt8]? in Identity.fetchUserEd25519KeyPair(db)?.secretKey } + .subscribe(on: queue) + .receive(on: queue) + .tryFlatMap { maybeEd25519SecretKey in + guard let ed25519SecretKey: [UInt8] = maybeEd25519SecretKey else { throw StorageError.objectNotFound } + + return LibSession.checkClientVersion( + ed25519SecretKey: ed25519SecretKey, + using: dependencies + ) + } + .sinkUntilComplete( + receiveCompletion: { _ in + var updatedJob: Job = job.with( + failureCount: 0, + nextRunTimestamp: (dependencies.dateNow.timeIntervalSince1970 + updateCheckFrequency) + ) + + dependencies.storage.write(using: dependencies) { db in + try updatedJob.save(db) + } + + success(updatedJob, false, dependencies) + }, + receiveValue: { _, versionInfo in + switch versionInfo.prerelease { + case .none: Log.info("[CheckForAppUpdatesJob] Latest version: \(versionInfo.version)") + case .some(let prerelease): + Log.info("[CheckForAppUpdatesJob] Latest version: \(versionInfo.version), pre-release version: \(prerelease.version)") + } + } + ) + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift index d3269bf7d..db480d14b 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift @@ -49,7 +49,7 @@ public final class ClosedGroupPoller: Poller { // MARK: - Abstract Methods override func pollerName(for publicKey: String) -> String { - return "closed group with public key: \(publicKey)" + return "Closed group poller with public key: \(publicKey)" } override func nextPollDelay(for publicKey: String, using dependencies: Dependencies) -> TimeInterval { @@ -80,8 +80,7 @@ public final class ClosedGroupPoller: Poller { return nextPollInterval } - override func handlePollError(_ error: Error, for publicKey: String, using dependencies: Dependencies) -> Bool { - SNLog("Polling failed for closed group with public key: \(publicKey) due to error: \(error).") - return true + override func handlePollError(_ error: Error, for publicKey: String, using dependencies: Dependencies) -> PollerErrorResponse { + return .continuePolling } } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift index 827d29d2f..2e19c3076 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift @@ -63,7 +63,7 @@ public final class CurrentUserPoller: Poller { return min(maxRetryInterval, nextDelay) } - override func handlePollError(_ error: Error, for publicKey: String, using dependencies: Dependencies) -> Bool { + override func handlePollError(_ error: Error, for publicKey: String, using dependencies: Dependencies) -> PollerErrorResponse { if UserDefaults.sharedLokiProject?[.isMainAppActive] != true { // Do nothing when an error gets throws right after returning from the background (happens frequently) } @@ -71,13 +71,13 @@ public final class CurrentUserPoller: Poller { let drainBehaviour: Atomic = drainBehaviour.wrappedValue[publicKey], case .limitedReuse(_, .some(let targetSnode), _, _, _) = drainBehaviour.wrappedValue { - SNLog("Main Poller polling \(targetSnode) failed with error: \(period: "\(error)"); switching to next snode.") drainBehaviour.mutate { $0 = $0.clearTargetSnode() } + return .continuePollingInfo("Switching from \(targetSnode) to next snode.") } else { - SNLog("Polling failed due to having no target service node.") + return .continuePollingInfo("Had no target snode.") } - return true + return .continuePolling } } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift index ce25ea0ce..92495786a 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift @@ -17,6 +17,12 @@ public class Poller { hadValidHashUpdate: Bool ) + internal enum PollerErrorResponse { + case stopPolling + case continuePolling + case continuePollingInfo(String) + } + private var cancellables: Atomic<[String: AnyCancellable]> = Atomic([:]) internal var isPolling: Atomic<[String: Bool]> = Atomic([:]) internal var pollCount: Atomic<[String: Int]> = Atomic([:]) @@ -72,7 +78,7 @@ public class Poller { } /// Perform and logic which should occur when the poll errors, will stop polling if `false` is returned - internal func handlePollError(_ error: Error, for publicKey: String, using dependencies: Dependencies) -> Bool { + internal func handlePollError(_ error: Error, for publicKey: String, using dependencies: Dependencies) -> PollerErrorResponse { preconditionFailure("abstract class - override in subclass") } @@ -125,18 +131,24 @@ public class Poller { .sink( receiveCompletion: { _ in }, // Never called receiveValue: { result in + // If the polling has been cancelled then don't continue + guard self?.isPolling.wrappedValue[swarmPublicKey] == true else { return } + let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 // Log information about the poll switch result { case .failure(let error): // Determine if the error should stop us from polling anymore - guard self?.handlePollError(error, for: swarmPublicKey, using: dependencies) == true else { - return + switch self?.handlePollError(error, for: swarmPublicKey, using: dependencies) { + case .stopPolling: return + case .continuePollingInfo(let info): + Log.error("\(pollerName) failed to process any messages due to error: \(error). \(info)") + + case .continuePolling, .none: + Log.error("\(pollerName) failed to process any messages due to error: \(error).") } - Log.error("\(pollerName) failed to process any messages due to error: \(error)") - case .success(let response): let duration: TimeUnit = .seconds(endTime - lastPollStart) diff --git a/SessionNotificationServiceExtension/NotificationError.swift b/SessionNotificationServiceExtension/NotificationError.swift index abf243ecf..355c5563c 100644 --- a/SessionNotificationServiceExtension/NotificationError.swift +++ b/SessionNotificationServiceExtension/NotificationError.swift @@ -8,6 +8,7 @@ import SessionMessagingKit enum NotificationError: Error, CustomStringConvertible { case processing(PushNotificationAPI.ProcessResult) case messageProcessing + case ignorableMessage case messageHandling(MessageReceiverError) case other(Error) @@ -15,6 +16,7 @@ enum NotificationError: Error, CustomStringConvertible { switch self { case .processing(let result): return "Failed to process notification (\(result)) (NotificationError.processing)." case .messageProcessing: return "Failed to process message (NotificationError.messageProcessing)." + case .ignorableMessage: return "Ignorable message (NotificationError.ignorableMessage)." case .messageHandling(let error): return "Failed to handle message (\(error)) (NotificationError.messageHandling)." case .other(let error): return "Unknown error occurred: \(error) (NotificationError.other)." } diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 5b321b48b..ea58c07ed 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -15,7 +15,6 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension private var didPerformSetup = false private var contentHandler: ((UNNotificationContent) -> Void)? private var request: UNNotificationRequest? - private var openGroupPollCancellable: AnyCancellable? private var hasCompleted: Atomic = Atomic(false) public static let isFromRemoteKey = "remote" // stringlint:disable @@ -27,19 +26,22 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // MARK: Did receive a remote push notification request override public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { - Log.info("didReceive called.") self.contentHandler = contentHandler self.request = request - - guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else { - return self.completeSilenty() - } // Abort if the main app is running guard !(UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) else { + Log.info("didReceive called while main app running.") + return self.completeSilenty(isMainAppAndActive: true) + } + + guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else { + Log.info("didReceive called with no content.") return self.completeSilenty() } + Log.info("didReceive called.") + /// Create the context if we don't have it (needed before _any_ interaction with the database) if !Singleton.hasAppContext { Singleton.setup(appContext: NotificationServiceExtensionContext()) @@ -50,23 +52,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // Perform main setup Storage.resumeDatabaseAccess() - LibSession.resumeNetworkAccess() DispatchQueue.main.sync { self.setUpIfNecessary() { } } // Handle the push notification Singleton.appReadiness.runNowOrWhenAppDidBecomeReady { - let openGroupPollingPublishers: [AnyPublisher] = self.pollForOpenGroups() - defer { - self.openGroupPollCancellable = Publishers - .MergeMany(openGroupPollingPublishers) - .subscribe(on: DispatchQueue.global(qos: .background)) - .subscribe(on: DispatchQueue.main) - .sink( - receiveCompletion: { [weak self] _ in self?.completeSilenty() }, - receiveValue: { _ in } - ) - } - let (maybeData, metadata, result) = PushNotificationAPI.processNotification( notificationContent: notificationContent ) @@ -84,16 +73,17 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // Just log if the notification was too long (a ~2k message should be able to fit so // these will most commonly be call or config messages) case .successTooLong: - return Log.info("Received too long notification for namespace: \(metadata.namespace).") + Log.info("Received too long notification for namespace: \(metadata.namespace).") + return self.completeSilenty() - case .legacyForceSilent, .failureNoContent: return + case .legacyForceSilent, .failureNoContent: return self.completeSilenty() } } // HACK: It is important to use write synchronously here to avoid a race condition // where the completeSilenty() is called before the local notification request // is added to notification center - Storage.shared.write { db in + Storage.shared.write { [weak self] db in do { guard let processedMessage: ProcessedMessage = try Message.processRawReceivedMessageAsNotification(db, data: data, metadata: metadata) else { throw NotificationError.messageProcessing @@ -119,7 +109,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension /// extension, for all other message types we want to just use the standard `MessageReceiver.handle` call case .standard(let threadId, let threadVariant, _, let messageInfo) where messageInfo.message is CallMessage: guard let callMessage = messageInfo.message as? CallMessage else { - return self.completeSilenty() + throw NotificationError.ignorableMessage } // Throw if the message is outdated and shouldn't be processed @@ -138,7 +128,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension ) guard case .preOffer = callMessage.kind else { - return self.completeSilenty() + throw NotificationError.ignorableMessage } switch (db[.areCallsEnabled], isCallOngoing) { @@ -179,7 +169,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension case (true, false): try MessageReceiver.insertCallInfoMessage(db, for: callMessage) - self.handleSuccessForIncomingCall(db, for: callMessage) + return self?.handleSuccessForIncomingCall(db, for: callMessage) } // Perform any required post-handling logic @@ -200,6 +190,11 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension associatedWithProto: proto ) } + + db.afterNextTransaction( + onCommit: { _ in self?.completeSilenty() }, + onRollback: { _ in self?.completeSilenty() } + ) } catch { // If an error occurred we want to rollback the transaction (by throwing) and then handle @@ -207,16 +202,16 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension let handleError = { switch error { case MessageReceiverError.invalidGroupPublicKey, MessageReceiverError.noGroupKeyPair, - MessageReceiverError.outdatedMessage: - self.completeSilenty() + MessageReceiverError.outdatedMessage, NotificationError.ignorableMessage: + self?.completeSilenty() case NotificationError.messageProcessing: - self.handleFailure(for: notificationContent, error: .messageProcessing) + self?.handleFailure(for: notificationContent, error: .messageProcessing) case let msgError as MessageReceiverError: - self.handleFailure(for: notificationContent, error: .messageHandling(msgError)) + self?.handleFailure(for: notificationContent, error: .messageHandling(msgError)) - default: self.handleFailure(for: notificationContent, error: .other(error)) + default: self?.handleFailure(for: notificationContent, error: .other(error)) } } @@ -330,12 +325,11 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. Log.warn("Execution time expired.") - openGroupPollCancellable?.cancel() completeSilenty() } - private func completeSilenty() { - // Ensure we on'y run this once + private func completeSilenty(isMainAppAndActive: Bool = false) { + // Ensure we only run this once guard hasCompleted.mutate({ hasCompleted in let wasCompleted: Bool = hasCompleted @@ -349,9 +343,11 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension .read { db in try Interaction.fetchUnreadCount(db) } .map { NSNumber(value: $0) } .defaulting(to: NSNumber(value: 0)) + Log.info("Complete silently.") - LibSession.suspendNetworkAccess() - Storage.suspendDatabaseAccess() + if !isMainAppAndActive { + Storage.suspendDatabaseAccess() + } Log.flush() self.contentHandler!(silentContent) @@ -361,23 +357,32 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension if #available(iOSApplicationExtension 14.5, *), Preferences.isCallKitSupported { guard let caller: String = callMessage.sender, let timestamp = callMessage.sentTimestamp else { return } - let payload: JSON = [ - "uuid": callMessage.uuid, // stringlint:disable - "caller": caller, // stringlint:disable - "timestamp": timestamp // stringlint:disable - ] - - CXProvider.reportNewIncomingVoIPPushPayload(payload) { error in - if let error = error { - self.handleFailureForVoIP(db, for: callMessage) - Log.error("Failed to notify main app of call message: \(error).") - } - else { - Log.info("Successfully notified main app of call message.") - UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date() - self.completeSilenty() + let reportCall: () -> () = { [weak self] in + let payload: JSON = [ + "uuid": callMessage.uuid, // stringlint:disable + "caller": caller, // stringlint:disable + "timestamp": timestamp // stringlint:disable + ] + + CXProvider.reportNewIncomingVoIPPushPayload(payload) { error in + if let error = error { + Log.error("Failed to notify main app of call message: \(error).") + Storage.shared.read { db in + self?.handleFailureForVoIP(db, for: callMessage) + } + } + else { + Log.info("Successfully notified main app of call message.") + UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date() + self?.completeSilenty() + } } } + + db.afterNextTransaction( + onCommit: { _ in reportCall() }, + onRollback: { _ in reportCall() } + ) } else { self.handleFailureForVoIP(db, for: callMessage) @@ -412,12 +417,15 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension } semaphore.wait() Log.info("Add remote notification request.") - Log.flush() + + db.afterNextTransaction( + onCommit: { [weak self] _ in self?.completeSilenty() }, + onRollback: { [weak self] _ in self?.completeSilenty() } + ) } private func handleFailure(for content: UNMutableNotificationContent, error: NotificationError) { Log.error("Show generic failure message due to error: \(error).") - LibSession.suspendNetworkAccess() Storage.suspendDatabaseAccess() Log.flush() @@ -427,36 +435,4 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension content.userInfo = userInfo contentHandler!(content) } - - // MARK: - Poll for open groups - - private func pollForOpenGroups() -> [AnyPublisher] { - return Storage.shared - .read { db in - // The default room promise creates an OpenGroup with an empty `roomToken` value, - // we don't want to start a poller for this as the user hasn't actually joined a room - try OpenGroup - .select(.server) - .filter(OpenGroup.Columns.roomToken != "") - .filter(OpenGroup.Columns.isActive) - .distinct() - .asRequest(of: String.self) - .fetchSet(db) - } - .defaulting(to: []) - .map { server -> AnyPublisher in - OpenGroupAPI.Poller(for: server) - .poll(calledFromBackgroundPoller: true, isPostCapabilitiesRetry: false) - .timeout( - .seconds(20), - scheduler: DispatchQueue.global(qos: .default), - customError: { NotificationServiceError.timeout } - ) - .eraseToAnyPublisher() - } - } - - private enum NotificationServiceError: Error { - case timeout - } } diff --git a/SessionSnodeKit/LibSession/LibSession+Networking.swift b/SessionSnodeKit/LibSession/LibSession+Networking.swift index d755536c5..0c6173c4a 100644 --- a/SessionSnodeKit/LibSession/LibSession+Networking.swift +++ b/SessionSnodeKit/LibSession/LibSession+Networking.swift @@ -204,7 +204,7 @@ public extension LibSession { switch result { case .failure(let error): throw error case .success(let nodes): - guard nodes.count > count else { throw SnodeAPIError.unableToRetrieveSwarm } + guard nodes.count >= count else { throw SnodeAPIError.unableToRetrieveSwarm } return nodes } @@ -358,6 +358,7 @@ public extension LibSession { } static func checkClientVersion( + ed25519SecretKey: [UInt8], using dependencies: Dependencies = Dependencies() ) -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error> { typealias Output = (success: Bool, timeout: Bool, statusCode: Int, data: Data?) @@ -366,9 +367,12 @@ public extension LibSession { .tryFlatMap { network in return CallbackWrapper .create { wrapper in + var cEd25519SecretKey: [UInt8] = Array(ed25519SecretKey) + network_get_client_version( network, CLIENT_PLATFORM_IOS, + &cEd25519SecretKey, Int64(floor(Network.fileDownloadTimeout * 1000)), { success, timeout, statusCode, dataPtr, dataLen, ctx in let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } @@ -422,7 +426,7 @@ public extension LibSession { return nil } - guard network_init(&network, cCachePath, Features.useTestnet, true, &error) else { + guard network_init(&network, cCachePath, Features.useTestnet, !Singleton.appContext.isMainApp, true, &error) else { Log.error("[LibQuic] Unable to create network object: \(String(cString: error))") return nil } @@ -550,6 +554,7 @@ public extension LibSession { throw SnodeAPIError.nodeNotFound(String(responseString.suffix(64))) + case (504, _): throw NetworkError.gatewayTimeout case (_, .none): throw NetworkError.unknown case (_, .some(let responseString)): throw NetworkError.requestFailed(error: responseString, rawData: data) } diff --git a/SessionSnodeKit/Models/AppVersionResponse.swift b/SessionSnodeKit/Models/AppVersionResponse.swift index eea0eae0b..ae5c33739 100644 --- a/SessionSnodeKit/Models/AppVersionResponse.swift +++ b/SessionSnodeKit/Models/AppVersionResponse.swift @@ -2,10 +2,96 @@ import Foundation -public struct AppVersionResponse: Codable { +public class AppVersionResponse: AppVersionInfo { + enum CodingKeys: String, CodingKey { + case prerelease + } + + public let prerelease: AppVersionInfo? + + public init( + version: String, + updated: TimeInterval?, + name: String?, + notes: String?, + assets: [Asset]?, + prerelease: AppVersionInfo? + ) { + self.prerelease = prerelease + + super.init( + version: version, + updated: updated, + name: name, + notes: notes, + assets: assets + ) + } + + required init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + self.prerelease = try? container.decode(AppVersionInfo?.self, forKey: .prerelease) + + try super.init(from: decoder) + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(prerelease, forKey: .prerelease) + } +} + +// MARK: - AppVersionInfo + +public class AppVersionInfo: Codable { enum CodingKeys: String, CodingKey { case version = "result" + case updated + case name + case notes + case assets + } + + public struct Asset: Codable { + enum CodingKeys: String, CodingKey { + case name + case url + } + + public let name: String + public let url: String } public let version: String + public let updated: TimeInterval? + public let name: String? + public let notes: String? + public let assets: [Asset]? + + public init( + version: String, + updated: TimeInterval?, + name: String?, + notes: String?, + assets: [Asset]? + ) { + self.version = version + self.updated = updated + self.name = name + self.notes = notes + self.assets = assets + } + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(version, forKey: .version) + try container.encodeIfPresent(updated, forKey: .updated) + try container.encodeIfPresent(name, forKey: .name) + try container.encodeIfPresent(notes, forKey: .notes) + try container.encodeIfPresent(assets, forKey: .assets) + } } diff --git a/SessionUtilitiesKit/Database/Models/Job.swift b/SessionUtilitiesKit/Database/Models/Job.swift index 2744ed2b6..3dc4aca4c 100644 --- a/SessionUtilitiesKit/Database/Models/Job.swift +++ b/SessionUtilitiesKit/Database/Models/Job.swift @@ -128,6 +128,9 @@ public struct Job: Codable, Equatable, Hashable, Identifiable, FetchableRecord, /// This is a job that runs once whenever a message is marked as read because of syncing from user config and /// needs to get expiration from network case getExpiration + + /// This is a job that runs at most once every 24 hours in order to check if there is a new update available on GitHub + case checkForAppUpdates = 3011 } public enum Behaviour: Int, Codable, DatabaseValueConvertible, CaseIterable { diff --git a/SessionUtilitiesKit/General/Logging.swift b/SessionUtilitiesKit/General/Logging.swift index 20868c13b..6ad692b68 100644 --- a/SessionUtilitiesKit/General/Logging.swift +++ b/SessionUtilitiesKit/General/Logging.swift @@ -37,8 +37,7 @@ public enum Log { public static func appResumedExecution() { guard logger.wrappedValue != nil else { return } - Log.empty() - Log.empty() + logger.wrappedValue?.loadExtensionLogsAndResumeLogging() } public static func logFilePath() -> String? { @@ -46,7 +45,51 @@ public enum Log { let logger: Logger = logger.wrappedValue else { return nil } - return logger.fileLogger.logFileManager.sortedLogFilePaths.first + let logFiles: [String] = logger.fileLogger.logFileManager.sortedLogFilePaths + + guard !logFiles.isEmpty else { return nil } + + // If the latest log file is too short (ie. less that ~100kb) then we want to create a temporary file + // which contains the previous log file logs plus the logs from the newest file so we don't miss info + // that might be relevant for debugging + guard + logFiles.count > 1, + let attributes: [FileAttributeKey: Any] = try? FileManager.default.attributesOfItem(atPath: logFiles[0]), + let fileSize: UInt64 = attributes[.size] as? UInt64, + fileSize < (100 * 1024) + else { return logFiles[0] } + + // The file is too small so lets create a temp file to share instead + let tempDirectory: String = NSTemporaryDirectory() + let tempFilePath: String = URL(fileURLWithPath: tempDirectory) + .appendingPathComponent(URL(fileURLWithPath: logFiles[1]).lastPathComponent) + .path + + do { + try FileManager.default.copyItem( + atPath: logFiles[1], + toPath: tempFilePath + ) + + guard let fileHandle: FileHandle = FileHandle(forWritingAtPath: tempFilePath) else { + throw StorageError.objectNotFound + } + + // Ensure we close the file handle + defer { fileHandle.closeFile() } + + // Move to the end of the file to insert the logs + if #available(iOS 13.4, *) { try fileHandle.seekToEnd() } + else { fileHandle.seekToEndOfFile() } + + // Append the data from the newest log to the temp file + let newestLogData: Data = try Data(contentsOf: URL(fileURLWithPath: logFiles[0])) + if #available(iOS 13.4, *) { try fileHandle.write(contentsOf: newestLogData) } + else { fileHandle.write(newestLogData) } + } + catch { return logFiles[0] } + + return tempFilePath } public static func flush() { @@ -129,7 +172,7 @@ public enum Log { ) { guard let logger: Logger = logger.wrappedValue, - logger.startupCompleted.wrappedValue + !logger.isSuspended.wrappedValue else { return pendingStartupLogs.mutate { $0.append((level, message, withPrefixes, silenceForTests)) } } logger.log(level, message, withPrefixes: withPrefixes, silenceForTests: silenceForTests) @@ -143,7 +186,7 @@ public class Logger { private let primaryPrefix: String private let forceNSLog: Bool fileprivate let fileLogger: DDFileLogger - fileprivate let startupCompleted: Atomic = Atomic(false) + fileprivate let isSuspended: Atomic = Atomic(true) fileprivate var retrievePendingStartupLogs: (() -> [Log.LogInfo])? public init( @@ -178,19 +221,22 @@ public class Logger { // Now that we are setup we should load the extension logs which will then // complete the startup process when completed - self.loadExtensionLogs() + self.loadExtensionLogsAndResumeLogging() } // MARK: - Functions - private func loadExtensionLogs() { + fileprivate func loadExtensionLogsAndResumeLogging() { + // Pause logging while we load the extension logs (want to avoid interleaving them where possible) + isSuspended.mutate { $0 = true } + // The extensions write their logs to the app shared directory but the main app writes // to a local directory (so they can be exported via XCode) - the below code reads any // logs from the shared directly and attempts to add them to the main app logs to make // debugging user issues in extensions easier DispatchQueue.global(qos: .utility).async { [weak self] in guard let currentLogFileInfo: DDLogFileInfo = self?.fileLogger.currentLogFileInfo else { - self?.completeStartup(error: "Unable to retrieve current log file.") + self?.completeResumeLogging(error: "Unable to retrieve current log file.") return } @@ -257,21 +303,27 @@ public class Logger { } } catch { - self?.completeStartup(error: "Unable to write extension logs to current log file") + self?.completeResumeLogging(error: "Unable to write extension logs to current log file") return } - self?.completeStartup() + self?.completeResumeLogging() } } } - private func completeStartup(error: String? = nil) { - let pendingLogs: [Log.LogInfo] = startupCompleted.mutate { startupCompleted in - startupCompleted = true + private func completeResumeLogging(error: String? = nil) { + let pendingLogs: [Log.LogInfo] = isSuspended.mutate { isSuspended in + isSuspended = false return (retrievePendingStartupLogs?() ?? []) } + // If we had an error loading the extension logs then actually log it + if let error: String = error { + Log.empty() + log(.error, error, withPrefixes: true, silenceForTests: false) + } + // After creating a new logger we want to log two empty lines to make it easier to read Log.empty() Log.empty() diff --git a/SessionUtilitiesKit/General/String+Utilities.swift b/SessionUtilitiesKit/General/String+Utilities.swift index 92c7dc74f..fe7d66cc2 100644 --- a/SessionUtilitiesKit/General/String+Utilities.swift +++ b/SessionUtilitiesKit/General/String+Utilities.swift @@ -82,8 +82,8 @@ public extension String.StringInterpolation { appendInterpolation(value == 1 ? "" : "s") // stringlint:disable } - public mutating func appendInterpolation(period value: String) { - appendInterpolation(value.hasSuffix(".") ? "" : ".") // stringlint:disable + mutating func appendInterpolation(_ value: String, number: Int, singular: String = "", plural: String = "s") { + appendInterpolation("\(value)\(number == 1 ? singular : plural)") // stringlint:disable } mutating func appendInterpolation(_ value: TimeUnit, unit: TimeUnit.Unit, resolution: Int = 2) { diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index d91006046..ba75a639b 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -156,7 +156,7 @@ public final class JobRunner: JobRunnerType { case (.failed(let lhsError, let lhsPermanent), .failed(let rhsError, let rhsPermanent)): return ( // Not a perfect solution but should be good enough - "\(lhsError ?? JobRunnerError.generic)" == "\(rhsError ?? JobRunnerError.generic)" && + "\(lhsError ?? JobRunnerError.unknown)" == "\(rhsError ?? JobRunnerError.unknown)" && lhsPermanent == rhsPermanent ) @@ -299,7 +299,8 @@ public final class JobRunner: JobRunnerType { jobVariants: [ jobVariants.remove(.expirationUpdate), jobVariants.remove(.getExpiration), - jobVariants.remove(.disappearingMessages) + jobVariants.remove(.disappearingMessages), + jobVariants.remove(.checkForAppUpdates) // Don't want this to block other jobs ].compactMap { $0 } ), @@ -1632,7 +1633,7 @@ public final class JobQueue: Hashable { // immediately (in this case we don't trigger any job callbacks because the // job isn't actually done, it's going to try again immediately) if self.type == .blocking && job.shouldBlock { - SNLog("[JobRunner] \(queueContext) \(job.variant) job failed; retrying immediately") + SNLog("[JobRunner] \(queueContext) \(job.variant) job failed due to error: \(error ?? JobRunnerError.unknown); retrying immediately") // If it was a possible deferral loop then we don't actually want to // retry the job (even if it's a blocking one, this gives a small chance @@ -1664,7 +1665,7 @@ public final class JobQueue: Hashable { let maxFailureCount: Int = (executorMap.wrappedValue[job.variant]?.maxFailureCount ?? 0) let nextRunTimestamp: TimeInterval = (dependencies.dateNow.timeIntervalSince1970 + JobRunner.getRetryInterval(for: job)) var dependantJobIds: [Int64] = [] - var failureText: String = "failed" + var failureText: String = "failed due to error: \(error ?? JobRunnerError.unknown)" dependencies.storage.write(using: dependencies) { db in /// Retrieve a list of dependant jobs so we can clear them from the queue @@ -1683,8 +1684,8 @@ public final class JobQueue: Hashable { ) else { failureText = (maxFailureCount >= 0 && updatedFailureCount > maxFailureCount ? - "failed permanently; too many retries" : - "failed permanently" + "failed permanently due to error: \(error ?? JobRunnerError.unknown); too many retries" : + "failed permanently due to error: \(error ?? JobRunnerError.unknown)" ) // If the job permanently failed or we have performed all of our retry attempts @@ -1696,7 +1697,7 @@ public final class JobQueue: Hashable { return } - failureText = "failed; scheduling retry (failure count is \(updatedFailureCount))" + failureText = "failed due to error: \(error ?? JobRunnerError.unknown); scheduling retry (failure count is \(updatedFailureCount))" try job .with( diff --git a/SessionUtilitiesKit/JobRunner/JobRunnerError.swift b/SessionUtilitiesKit/JobRunner/JobRunnerError.swift index 0932187ed..51ab05775 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunnerError.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunnerError.swift @@ -3,8 +3,6 @@ import Foundation public enum JobRunnerError: Error { - case generic - case executorMissing case jobIdMissing case requiredThreadIdMissing @@ -14,4 +12,6 @@ public enum JobRunnerError: Error { case missingDependencies case possibleDeferralLoop + + case unknown } diff --git a/SessionUtilitiesKit/Networking/NetworkError.swift b/SessionUtilitiesKit/Networking/NetworkError.swift index 761ff3ec6..05ed29319 100644 --- a/SessionUtilitiesKit/Networking/NetworkError.swift +++ b/SessionUtilitiesKit/Networking/NetworkError.swift @@ -15,6 +15,7 @@ public enum NetworkError: Error, Equatable, CustomStringConvertible { case internalServerError case badGateway case serviceUnavailable + case gatewayTimeout case badRequest(error: String, rawData: Data?) case requestFailed(error: String, rawData: Data?) case timeout @@ -33,6 +34,7 @@ public enum NetworkError: Error, Equatable, CustomStringConvertible { case .internalServerError: return "Internal server error (NetworkError.internalServerError)." case .badGateway: return "Bad gateway (NetworkError.badGateway)." case .serviceUnavailable: return "Service unavailable (NetworkError.serviceUnavailable)." + case .gatewayTimeout: return "Gateway timeout (NetworkError.gatewayTimeout)." case .badRequest(let error, _), .requestFailed(let error, _): return error case .timeout: return "The request timed out (NetworkError.timeout)." case .suspended: return "Network requests are suspended (NetworkError.suspended)."