Improve configuration on app launch/active (#821)

- Centralize context initialization/refresh in platform-specific app
delegates
- Prevent multiple calls to .onApplicationActive()
- Simplify local/remote profile fingerprint comparison
- Revert to always replacing Core Data entities
- The remote store somehow ended up having duplicates, which caused
repeated imports of remote profiles due to randomly different
fingerprints
- Optimize reload of in-app receipt
This commit is contained in:
Davide 2024-11-06 18:42:42 +01:00 committed by GitHub
parent d8c4e87239
commit 68df6066ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 55 additions and 47 deletions

View File

@ -41,7 +41,7 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source", "location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
"state" : { "state" : {
"revision" : "caf31aff2e2641356de0d01f3c2c2d0d635d6a2b" "revision" : "e7bd9636ac31d6111b0bc7c171398e68eeb384b5"
} }
}, },
{ {

View File

@ -35,6 +35,11 @@ final class AppDelegate: NSObject {
func configure(with uiConfiguring: UILibraryConfiguring) { func configure(with uiConfiguring: UILibraryConfiguring) {
UILibrary(uiConfiguring) UILibrary(uiConfiguring)
.configure(with: context) .configure()
Task {
pp_log(.app, .notice, "Fetch providers index...")
try await context.providerManager.fetchIndex(from: API.shared)
}
} }
} }

View File

@ -30,7 +30,7 @@ import SwiftUI
extension AppDelegate: UIApplicationDelegate { extension AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
configure(with: AppUIMain(isStartedFromLoginItem: false)) configure(with: AppUIMain())
return true return true
} }
} }

View File

@ -33,8 +33,11 @@ import SwiftUI
extension AppDelegate: NSApplicationDelegate { extension AppDelegate: NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
configure(with: AppUIMain(isStartedFromLoginItem: isStartedFromLoginItem)) configure(with: AppUIMain())
hideIfLoginItem() context.onApplicationActive()
if isStartedFromLoginItem {
AppWindow.shared.isVisible = false
}
} }
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
@ -60,12 +63,6 @@ private extension AppDelegate {
var isStartedFromLoginItem: Bool { var isStartedFromLoginItem: Bool {
NSApp.isHidden NSApp.isHidden
} }
func hideIfLoginItem() {
if isStartedFromLoginItem {
AppWindow.shared.isVisible = false
}
}
} }
extension PassepartoutApp { extension PassepartoutApp {

View File

@ -40,7 +40,7 @@ let package = Package(
], ],
dependencies: [ dependencies: [
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.9.0"), // .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.9.0"),
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "caf31aff2e2641356de0d01f3c2c2d0d635d6a2b"), .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "e7bd9636ac31d6111b0bc7c171398e68eeb384b5"),
// .package(path: "../../../passepartoutkit-source"), // .package(path: "../../../passepartoutkit-source"),
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", from: "0.9.1"), .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", from: "0.9.1"),
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"), // .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"),

View File

