Merge remote-tracking branch 'upstream/dev' into fix/add-migration-tests

# Conflicts:
#	SessionMessagingKit/SessionUtil/SessionUtil.swift
pull/919/head
Morgan Pretty 2 years ago
commit 9dd2e896bb

@ -114,7 +114,7 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate {
publicKey: call.sessionId, publicKey: call.sessionId,
threadVariant: .contact, threadVariant: .contact,
customImageData: nil, customImageData: nil,
profile: Storage.shared.read { db in Profile.fetchOrCreate(db, id: call.sessionId) }, profile: Storage.shared.read { [sessionId = call.sessionId] db in Profile.fetchOrCreate(db, id: sessionId) },
additionalProfile: nil additionalProfile: nil
) )
displayNameLabel.text = call.contactName displayNameLabel.text = call.contactName

@ -324,7 +324,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// the user is in an invalid state (and should have already been shown a modal) // the user is in an invalid state (and should have already been shown a modal)
guard success else { return } guard success else { return }
SNLog("RootViewController ready, readying remaining processes") SNLog("RootViewController ready for state: \(Onboarding.State.current), readying remaining processes")
self?.initialLaunchFailed = false self?.initialLaunchFailed = false
/// Trigger any launch-specific jobs and start the JobRunner with `JobRunner.appDidFinishLaunching()` some /// Trigger any launch-specific jobs and start the JobRunner with `JobRunner.appDidFinishLaunching()` some

