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:
parent
d8c4e87239
commit
68df6066ba
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,6 @@ public final class AppUITV: UILibraryConfiguring {
|
||||||
public init() {
|
public init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func configure(with context: AppContext) {
|
public func configure() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue