Additional bug fixes, log tweaks and update checking

• Added a new CheckForAppUpdates job which runs at most once every 24 hours
• Updated the job failure logs to include the error that caused the failure
• Updated the network instance to use 'single_path_mode' when not executing within the main app
• Updated the logger to append extension logs when resuming instead of only during startup
• Updated the export logs behaviour to append the previously rotated log data if the latest log file is too short (to ensure we get more useful info when debugging)
• Updated to the latest libSession commit to resolve a couple of edge-cases
pull/981/head
Morgan Pretty 10 months ago
parent 6663bd64c4
commit cac5542868

@ -1 +1 @@
Subproject commit 714230c0c8867ac8662603fd6debb5b4c4369ef1
Subproject commit 9a867d563266b875144285cd49b07b3aacf7206e

@ -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 = "<group>"; };
FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessResult.swift; sourceTree = "<group>"; };
FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionTypeConversionUtilitiesSpec.swift; sourceTree = "<group>"; };
FDDD554B2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForAppUpdatesJob.swift; sourceTree = "<group>"; };
FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _019_ScheduleAppUpdateCheckJob.swift; sourceTree = "<group>"; };
FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequest+Utilities.swift"; sourceTree = "<group>"; };
FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerSpec.swift; sourceTree = "<group>"; };
FDE125222A837E4E002DA685 /* MainAppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAppContext.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -4454,6 +4459,7 @@
FD2B4AFE2946C93200AB4848 /* ConfigurationSyncJob.swift */,
7B7E5B512A4D024C00A8208E /* ExpirationUpdateJob.swift */,
7B7AD41E2A5512CA00469FB1 /* GetExpirationJob.swift */,
FDDD554B2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift */,
);
path = Types;
sourceTree = "<group>";
@ -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 = 450;
CURRENT_PROJECT_VERSION = 451;
ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@ -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 = 450;
CURRENT_PROJECT_VERSION = 451;
ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_NO_COMMON_BLOCKS = YES;

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

@ -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)
}
}

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

@ -0,0 +1,55 @@
// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import SessionUtilitiesKit
import SessionSnodeKit
public enum CheckForAppUpdatesJob: JobExecutor {
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 + (24 * 60 * 60))
)
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)")
}
}
)
}
}

@ -28,15 +28,15 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
override public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
self.request = request
guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
Log.info("didReceive called with no content.")
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()
}
@ -328,7 +328,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
completeSilenty()
}
private func completeSilenty() {
private func completeSilenty(isMainAppAndActive: Bool = false) {
// Ensure we only run this once
guard
hasCompleted.mutate({ hasCompleted in
@ -343,8 +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.")
Storage.suspendDatabaseAccess()
if !isMainAppAndActive {
Storage.suspendDatabaseAccess()
}
Log.flush()
self.contentHandler!(silentContent)

@ -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<Output>
.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
}

@ -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<CodingKeys> = 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<CodingKeys> = 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<CodingKeys> = 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)
}
}

@ -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 {

@ -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<Bool> = Atomic(false)
fileprivate let isSuspended: Atomic<Bool> = 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()

@ -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(

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

Loading…
Cancel
Save