@ -130,10 +130,6 @@ final class LinkDeviceVC: BaseVC, UIPageViewControllerDataSource, UIPageViewCont
// MARK: - Interaction // MARK: - Interaction
@objc private func close() {
dismiss(animated: true, completion: nil)
}
func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onError: (() -> ())?) { func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onError: (() -> ())?) {
let seed = Data(hex: string) let seed = Data(hex: string)
continueWithSeed(seed, onError: onError) continueWithSeed(seed, onError: onError)

@ -121,7 +121,7 @@ enum Onboarding {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
enum State { enum State: CustomStringConvertible {
case newUser case newUser
case missingName case missingName
case completed case completed
@ -138,6 +138,14 @@ enum Onboarding {
// Otherwise we have enough for a full user and can start the app // Otherwise we have enough for a full user and can start the app
return .completed return .completed
} }
var description: String {
switch self {
case .newUser: return "New User" // stringlint:disable
case .missingName: return "Missing Name" // stringlint:disable
case .completed: return "Completed" // stringlint:disable
}
}
} }
enum Flow { enum Flow {
@ -145,7 +153,7 @@ enum Onboarding {
/// If the user returns to an earlier screen during Onboarding we might need to clear out a partially created /// If the user returns to an earlier screen during Onboarding we might need to clear out a partially created
/// account (eg. returning from the PN setting screen to the seed entry screen when linking a device) /// account (eg. returning from the PN setting screen to the seed entry screen when linking a device)
func unregister() { func unregister(using dependencies: Dependencies = Dependencies()) {
// Clear the in-memory state from SessionUtil // Clear the in-memory state from SessionUtil
SessionUtil.clearMemoryState() SessionUtil.clearMemoryState()
@ -165,6 +173,9 @@ enum Onboarding {
profileNameRetrievalIdentifier.mutate { $0 = nil } profileNameRetrievalIdentifier.mutate { $0 = nil }
profileNameRetrievalPublisher.mutate { $0 = nil } profileNameRetrievalPublisher.mutate { $0 = nil }
// Clear the cached 'encodedPublicKey' if needed
dependencies.caches.mutate(cache: .general) { $0.encodedPublicKey = nil }
UserDefaults.standard[.hasSyncedInitialConfiguration] = false UserDefaults.standard[.hasSyncedInitialConfiguration] = false
} }

@ -8,6 +8,8 @@ import SignalUtilitiesKit
final class SeedVC: BaseVC { final class SeedVC: BaseVC {
public static func mnemonic() throws -> String { public static func mnemonic() throws -> String {
let dbIsValid: Bool = Storage.shared.isValid let dbIsValid: Bool = Storage.shared.isValid
let dbHasRead: Bool = Storage.shared.hasSuccessfullyRead
let dbHasWritten: Bool = Storage.shared.hasSuccessfullyWritten
let dbIsSuspendedUnsafe: Bool = Storage.shared.isSuspendedUnsafe let dbIsSuspendedUnsafe: Bool = Storage.shared.isSuspendedUnsafe
if let hexEncodedSeed: String = Identity.fetchHexEncodedSeed() { if let hexEncodedSeed: String = Identity.fetchHexEncodedSeed() {
@ -19,6 +21,8 @@ final class SeedVC: BaseVC {
let hasStoredEdKeyPair: Bool = (Identity.fetchUserEd25519KeyPair() != nil) let hasStoredEdKeyPair: Bool = (Identity.fetchUserEd25519KeyPair() != nil)
let dbStates: [String] = [ let dbStates: [String] = [
"dbIsValid: \(dbIsValid)", // stringlint:disable "dbIsValid: \(dbIsValid)", // stringlint:disable
"dbHasRead: \(dbHasRead)", // stringlint:disable
"dbHasWritten: \(dbHasWritten)", // stringlint:disable
"dbIsSuspendedUnsafe: \(dbIsSuspendedUnsafe)", // stringlint:disable "dbIsSuspendedUnsafe: \(dbIsSuspendedUnsafe)", // stringlint:disable
"storedSeed: false", // stringlint:disable "storedSeed: false", // stringlint:disable
"userPublicKey: \(hasStoredPublicKey)", // stringlint:disable "userPublicKey: \(hasStoredPublicKey)", // stringlint:disable

@ -77,7 +77,7 @@ internal extension SessionUtil {
} }
} }
catch { catch {
SNLog("[libSession] Failed to update/dump updated \(variant) config data due to error: \(error)") SNLog("[SessionUtil] Failed to update/dump updated \(variant) config data due to error: \(error)")
throw error throw error
} }
@ -368,7 +368,7 @@ internal extension SessionUtil {
loopCounter += 1 loopCounter += 1
guard loopCounter < maxLoopCount else { guard loopCounter < maxLoopCount else {
SNLog("[libSession] Got stuck in infinite loop processing '\(variant.configMessageKind.description)' data") SNLog("[SessionUtil] Got stuck in infinite loop processing '\(variant.configMessageKind.description)' data")
throw SessionUtilError.processingLoopLimitReached throw SessionUtilError.processingLoopLimitReached
} }
} }

@ -45,7 +45,7 @@ public enum SessionUtil {
// MARK: - Variables // MARK: - Variables
internal static func syncDedupeId(_ publicKey: String) -> String { internal static func syncDedupeId(_ publicKey: String) -> String {
return "EnqueueConfigurationSyncJob-\(publicKey)" return "EnqueueConfigurationSyncJob-\(publicKey)" // stringlint:disable
} }
/// Returns `true` if there is a config which needs to be pushed, but returns `false` if the configs are all up to date or haven't been /// Returns `true` if there is a config which needs to be pushed, but returns `false` if the configs are all up to date or haven't been
@ -63,7 +63,7 @@ public enum SessionUtil {
public static var libSessionVersion: String { String(cString: LIBSESSION_UTIL_VERSION_STR) } public static var libSessionVersion: String { String(cString: LIBSESSION_UTIL_VERSION_STR) }
internal static func lastError(_ conf: UnsafeMutablePointer<config_object>?) -> String { internal static func lastError(_ conf: UnsafeMutablePointer<config_object>?) -> String {
return (conf?.pointee.last_error.map { String(cString: $0) } ?? "Unknown") return (conf?.pointee.last_error.map { String(cString: $0) } ?? "Unknown") // stringlint:disable
} }
// MARK: - Loading // MARK: - Loading
@ -84,7 +84,7 @@ public enum SessionUtil {
guard guard
let secretKey: [UInt8] = ed25519SecretKey, let secretKey: [UInt8] = ed25519SecretKey,
SessionUtil.configStore.wrappedValue.isEmpty SessionUtil.configStore.wrappedValue.isEmpty
else { return } else { return SNLog("[SessionUtil] Ignoring loadState for '\(userPublicKey)' due to existing state") }
// If we weren't given a database instance then get one // If we weren't given a database instance then get one
guard let db: Database = db else { guard let db: Database = db else {
@ -125,6 +125,8 @@ public enum SessionUtil {
) )
} }
} }
SNLog("[SessionUtil] Completed loadState for '\(userPublicKey)'")
} }
private static func loadState( private static func loadState(
@ -234,7 +236,7 @@ public enum SessionUtil {
var cPushData: UnsafeMutablePointer<config_push_data>! var cPushData: UnsafeMutablePointer<config_push_data>!
let configCountInfo: String = { let configCountInfo: String = {
var result: String = "Invalid" var result: String = "Invalid" // stringlint:disable
try? CExceptionHelper.performSafely { try? CExceptionHelper.performSafely {
switch variant { switch variant {
@ -254,7 +256,7 @@ public enum SessionUtil {
} }
} }
catch { catch {
SNLog("[libSession] Failed to generate push data for \(variant) config data, size: \(configCountInfo), error: \(error)") SNLog("[SessionUtil] Failed to generate push data for \(variant) config data, size: \(configCountInfo), error: \(error)")
throw error throw error
} }
@ -411,7 +413,7 @@ public enum SessionUtil {
} }
} }
catch { catch {
SNLog("[libSession] Failed to process merge of \(next.key) config data") SNLog("[SessionUtil] Failed to process merge of \(next.key) config data")
throw error throw error
} }

@ -118,7 +118,7 @@ public enum Mnemonic {
} }
public static func decode(mnemonic: String, language: Language = .english) throws -> String { public static func decode(mnemonic: String, language: Language = .english) throws -> String {
var words: [String] = mnemonic.split(separator: " ").map { String($0) } var words: [String] = mnemonic.components(separatedBy: .whitespacesAndNewlines)
let truncatedWordSet: [String] = language.loadTruncatedWordSet() let truncatedWordSet: [String] = language.loadTruncatedWordSet()
let prefixLength: Int = language.prefixLength let prefixLength: Int = language.prefixLength
var result = "" var result = ""

@ -41,6 +41,12 @@ open class Storage {
/// this should be taken into consideration when used /// this should be taken into consideration when used
public private(set) var isSuspendedUnsafe: Bool = false public private(set) var isSuspendedUnsafe: Bool = false
/// This property gets set the first time we successfully read from the database
public private(set) var hasSuccessfullyRead: Bool = false
/// This property gets set the first time we successfully write to the database
public private(set) var hasSuccessfullyWritten: Bool = false
public var hasCompletedMigrations: Bool { migrationsCompleted.wrappedValue } public var hasCompletedMigrations: Bool { migrationsCompleted.wrappedValue }
public var currentlyRunningMigration: (identifier: TargetMigrations.Identifier, migration: Migration.Type)? { public var currentlyRunningMigration: (identifier: TargetMigrations.Identifier, migration: Migration.Type)? {
internalCurrentlyRunningMigration.wrappedValue internalCurrentlyRunningMigration.wrappedValue
@ -452,37 +458,60 @@ open class Storage {
// MARK: - Logging Functions // MARK: - Logging Functions
typealias CallInfo = (file: String, function: String, line: Int) private enum Action {
case read
case write
case logIfSlow
}
private static func logSlowWrites<T>( private typealias CallInfo = (storage: Storage?, actions: [Action], file: String, function: String, line: Int)
private static func perform<T>(
info: CallInfo, info: CallInfo,
updates: @escaping (Database) throws -> T updates: @escaping (Database) throws -> T
) -> (Database) throws -> T { ) -> (Database) throws -> T {
return { db in return { db in
let start: CFTimeInterval = CACurrentMediaTime() let start: CFTimeInterval = CACurrentMediaTime()
let actionName: String = (info.actions.contains(.write) ? "write" : "read")
let fileName: String = (info.file.components(separatedBy: "/").last.map { " \($0):\(info.line)" } ?? "") let fileName: String = (info.file.components(separatedBy: "/").last.map { " \($0):\(info.line)" } ?? "")
let timeout: Timer = Timer.scheduledTimerOnMainThread(withTimeInterval: writeWarningThreadshold) { let timeout: Timer? = {
guard info.actions.contains(.logIfSlow) else { return nil }
return Timer.scheduledTimerOnMainThread(withTimeInterval: Storage.writeWarningThreadshold) {
$0.invalidate() $0.invalidate()
// Don't want to log on the main thread as to avoid confusion when debugging issues // Don't want to log on the main thread as to avoid confusion when debugging issues
DispatchQueue.global(qos: .default).async { DispatchQueue.global(qos: .default).async {
SNLog("[Storage\(fileName)] Slow write taking longer than \(writeWarningThreadshold, format: ".2", omitZeroDecimal: true)s - \(info.function)") SNLog("[Storage\(fileName)] Slow \(actionName) taking longer than \(Storage.writeWarningThreadshold, format: ".2", omitZeroDecimal: true)s - \(info.function)")
} }
} }
}()
// If we timed out and are logging slow actions then log the actual duration to help us
// prioritise performance issues
defer { defer {
// If we timed out then log the actual duration to help us prioritise performance issues if timeout != nil && timeout?.isValid == false {
if !timeout.isValid {
let end: CFTimeInterval = CACurrentMediaTime() let end: CFTimeInterval = CACurrentMediaTime()
DispatchQueue.global(qos: .default).async { DispatchQueue.global(qos: .default).async {
SNLog("[Storage\(fileName)] Slow write completed after \(end - start, format: ".2", omitZeroDecimal: true)s") SNLog("[Storage\(fileName)] Slow \(actionName) completed after \(end - start, format: ".2", omitZeroDecimal: true)s")
} }
} }
timeout.invalidate() timeout?.invalidate()
} }
return try updates(db) // Get the result
let result: T = try updates(db)
// Update the state flags
switch info.actions {
case [.write], [.write, .logIfSlow]: info.storage?.hasSuccessfullyWritten = true
case [.read], [.read, .logIfSlow]: info.storage?.hasSuccessfullyRead = true
default: break
}
return result
} }
} }
@ -512,9 +541,8 @@ open class Storage {
) -> T? { ) -> T? {
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return nil } guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return nil }
let info: CallInfo = (fileName, functionName, lineNumber) let info: CallInfo = { [weak self] in (self, [.write, .logIfSlow], fileName, functionName, lineNumber) }()
do { return try dbWriter.write(Storage.perform(info: info, updates: updates)) }
do { return try dbWriter.write(Storage.logSlowWrites(info: info, updates: updates)) }
catch { return Storage.logIfNeeded(error, isWrite: true) } catch { return Storage.logIfNeeded(error, isWrite: true) }
} }
@ -545,10 +573,10 @@ open class Storage {
) { ) {
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return } guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return }
let info: CallInfo = (fileName, functionName, lineNumber) let info: CallInfo = { [weak self] in (self, [.write, .logIfSlow], fileName, functionName, lineNumber) }()
dbWriter.asyncWrite( dbWriter.asyncWrite(
Storage.logSlowWrites(info: info, updates: updates), Storage.perform(info: info, updates: updates),
completion: { db, result in completion: { db, result in
switch result { switch result {
case .failure(let error): Storage.logIfNeeded(error, isWrite: true) case .failure(let error): Storage.logIfNeeded(error, isWrite: true)
@ -572,7 +600,7 @@ open class Storage {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
let info: CallInfo = (fileName, functionName, lineNumber) let info: CallInfo = { [weak self] in (self, [.write, .logIfSlow], fileName, functionName, lineNumber) }()
/// **Note:** GRDB does have a `writePublisher` method but it appears to asynchronously trigger /// **Note:** GRDB does have a `writePublisher` method but it appears to asynchronously trigger
/// both the `output` and `complete` closures at the same time which causes a lot of unexpected /// both the `output` and `complete` closures at the same time which causes a lot of unexpected
@ -583,7 +611,7 @@ open class Storage {
/// which behaves in a much more expected way than the GRDB `writePublisher` does /// which behaves in a much more expected way than the GRDB `writePublisher` does
return Deferred { return Deferred {
Future { resolver in Future { resolver in
do { resolver(Result.success(try dbWriter.write(Storage.logSlowWrites(info: info, updates: updates)))) } do { resolver(Result.success(try dbWriter.write(Storage.perform(info: info, updates: updates)))) }
catch { catch {
Storage.logIfNeeded(error, isWrite: true) Storage.logIfNeeded(error, isWrite: true)
resolver(Result.failure(error)) resolver(Result.failure(error))
@ -593,6 +621,9 @@ open class Storage {
} }
open func readPublisher<T>( open func readPublisher<T>(
fileName: String = #file,
functionName: String = #function,
lineNumber: Int = #line,
using dependencies: Dependencies = Dependencies(), using dependencies: Dependencies = Dependencies(),
value: @escaping (Database) throws -> T value: @escaping (Database) throws -> T
) -> AnyPublisher<T, Error> { ) -> AnyPublisher<T, Error> {
@ -601,6 +632,8 @@ open class Storage {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
let info: CallInfo = { [weak self] in (self, [.read], fileName, functionName, lineNumber) }()
/// **Note:** GRDB does have a `readPublisher` method but it appears to asynchronously trigger /// **Note:** GRDB does have a `readPublisher` method but it appears to asynchronously trigger
/// both the `output` and `complete` closures at the same time which causes a lot of unexpected /// both the `output` and `complete` closures at the same time which causes a lot of unexpected
/// behaviours (this behaviour is apparently expected but still causes a number of odd behaviours in our code /// behaviours (this behaviour is apparently expected but still causes a number of odd behaviours in our code
@ -610,7 +643,7 @@ open class Storage {
/// which behaves in a much more expected way than the GRDB `readPublisher` does /// which behaves in a much more expected way than the GRDB `readPublisher` does
return Deferred { return Deferred {
Future { resolver in Future { resolver in
do { resolver(Result.success(try dbWriter.read(value))) } do { resolver(Result.success(try dbWriter.read(Storage.perform(info: info, updates: value)))) }
catch { catch {
Storage.logIfNeeded(error, isWrite: false) Storage.logIfNeeded(error, isWrite: false)
resolver(Result.failure(error)) resolver(Result.failure(error))
@ -620,12 +653,16 @@ open class Storage {
} }
@discardableResult public func read<T>( @discardableResult public func read<T>(
fileName: String = #file,
functionName: String = #function,
lineNumber: Int = #line,
using dependencies: Dependencies = Dependencies(), using dependencies: Dependencies = Dependencies(),
_ value: (Database) throws -> T? _ value: @escaping (Database) throws -> T?
) -> T? { ) -> T? {
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return nil } guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return nil }
do { return try dbWriter.read(value) } let info: CallInfo = { [weak self] in (self, [.read], fileName, functionName, lineNumber) }()
do { return try dbWriter.read(Storage.perform(info: info, updates: value)) }
catch { return Storage.logIfNeeded(error, isWrite: false) } catch { return Storage.logIfNeeded(error, isWrite: false) }
} }

@ -131,6 +131,7 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
// Retrieve the pagedRowId for the related value that is // Retrieve the pagedRowId for the related value that is
// getting deleted // getting deleted
let pagedTableName: String = self.pagedTableName
let pagedRowIds: [Int64] = Storage.shared let pagedRowIds: [Int64] = Storage.shared
.read { db in .read { db in
PagedData.pagedRowIdsForRelatedRowIds( PagedData.pagedRowIdsForRelatedRowIds(
@ -183,10 +184,13 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
// Store the instance variables locally to avoid unwrapping // Store the instance variables locally to avoid unwrapping
let dataCache: DataCache<T> = self.dataCache.wrappedValue let dataCache: DataCache<T> = self.dataCache.wrappedValue
let pageInfo: PagedData.PageInfo = self.pageInfo.wrappedValue let pageInfo: PagedData.PageInfo = self.pageInfo.wrappedValue
let pagedTableName: String = self.pagedTableName
let joinSQL: SQL? = self.joinSQL let joinSQL: SQL? = self.joinSQL
let orderSQL: SQL = self.orderSQL let orderSQL: SQL = self.orderSQL
let filterSQL: SQL = self.filterSQL let filterSQL: SQL = self.filterSQL
let dataQuery: ([Int64]) -> any FetchRequest<T> = self.dataQuery
let associatedRecords: [ErasedAssociatedRecord] = self.associatedRecords let associatedRecords: [ErasedAssociatedRecord] = self.associatedRecords
let observedTableChangeTypes: [String: PagedData.ObservedChanges] = self.observedTableChangeTypes
let getAssociatedDataInfo: (Database, PagedData.PageInfo) -> AssociatedDataInfo = { db, updatedPageInfo in let getAssociatedDataInfo: (Database, PagedData.PageInfo) -> AssociatedDataInfo = { db, updatedPageInfo in
associatedRecords.map { associatedRecord in associatedRecords.map { associatedRecord in
let hasChanges: Bool = associatedRecord.tryUpdateForDatabaseCommit( let hasChanges: Bool = associatedRecord.tryUpdateForDatabaseCommit(

@ -1096,6 +1096,7 @@ public final class JobQueue: Hashable {
hasStartedAtLeastOnce.mutate { $0 = true } hasStartedAtLeastOnce.mutate { $0 = true }
// Get any pending jobs // Get any pending jobs
let jobVariants: [Job.Variant] = self.jobVariants
let jobIdsAlreadyRunning: Set<Int64> = currentlyRunningJobIds.wrappedValue let jobIdsAlreadyRunning: Set<Int64> = currentlyRunningJobIds.wrappedValue
let jobsAlreadyInQueue: Set<Int64> = pendingJobsQueue.wrappedValue.compactMap { $0.id }.asSet() let jobsAlreadyInQueue: Set<Int64> = pendingJobsQueue.wrappedValue.compactMap { $0.id }.asSet()
let jobsToRun: [Job] = dependencies.storage.read(using: dependencies) { db in let jobsToRun: [Job] = dependencies.storage.read(using: dependencies) { db in
@ -1320,6 +1321,7 @@ public final class JobQueue: Hashable {
} }
private func scheduleNextSoonestJob(using dependencies: Dependencies) { private func scheduleNextSoonestJob(using dependencies: Dependencies) {
let jobVariants: [Job.Variant] = self.jobVariants
let jobIdsAlreadyRunning: Set<Int64> = currentlyRunningJobIds.wrappedValue let jobIdsAlreadyRunning: Set<Int64> = currentlyRunningJobIds.wrappedValue
let nextJobTimestamp: TimeInterval? = dependencies.storage.read(using: dependencies) { db in let nextJobTimestamp: TimeInterval? = dependencies.storage.read(using: dependencies) { db in
try Job try Job

@ -112,16 +112,6 @@ public enum AppSetup {
} }
}, },
onComplete: { result, needsConfigSync in onComplete: { result, needsConfigSync in
// After the migrations have run but before the migration completion we load the
// SessionUtil state and update the 'needsConfigSync' flag based on whether the
// configs also need to be sync'ed
if Identity.userExists() {
SessionUtil.loadState(
userPublicKey: getUserHexEncodedPublicKey(),
ed25519SecretKey: Identity.fetchUserEd25519KeyPair()?.secretKey
)
}
// The 'needsConfigSync' flag should be based on whether either a migration or the // The 'needsConfigSync' flag should be based on whether either a migration or the
// configs need to be sync'ed // configs need to be sync'ed
migrationsCompletion(result, (needsConfigSync || SessionUtil.needsSync)) migrationsCompletion(result, (needsConfigSync || SessionUtil.needsSync))

Loading…
Cancel
Save