mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2024-12-24 18:32:36 +00:00
21340e9f56
Loading remote profiles before local profiles may cause duplicated NE managers. This happened because if local profiles are empty, any remote profile is imported regardless of their former existence in the local store. The importer just doesn't know. Therefore, revisit the sequence of AppContext registrations: - First off - Skip Tunnel prepare() because NEProfileRepository.fetch() does it already - NE is both Tunnel and ProfileRepository, so calling tunnel.prepare() loads local NE profiles twice - onLaunch() - **run this once and before anything else** - Read local profiles - Reload in-app receipt - Observe in-app eligibility → Triggers onEligibleFeatures() - Observe profile save → Triggers onSaveProfile() - Fetch providers index - onForeground() - Read local profiles - Read remote profiles, and toggle CloudKit sync based on eligibility - onEligibleFeatures() - Read remote profiles, and toggle CloudKit sync based on eligibility - onSaveProfile() - Reconnect if necessary
233 lines
7.0 KiB
Swift
233 lines
7.0 KiB
Swift
//
|
|
// AppContext.swift
|
|
// Passepartout
|
|
//
|
|
// Created by Davide De Rosa on 8/29/24.
|
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
|
//
|
|
// https://github.com/passepartoutvpn
|
|
//
|
|
// This file is part of Passepartout.
|
|
//
|
|
// Passepartout is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Passepartout is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
//
|
|
|
|
import Combine
|
|
import CommonLibrary
|
|
import CommonUtils
|
|
import Foundation
|
|
import PassepartoutKit
|
|
|
|
@MainActor
|
|
public final class AppContext: ObservableObject {
|
|
public let iapManager: IAPManager
|
|
|
|
public let registry: Registry
|
|
|
|
public let profileManager: ProfileManager
|
|
|
|
public let tunnel: ExtendedTunnel
|
|
|
|
public let providerManager: ProviderManager
|
|
|
|
private var launchTask: Task<Void, Error>?
|
|
|
|
private var pendingTask: Task<Void, Never>?
|
|
|
|
private var subscriptions: Set<AnyCancellable>
|
|
|
|
public init(
|
|
iapManager: IAPManager,
|
|
registry: Registry,
|
|
profileManager: ProfileManager,
|
|
tunnel: ExtendedTunnel,
|
|
providerManager: ProviderManager
|
|
) {
|
|
self.iapManager = iapManager
|
|
self.registry = registry
|
|
self.profileManager = profileManager
|
|
self.tunnel = tunnel
|
|
self.providerManager = providerManager
|
|
subscriptions = []
|
|
}
|
|
}
|
|
|
|
// MARK: - Observation
|
|
|
|
// invoked by AppDelegate
|
|
extension AppContext {
|
|
public func onApplicationActive() {
|
|
Task {
|
|
// TODO: ###, should handle AppError.couldNotLaunch (although extremely rare)
|
|
try await onForeground()
|
|
}
|
|
}
|
|
}
|
|
|
|
// invoked on internal events
|
|
private extension AppContext {
|
|
func onLaunch() async throws {
|
|
pp_log(.app, .notice, "Application did launch")
|
|
|
|
pp_log(.App.profiles, .info, "Read and observe local profiles...")
|
|
try await profileManager.observeLocal()
|
|
|
|
iapManager.observeObjects()
|
|
await iapManager.reloadReceipt()
|
|
|
|
iapManager
|
|
.$eligibleFeatures
|
|
.removeDuplicates()
|
|
.sink { [weak self] eligible in
|
|
Task {
|
|
try await self?.onEligibleFeatures(eligible)
|
|
}
|
|
}
|
|
.store(in: &subscriptions)
|
|
|
|
profileManager
|
|
.didChange
|
|
.sink { [weak self] event in
|
|
switch event {
|
|
case .save(let profile):
|
|
Task {
|
|
try await self?.onSaveProfile(profile)
|
|
}
|
|
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
.store(in: &subscriptions)
|
|
|
|
do {
|
|
pp_log(.app, .notice, "Fetch providers index...")
|
|
try await providerManager.fetchIndex(from: API.shared)
|
|
} catch {
|
|
pp_log(.app, .error, "Unable to fetch providers index: \(error)")
|
|
}
|
|
}
|
|
|
|
func onForeground() async throws {
|
|
let didLaunch = try await waitForTasks()
|
|
guard !didLaunch else {
|
|
return // foreground is redundant after launch
|
|
}
|
|
|
|
pp_log(.app, .notice, "Application did enter foreground")
|
|
pendingTask = Task {
|
|
do {
|
|
pp_log(.App.profiles, .info, "Refresh local profiles observers...")
|
|
try await profileManager.observeLocal()
|
|
} catch {
|
|
pp_log(.App.profiles, .error, "Unable to re-observe local profiles: \(error)")
|
|
}
|
|
await iapManager.reloadReceipt()
|
|
}
|
|
await pendingTask?.value
|
|
pendingTask = nil
|
|
}
|
|
|
|
func onEligibleFeatures(_ features: Set<AppFeature>) async throws {
|
|
try await waitForTasks()
|
|
|
|
pp_log(.app, .notice, "Application did update eligible features")
|
|
pendingTask = Task {
|
|
let isEligible = features.contains(.sharing)
|
|
do {
|
|
pp_log(.App.profiles, .info, "Refresh remote profiles observers (eligible=\(isEligible), CloudKit=\(isCloudKitEnabled))...")
|
|
try await profileManager.observeRemote(isEligible && isCloudKitEnabled)
|
|
} catch {
|
|
pp_log(.App.profiles, .error, "Unable to re-observe remote profiles: \(error)")
|
|
}
|
|
}
|
|
await pendingTask?.value
|
|
pendingTask = nil
|
|
}
|
|
|
|
func onSaveProfile(_ profile: Profile) async throws {
|
|
try await waitForTasks()
|
|
|
|
pp_log(.app, .notice, "Application did save profile (\(profile.id))")
|
|
guard profile.id == tunnel.currentProfile?.id else {
|
|
pp_log(.app, .debug, "Profile \(profile.id) is not current, do nothing")
|
|
return
|
|
}
|
|
guard [.active, .activating].contains(tunnel.status) else {
|
|
pp_log(.app, .debug, "Connection is not active (\(tunnel.status)), do nothing")
|
|
return
|
|
}
|
|
pendingTask = Task {
|
|
do {
|
|
if profile.isInteractive {
|
|
pp_log(.app, .info, "Profile \(profile.id) is interactive, disconnect")
|
|
try await tunnel.disconnect()
|
|
return
|
|
}
|
|
do {
|
|
pp_log(.app, .info, "Reconnect profile \(profile.id)")
|
|
try await tunnel.connect(with: profile)
|
|
} catch {
|
|
pp_log(.app, .error, "Unable to reconnect profile \(profile.id), disconnect: \(error)")
|
|
try await tunnel.disconnect()
|
|
}
|
|
} catch {
|
|
pp_log(.app, .error, "Unable to reinstate connection on save profile \(profile.id): \(error)")
|
|
}
|
|
}
|
|
await pendingTask?.value
|
|
pendingTask = nil
|
|
}
|
|
|
|
@discardableResult
|
|
func waitForTasks() async throws -> Bool {
|
|
var didLaunch = false
|
|
|
|
// must launch once before anything else
|
|
if launchTask == nil {
|
|
launchTask = Task {
|
|
do {
|
|
try await onLaunch()
|
|
} catch {
|
|
launchTask = nil // redo launch
|
|
throw AppError.couldNotLaunch(reason: error)
|
|
}
|
|
}
|
|
didLaunch = true
|
|
}
|
|
|
|
// will throw on .couldNotLaunch
|
|
// next wait will re-attempt launch (launchTask == nil)
|
|
try await launchTask?.value
|
|
|
|
// wait for pending task if any
|
|
await pendingTask?.value
|
|
pendingTask = nil
|
|
|
|
return didLaunch
|
|
}
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private extension AppContext {
|
|
var isCloudKitEnabled: Bool {
|
|
#if os(tvOS)
|
|
true
|
|
#else
|
|
FileManager.default.ubiquityIdentityToken != nil
|
|
#endif
|
|
}
|
|
}
|