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",
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
"state" : {
"revision" : "caf31aff2e2641356de0d01f3c2c2d0d635d6a2b"
"revision" : "e7bd9636ac31d6111b0bc7c171398e68eeb384b5"
}
},
{

View File

@ -35,6 +35,11 @@ final class AppDelegate: NSObject {
func configure(with uiConfiguring: UILibraryConfiguring) {
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 {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
configure(with: AppUIMain(isStartedFromLoginItem: false))
configure(with: AppUIMain())
return true
}
}

View File

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

View File

@ -40,7 +40,7 @@ let package = Package(
],
dependencies: [
// .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(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"),

View File

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

View File

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

View File

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

View File

@ -353,18 +353,28 @@ private extension ProfileManager {
}
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
guard let self else {
return
}
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] = []
if !remotelyDeletedIds.isEmpty {
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)
continue
}
if let localFingerprint = allFingerprints[remoteProfile.id] {
guard remoteProfile.attributes.fingerprint != localFingerprint else {
pp_log(.app, .info, "Skip re-importing local profile \(remoteProfile.id)")
continue
}
guard remoteFingerprints[remoteProfile.id] != localFingerprints[remoteProfile.id] else {
pp_log(.app, .info, "Skip re-importing local profile \(remoteProfile.id)")
continue
}
pp_log(.app, .notice, "Import remote profile \(remoteProfile.id)...")
try await save(remoteProfile)

View File

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

View File

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

View File

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

View File

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