Merge pull request #981 from mpretty-cyro/fix/release-2-6-1-issues

Fix release 2.6.1 Issues
pull/984/head
Morgan Pretty 2 weeks ago committed by GitHub
commit ec72a5310f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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',

@ -1 +1 @@
Subproject commit 714230c0c8867ac8662603fd6debb5b4c4369ef1
Subproject commit fdfd9dbb698e4ce6e6e08dd1b85eec428ab2077e

@ -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 = 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",

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

@ -15,46 +15,50 @@ public final class BackgroundPoller {
completionHandler: @escaping (UIBackgroundFetchResult) -> Void,
using dependencies: Dependencies = Dependencies()
) {
let (groupIds, servers): (Set<String>, Set<String>) = 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<Void, Error> 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<String>,
using dependencies: Dependencies
) -> [AnyPublisher<Void, Error>] {
// 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<String>,
using dependencies: Dependencies
) -> [AnyPublisher<Void, Error>] {
return servers.map { server -> AnyPublisher<Void, Error> in
let poller: OpenGroupAPI.Poller = OpenGroupAPI.Poller(for: server)
poller.stop()
return poller.poll(
calledFromBackgroundPoller: true,
isBackgroundPollerValid: { BackgroundPoller.isValid },
isPostCapabilitiesRetry: false,
using: dependencies
)
}
}
}

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

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

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

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

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

@ -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<Bool> = 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<Void, Error>] = 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<Void, Error>] {
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<Void, Error> 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
}
}

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

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

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

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

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

Loading…
Cancel
Save