@ -50,7 +50,7 @@ extension AppData {
} fromMapper: { } fromMapper: {
try fromMapper($0, registry: registry, coder: coder) try fromMapper($0, registry: registry, coder: coder)
} toMapper: { } toMapper: {
try toMapper($0, $1, $2, registry: registry, coder: coder) try toMapper($0, $1, registry: registry, coder: coder)
} onResultError: { } onResultError: {
onResultError?($0) ?? .ignore onResultError?($0) ?? .ignore
} }
@ -73,14 +73,13 @@ private extension AppData {
static func toMapper( static func toMapper(
_ profile: Profile, _ profile: Profile,
_ oldCdEntity: CDProfileV3?,
_ context: NSManagedObjectContext, _ context: NSManagedObjectContext,
registry: Registry, registry: Registry,
coder: ProfileCoder coder: ProfileCoder
) throws -> CDProfileV3 { ) throws -> CDProfileV3 {
let encoded = try registry.encodedProfile(profile, with: coder) let encoded = try registry.encodedProfile(profile, with: coder)
let cdProfile = oldCdEntity ?? CDProfileV3(context: context) let cdProfile = CDProfileV3(context: context)
cdProfile.uuid = profile.id cdProfile.uuid = profile.id
cdProfile.name = profile.name cdProfile.name = profile.name
cdProfile.encoded = encoded cdProfile.encoded = encoded

View File

@ -27,15 +27,11 @@ import Foundation
@_exported import UILibrary @_exported import UILibrary
public final class AppUIMain: UILibraryConfiguring { public final class AppUIMain: UILibraryConfiguring {
private let isStartedFromLoginItem: Bool public init() {
public init(isStartedFromLoginItem: Bool) {
self.isStartedFromLoginItem = isStartedFromLoginItem
} }
public func configure(with context: AppContext) { public func configure() {
assertMissingImplementations() assertMissingImplementations()
context.onApplicationActive()
} }
} }

View File

@ -30,6 +30,6 @@ public final class AppUITV: UILibraryConfiguring {
public init() { public init() {
} }
public func configure(with context: AppContext) { public func configure() {
} }
} }

View File

@ -353,18 +353,28 @@ private extension ProfileManager {
} }
objectWillChange.send() objectWillChange.send()
let profilesToImport = result
let allFingerprints = allProfiles.values.reduce(into: [:]) {
$0[$1.id] = $1.attributes.fingerprint
}
let remotelyDeletedIds = Set(allProfiles.keys).subtracting(Set(allRemoteProfiles.keys))
let deletingRemotely = deletingRemotely
Task.detached { [weak self] in Task.detached { [weak self] in
guard let self else { guard let self else {
return return
} }
pp_log(.app, .info, "Start importing remote profiles...") pp_log(.app, .info, "Start importing remote profiles...")
pp_log(.app, .debug, "Local fingerprints:")
let localFingerprints: [Profile.ID: UUID] = await allProfiles.values.reduce(into: [:]) {
$0[$1.id] = $1.attributes.fingerprint
pp_log(.app, .debug, "\t\($1.id) = \($1.attributes.fingerprint?.description ?? "nil")")
}
pp_log(.app, .debug, "Remote fingerprints:")
let remoteFingerprints: [Profile.ID: UUID] = result.reduce(into: [:]) {
$0[$1.id] = $1.attributes.fingerprint
pp_log(.app, .debug, "\t\($1.id) = \($1.attributes.fingerprint?.description ?? "nil")")
}
let profilesToImport = result
let remotelyDeletedIds = await Set(allProfiles.keys).subtracting(Set(allRemoteProfiles.keys))
let deletingRemotely = deletingRemotely
var idsToRemove: [Profile.ID] = [] var idsToRemove: [Profile.ID] = []
if !remotelyDeletedIds.isEmpty { if !remotelyDeletedIds.isEmpty {
pp_log(.app, .info, "Will \(deletingRemotely ? "delete" : "retain") local profiles not present in remote repository: \(remotelyDeletedIds)") pp_log(.app, .info, "Will \(deletingRemotely ? "delete" : "retain") local profiles not present in remote repository: \(remotelyDeletedIds)")
@ -380,11 +390,9 @@ private extension ProfileManager {
idsToRemove.append(remoteProfile.id) idsToRemove.append(remoteProfile.id)
continue continue
} }
if let localFingerprint = allFingerprints[remoteProfile.id] { guard remoteFingerprints[remoteProfile.id] != localFingerprints[remoteProfile.id] else {
guard remoteProfile.attributes.fingerprint != localFingerprint else { pp_log(.app, .info, "Skip re-importing local profile \(remoteProfile.id)")
pp_log(.app, .info, "Skip re-importing local profile \(remoteProfile.id)") continue
continue
}
} }
pp_log(.app, .notice, "Import remote profile \(remoteProfile.id)...") pp_log(.app, .notice, "Import remote profile \(remoteProfile.id)...")
try await save(remoteProfile) try await save(remoteProfile)

View File

@ -104,6 +104,8 @@ extension IAPManager {
purchasedProducts.removeAll() purchasedProducts.removeAll()
eligibleFeatures.removeAll() eligibleFeatures.removeAll()
pp_log(.app, .notice, "Reload IAP receipt...")
if let receipt = await receiptReader.receipt(at: userLevel) { if let receipt = await receiptReader.receipt(at: userLevel) {
if let originalBuildNumber = receipt.originalBuildNumber { if let originalBuildNumber = receipt.originalBuildNumber {
purchasedAppBuild = originalBuildNumber purchasedAppBuild = originalBuildNumber

View File

@ -52,7 +52,7 @@ public actor CoreDataRepository<CD, T>: NSObject,
private let fromMapper: (CD) throws -> T? private let fromMapper: (CD) throws -> T?
private let toMapper: (T, CD?, NSManagedObjectContext) throws -> CD private let toMapper: (T, NSManagedObjectContext) throws -> CD
private let onResultError: ((Error) -> CoreDataResultAction)? private let onResultError: ((Error) -> CoreDataResultAction)?
@ -66,7 +66,7 @@ public actor CoreDataRepository<CD, T>: NSObject,
observingResults: Bool, observingResults: Bool,
beforeFetch: ((NSFetchRequest<CD>) -> Void)? = nil, beforeFetch: ((NSFetchRequest<CD>) -> Void)? = nil,
fromMapper: @escaping (CD) throws -> T?, fromMapper: @escaping (CD) throws -> T?,
toMapper: @escaping (T, CD?, NSManagedObjectContext) throws -> CD, toMapper: @escaping (T, NSManagedObjectContext) throws -> CD,
onResultError: ((Error) -> CoreDataResultAction)? = nil onResultError: ((Error) -> CoreDataResultAction)? = nil
) { ) {
guard let entityName = CD.entity().name else { guard let entityName = CD.entity().name else {
@ -127,11 +127,9 @@ public actor CoreDataRepository<CD, T>: NSObject,
existingIds existingIds
) )
let existing = try context.fetch(request) let existing = try context.fetch(request)
existing.forEach(context.delete)
for entity in entities { for entity in entities {
let oldCdEntity = existing.first { _ = try self.toMapper(entity, context)
$0.uuid == entity.uuid
}
_ = try self.toMapper(entity, oldCdEntity, context)
} }
try context.save() try context.save()
} catch { } catch {

View File

@ -41,6 +41,8 @@ public final class AppContext: ObservableObject {
public let providerManager: ProviderManager public let providerManager: ProviderManager
private var isActivating = false
private var subscriptions: Set<AnyCancellable> private var subscriptions: Set<AnyCancellable>
public init( public init(
@ -61,16 +63,20 @@ public final class AppContext: ObservableObject {
} }
public func onApplicationActive() { public func onApplicationActive() {
guard !isActivating else {
return
}
isActivating = true
Task { Task {
do { do {
pp_log(.app, .notice, "Application became active") pp_log(.app, .notice, "Application became active")
pp_log(.app, .notice, "Reload IAP receipt...")
await iapManager.reloadReceipt() await iapManager.reloadReceipt()
pp_log(.app, .notice, "Prepare tunnel and purge stale data...") pp_log(.app, .notice, "Prepare tunnel and purge stale data...")
try await tunnel.prepare(purge: true) try await tunnel.prepare(purge: true)
} catch { } catch {
pp_log(.app, .fault, "Unable to prepare tunnel: \(error)") pp_log(.app, .fault, "Unable to prepare tunnel: \(error)")
} }
isActivating = false
} }
} }
} }

View File

@ -30,7 +30,7 @@ import PassepartoutKit
@MainActor @MainActor
public protocol UILibraryConfiguring { public protocol UILibraryConfiguring {
func configure(with context: AppContext) func configure()
} }
public final class UILibrary: UILibraryConfiguring { public final class UILibrary: UILibraryConfiguring {
@ -40,15 +40,12 @@ public final class UILibrary: UILibraryConfiguring {
self.uiConfiguring = uiConfiguring self.uiConfiguring = uiConfiguring
} }
public func configure(with context: AppContext) { public func configure() {
PassepartoutConfiguration.shared.configureLogging( PassepartoutConfiguration.shared.configureLogging(
to: BundleConfiguration.urlForAppLog, to: BundleConfiguration.urlForAppLog,
parameters: Constants.shared.log, parameters: Constants.shared.log,
logsPrivateData: UserDefaults.appGroup.bool(forKey: AppPreference.logsPrivateData.key) logsPrivateData: UserDefaults.appGroup.bool(forKey: AppPreference.logsPrivateData.key)
) )
Task { uiConfiguring?.configure()
try await context.providerManager.fetchIndex(from: API.shared)
}
uiConfiguring?.configure(with: context)
} }
} }