Persist managers state to generic key-value store

Move all persisted state out of AppManager to where it really
belongs. To do that, inject a shared KeyValueStore object into
managers that need to persist part of their state in a strongly
typed manner.

Below are persisted states:

- PersistenceManager
    - persistenceAuthor

- ProfileManager
    - activeProfileId

- UpgradeManager (formerly AppManager)
    - didMigrateToV2 (migrate former value)

- VPNManager
    - tunnelLogFormat
    - masksPrivateData

A similar approach is used for app-specific preferences, by using
a strongly typed enum (AppPreference) together with SwiftUI
@AppStorage property wrapper.

Worth moving logging logic into a specific LogManager.

Finally, drop any former view dependency on AppManager, as states
are now accessed through specific managers.
This commit is contained in:
Davide De Rosa 2022-06-15 20:53:37 +02:00
parent 127ba28a8c
commit 14b42fbea5
20 changed files with 477 additions and 302 deletions

View File

@ -16,6 +16,7 @@
0E0BD27927B2EBE500583AC5 /* ShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0BD27827B2EBE500583AC5 /* ShortcutsView.swift */; }; 0E0BD27927B2EBE500583AC5 /* ShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0BD27827B2EBE500583AC5 /* ShortcutsView.swift */; };
0E0C0729236087A100155AAC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E0C072B236087A100155AAC /* InfoPlist.strings */; }; 0E0C0729236087A100155AAC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E0C072B236087A100155AAC /* InfoPlist.strings */; };
0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E12BC8E27F62C8500B2F912 /* Validators.swift */; }; 0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E12BC8E27F62C8500B2F912 /* Validators.swift */; };
0E293851285A70AC002A6E0E /* AppPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E293850285A70AC002A6E0E /* AppPreference.swift */; };
0E2A8D4927ADF87F00207D04 /* PassepartoutApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2A8D4727ADF87F00207D04 /* PassepartoutApp.swift */; }; 0E2A8D4927ADF87F00207D04 /* PassepartoutApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2A8D4727ADF87F00207D04 /* PassepartoutApp.swift */; };
0E2A8D4F27B04BBA00207D04 /* OrganizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */; }; 0E2A8D4F27B04BBA00207D04 /* OrganizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */; };
0E2AC24522EC3AC10037B4B0 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 0E2AC24422EC3AC10037B4B0 /* Settings.bundle */; }; 0E2AC24522EC3AC10037B4B0 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 0E2AC24422EC3AC10037B4B0 /* Settings.bundle */; };
@ -47,7 +48,6 @@
0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5349BD27C16A4500C71BB3 /* StyledPicker.swift */; }; 0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5349BD27C16A4500C71BB3 /* StyledPicker.swift */; };
0E5349C627C176C200C71BB3 /* EndpointView+OpenVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */; }; 0E5349C627C176C200C71BB3 /* EndpointView+OpenVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */; };
0E5349C827C176D100C71BB3 /* EndpointView+WireGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */; }; 0E5349C827C176D100C71BB3 /* EndpointView+WireGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */; };
0E53E63727E34FE2001D4902 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E53E63627E34FE2001D4902 /* AppContext.swift */; };
0E5683B927C2825D00EAF1CD /* DiagnosticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5683B827C2825D00EAF1CD /* DiagnosticsView.swift */; }; 0E5683B927C2825D00EAF1CD /* DiagnosticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5683B827C2825D00EAF1CD /* DiagnosticsView.swift */; };
0E6059CB27FCC5DE003F4063 /* Flags.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E6059C827FCC5DD003F4063 /* Flags.xcassets */; }; 0E6059CB27FCC5DE003F4063 /* Flags.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E6059C827FCC5DD003F4063 /* Flags.xcassets */; };
0E6059CC27FCC5DE003F4063 /* Providers.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E6059C927FCC5DE003F4063 /* Providers.xcassets */; }; 0E6059CC27FCC5DE003F4063 /* Providers.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E6059C927FCC5DE003F4063 /* Providers.xcassets */; };
@ -123,6 +123,7 @@
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */; }; 0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */; };
0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */; }; 0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */; };
0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */; }; 0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */; };
0EF2CC03285AFED800E501D5 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2CC02285AFED800E501D5 /* AppContext.swift */; };
0EF8C5A828213C510053CE89 /* OrganizerView+Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */; }; 0EF8C5A828213C510053CE89 /* OrganizerView+Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -202,6 +203,7 @@
0E12BC8E27F62C8500B2F912 /* Validators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validators.swift; sourceTree = "<group>"; }; 0E12BC8E27F62C8500B2F912 /* Validators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validators.swift; sourceTree = "<group>"; };
0E1C0A52238FFF97009FC087 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 0E1C0A52238FFF97009FC087 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
0E23B4A12298559800304C30 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; }; 0E23B4A12298559800304C30 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
0E293850285A70AC002A6E0E /* AppPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreference.swift; sourceTree = "<group>"; };
0E2A8D4727ADF87F00207D04 /* PassepartoutApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassepartoutApp.swift; sourceTree = "<group>"; }; 0E2A8D4727ADF87F00207D04 /* PassepartoutApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassepartoutApp.swift; sourceTree = "<group>"; };
0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizerView.swift; sourceTree = "<group>"; }; 0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizerView.swift; sourceTree = "<group>"; };
0E2AC24422EC3AC10037B4B0 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; }; 0E2AC24422EC3AC10037B4B0 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
@ -232,7 +234,6 @@
0E5349BD27C16A4500C71BB3 /* StyledPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyledPicker.swift; sourceTree = "<group>"; }; 0E5349BD27C16A4500C71BB3 /* StyledPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyledPicker.swift; sourceTree = "<group>"; };
0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EndpointView+OpenVPN.swift"; sourceTree = "<group>"; }; 0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EndpointView+OpenVPN.swift"; sourceTree = "<group>"; };
0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EndpointView+WireGuard.swift"; sourceTree = "<group>"; }; 0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EndpointView+WireGuard.swift"; sourceTree = "<group>"; };
0E53E63627E34FE2001D4902 /* AppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = "<group>"; };
0E5683B827C2825D00EAF1CD /* DiagnosticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsView.swift; sourceTree = "<group>"; }; 0E5683B827C2825D00EAF1CD /* DiagnosticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsView.swift; sourceTree = "<group>"; };
0E57F63820C83FC5008323CF /* Passepartout.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Passepartout.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0E57F63820C83FC5008323CF /* Passepartout.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Passepartout.app; sourceTree = BUILT_PRODUCTS_DIR; };
0E57F64720C83FC7008323CF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 0E57F64720C83FC7008323CF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -341,6 +342,7 @@
0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderView.swift; sourceTree = "<group>"; }; 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderView.swift; sourceTree = "<group>"; };
0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProfileView.swift; sourceTree = "<group>"; }; 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProfileView.swift; sourceTree = "<group>"; };
0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderViewModel.swift; sourceTree = "<group>"; }; 0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderViewModel.swift; sourceTree = "<group>"; };
0EF2CC02285AFED800E501D5 /* AppContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = "<group>"; };
0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Profiles.swift"; sourceTree = "<group>"; }; 0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Profiles.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -565,7 +567,8 @@
0EB17EA027D2263700D473B5 /* Constants */ = { 0EB17EA027D2263700D473B5 /* Constants */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0E53E63627E34FE2001D4902 /* AppContext.swift */, 0EF2CC02285AFED800E501D5 /* AppContext.swift */,
0E293850285A70AC002A6E0E /* AppPreference.swift */,
0EB17EA527D2263700D473B5 /* Constants+Extensions.swift */, 0EB17EA527D2263700D473B5 /* Constants+Extensions.swift */,
0E6059CE27FCC618003F4063 /* SwiftGen+Assets.swift */, 0E6059CE27FCC618003F4063 /* SwiftGen+Assets.swift */,
0EBC075F27EC587900208AD9 /* SwiftGen+Strings.swift */, 0EBC075F27EC587900208AD9 /* SwiftGen+Strings.swift */,
@ -907,7 +910,6 @@
0EBC075D27EC529000208AD9 /* DebugLog+Constants.swift in Sources */, 0EBC075D27EC529000208AD9 /* DebugLog+Constants.swift in Sources */,
0E3CD47F280DA14B007075C0 /* AddProfileMenu.swift in Sources */, 0E3CD47F280DA14B007075C0 /* AddProfileMenu.swift in Sources */,
0EB17EAA27D226C900D473B5 /* Constants+Extensions.swift in Sources */, 0EB17EAA27D226C900D473B5 /* Constants+Extensions.swift in Sources */,
0E53E63727E34FE2001D4902 /* AppContext.swift in Sources */,
0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */, 0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */,
0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */, 0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */,
0EBE880F281B18DE0090D9E6 /* ProfileRow.swift in Sources */, 0EBE880F281B18DE0090D9E6 /* ProfileRow.swift in Sources */,
@ -930,8 +932,10 @@
0E71ACEF27C106B500F85C4B /* ProviderPresetView.swift in Sources */, 0E71ACEF27C106B500F85C4B /* ProviderPresetView.swift in Sources */,
0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */, 0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */,
0EF0FAF627DD0211007EB181 /* PaywallView.swift in Sources */, 0EF0FAF627DD0211007EB181 /* PaywallView.swift in Sources */,
0E293851285A70AC002A6E0E /* AppPreference.swift in Sources */,
0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */, 0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */,
0E2C172B27CB63F9007E8488 /* Reviewer.swift in Sources */, 0E2C172B27CB63F9007E8488 /* Reviewer.swift in Sources */,
0EF2CC03285AFED800E501D5 /* AppContext.swift in Sources */,
0E71ACDD27C0295C00F85C4B /* View+Extensions.swift in Sources */, 0E71ACDD27C0295C00F85C4B /* View+Extensions.swift in Sources */,
0E34A2B627CAA8CC00C73B67 /* Core+L10n.swift in Sources */, 0E34A2B627CAA8CC00C73B67 /* Core+L10n.swift in Sources */,
0E7577DF2817E22C00081CBE /* VPNToggle.swift in Sources */, 0E7577DF2817E22C00081CBE /* VPNToggle.swift in Sources */,

View File

@ -33,6 +33,8 @@ import PassepartoutServices
class AppContext { class AppContext {
static let shared = AppContext() static let shared = AppContext()
private let logManager: LogManager
private let profilesPersistence: Persistence private let profilesPersistence: Persistence
private let providersPersistence: Persistence private let providersPersistence: Persistence
@ -45,7 +47,7 @@ class AppContext {
providersPersistence.containerURLs providersPersistence.containerURLs
} }
let appManager: AppManager let upgradeManager: UpgradeManager
let providerManager: ProviderManager let providerManager: ProviderManager
@ -62,18 +64,15 @@ class AppContext {
private var cancellables: Set<AnyCancellable> = [] private var cancellables: Set<AnyCancellable> = []
private init() { private init() {
let store = UserDefaultsStore(defaults: .standard)
// core logManager = LogManager(logFile: Constants.Log.appFileURL)
logManager.logLevel = Constants.Log.logLevel
logManager.logFormat = Constants.Log.appLogFormat
logManager.configureLogging()
pp_log.info("Logging to: \(logManager.logFile!)")
appManager = AppManager() let persistenceManager = PersistenceManager(store: store)
appManager.logLevel = Constants.Log.logLevel
appManager.logFile = Constants.Log.appFileURL
appManager.logFormat = Constants.Log.appLogFormat
appManager.tunnelLogFormat = Constants.Log.tunnelLogFormat
appManager.configureLogging()
pp_log.info("Logging to: \(appManager.logFile!)")
let persistenceManager = PersistenceManager(author: appManager.persistenceAuthor)
profilesPersistence = persistenceManager.profilesPersistence( profilesPersistence = persistenceManager.profilesPersistence(
withName: Constants.Persistence.profilesContainerName withName: Constants.Persistence.profilesContainerName
) )
@ -81,6 +80,8 @@ class AppContext {
withName: Constants.Persistence.providersContainerName withName: Constants.Persistence.providersContainerName
) )
upgradeManager = UpgradeManager(store: store)
providerManager = ProviderManager( providerManager = ProviderManager(
appBuild: Constants.Global.appBuildNumber, appBuild: Constants.Global.appBuildNumber,
bundleServices: DefaultWebServices.bundledServices( bundleServices: DefaultWebServices.bundledServices(
@ -95,6 +96,7 @@ class AppContext {
) )
profileManager = ProfileManager( profileManager = ProfileManager(
store: store,
providerManager: providerManager, providerManager: providerManager,
appGroup: Constants.App.appGroupId, appGroup: Constants.App.appGroupId,
keychainLabel: Unlocalized.Keychain.passwordLabel, keychainLabel: Unlocalized.Keychain.passwordLabel,
@ -112,7 +114,7 @@ class AppContext {
) )
#endif #endif
vpnManager = VPNManager( vpnManager = VPNManager(
appManager: appManager, store: store,
profileManager: profileManager, profileManager: profileManager,
providerManager: providerManager, providerManager: providerManager,
strategy: strategy strategy: strategy
@ -137,14 +139,11 @@ class AppContext {
// core // core
providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager
vpnManager.tunnelLogFormat = Constants.Log.tunnelLogFormat
vpnManager.isOnDemandRulesSupported = { vpnManager.isOnDemandRulesSupported = {
self.isEligibleForOnDemandRules() self.isEligibleForOnDemandRules()
} }
if let activeProfileId = appManager.activeProfileId {
profileManager.setActiveProfileId(activeProfileId)
}
profileManager.observeUpdates() profileManager.observeUpdates()
vpnManager.observeUpdates() vpnManager.observeUpdates()
@ -180,8 +179,8 @@ class AppContext {
} }
} }
extension AppManager { extension UpgradeManager {
static let shared = AppContext.shared.appManager static let shared = AppContext.shared.upgradeManager
} }
extension ProfileManager { extension ProfileManager {

View File

@ -0,0 +1,37 @@
//
// AppPreference.swift
// Passepartout
//
// Created by Davide De Rosa on 6/15/22.
// Copyright (c) 2022 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 Foundation
import PassepartoutUtils
enum AppPreference: String, KeyStoreDomainLocation {
case isShowingFavorites
case didHandleSubreddit
var domain: String {
"App"
}
}

View File

@ -37,8 +37,6 @@ extension DiagnosticsView {
} }
} }
@ObservedObject private var appManager: AppManager
@ObservedObject private var providerManager: ProviderManager @ObservedObject private var providerManager: ProviderManager
@ObservedObject private var vpnManager: VPNManager @ObservedObject private var vpnManager: VPNManager
@ -62,7 +60,6 @@ extension DiagnosticsView {
private let logUpdateInterval = Constants.Log.tunnelLogRefreshInterval private let logUpdateInterval = Constants.Log.tunnelLogRefreshInterval
init(providerName: ProviderName?) { init(providerName: ProviderName?) {
appManager = .shared
providerManager = .shared providerManager = .shared
vpnManager = .shared vpnManager = .shared
currentVPNState = .shared currentVPNState = .shared
@ -120,7 +117,7 @@ extension DiagnosticsView {
) )
} }
}.disabled(url == nil) }.disabled(url == nil)
Toggle(L10n.Diagnostics.Items.MasksPrivateData.caption, isOn: $appManager.masksPrivateData) Toggle(L10n.Diagnostics.Items.MasksPrivateData.caption, isOn: $vpnManager.masksPrivateData)
} footer: { } footer: {
Text(L10n.Diagnostics.Sections.DebugLog.footer) Text(L10n.Diagnostics.Sections.DebugLog.footer)
} }

View File

@ -142,7 +142,7 @@ extension OrganizerView {
private func performMigrationsIfNeeded() { private func performMigrationsIfNeeded() {
Task { Task {
AppManager.shared.doMigrations(profileManager) UpgradeManager.shared.doMigrations(profileManager)
} }
} }
} }

View File

@ -48,7 +48,7 @@ struct OrganizerView: View {
@State private var isHostFileImporterPresented = false @State private var isHostFileImporterPresented = false
@AppStorage(AppManager.DefaultKey.didHandleSubreddit.rawValue) var didHandleSubreddit = false @AppStorage(AppPreference.didHandleSubreddit.key) private var didHandleSubreddit = false
private let hostFileTypes = Constants.URLs.filetypes private let hostFileTypes = Constants.URLs.filetypes

View File

@ -29,8 +29,6 @@ import PassepartoutCore
struct ProviderLocationView: View, ProviderProfileAvailability { struct ProviderLocationView: View, ProviderProfileAvailability {
@ObservedObject var providerManager: ProviderManager @ObservedObject var providerManager: ProviderManager
@ObservedObject private var appManager: AppManager
@ObservedObject private var currentProfile: ObservableProfile @ObservedObject private var currentProfile: ObservableProfile
var profile: Profile { var profile: Profile {
@ -55,7 +53,7 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
@Binding private var favoriteLocationIds: Set<String>? @Binding private var favoriteLocationIds: Set<String>?
@AppStorage(AppManager.DefaultKey.isShowingFavorites.rawValue) private var isShowingFavorites = false @AppStorage(AppPreference.isShowingFavorites.key) private var isShowingFavorites = false
private var isShowingEmptyFavorites: Bool { private var isShowingEmptyFavorites: Bool {
guard isShowingFavorites else { guard isShowingFavorites else {
@ -68,7 +66,6 @@ struct ProviderLocationView: View, ProviderProfileAvailability {
init(currentProfile: ObservableProfile, isEditable: Bool, isPresented: Binding<Bool>) { init(currentProfile: ObservableProfile, isEditable: Bool, isPresented: Binding<Bool>) {
let providerManager: ProviderManager = .shared let providerManager: ProviderManager = .shared
appManager = .shared
self.providerManager = providerManager self.providerManager = providerManager
self.currentProfile = currentProfile self.currentProfile = currentProfile
self.isEditable = isEditable self.isEditable = isEditable

View File

@ -27,8 +27,6 @@ import SwiftUI
import PassepartoutCore import PassepartoutCore
struct VPNToggle: View { struct VPNToggle: View {
@ObservedObject private var appManager: AppManager
@ObservedObject private var profileManager: ProfileManager @ObservedObject private var profileManager: ProfileManager
@ObservedObject private var vpnManager: VPNManager @ObservedObject private var vpnManager: VPNManager
@ -64,7 +62,6 @@ struct VPNToggle: View {
@State private var canToggle = true @State private var canToggle = true
init(profileId: UUID, rateLimit: Int) { init(profileId: UUID, rateLimit: Int) {
appManager = .shared
profileManager = .shared profileManager = .shared
vpnManager = .shared vpnManager = .shared
currentVPNState = .shared currentVPNState = .shared
@ -87,10 +84,6 @@ struct VPNToggle: View {
} }
do { do {
let profile = try await vpnManager.connect(with: profileId) let profile = try await vpnManager.connect(with: profileId)
// IMPORTANT: save immediately to keep in sync with VPN status
appManager.activeProfileId = profileId
donateIntents(withProfile: profile) donateIntents(withProfile: profile)
} catch { } catch {
pp_log.warning("Unable to connect to profile \(profileId): \(error)") pp_log.warning("Unable to connect to profile \(profileId): \(error)")

View File

@ -1,221 +0,0 @@
//
// AppManager.swift
// Passepartout
//
// Created by Davide De Rosa on 2/8/22.
// Copyright (c) 2022 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 Foundation
import CoreData
import SwiftyBeaver
@MainActor
public class AppManager: ObservableObject {
public enum DefaultKey: String {
case activeProfileId
case launchesOnLogin
case isShowingFavorites
case confirmsQuit
case logFormat
case tunnelLogFormat
case masksPrivateData
case didHandleSubreddit
// internal use
case persistenceAuthor
case didMigrateToV2
}
private let defaults: UserDefaults = .standard
public var logLevel: SwiftyBeaver.Level = .info
public var logFile: URL?
// MARK: State
@Published public private(set) var isDoingMigrations = true
public init() {
defaults.register(keyedDefaults: [
.activeProfileId: nil,
.launchesOnLogin: false,
.isShowingFavorites: false,
.confirmsQuit: true,
.logFormat: nil,
.tunnelLogFormat: nil,
.masksPrivateData: true,
.didHandleSubreddit: false,
//
.didMigrateToV2: false
])
// set once
if persistenceAuthor == nil {
persistenceAuthor = UUID().uuidString
}
}
public func configureLogging() {
let console = ConsoleDestination()
console.minLevel = logLevel
// console.useNSLog = true
if let logFormat = logFormat {
console.format = logFormat
}
SwiftyBeaver.addDestination(console)
if let fileURL = logFile {
let file = FileDestination()
file.minLevel = logLevel
file.logFileURL = fileURL
if let logFormat = logFormat {
file.format = logFormat
}
_ = file.deleteLogFile()
SwiftyBeaver.addDestination(file)
}
CoreConfiguration.masksPrivateData = masksPrivateData
}
public func doMigrations(_ profileManager: ProfileManager) {
// profileManager.removeAllProfiles()
guard didMigrateToV2 else {
isDoingMigrations = true
let migrated = doMigrateToV2()
if !migrated.isEmpty {
pp_log.info("Migrating \(migrated.count) profiles")
migrated.forEach {
var profile = $0
if profileManager.isExistingProfile(withName: profile.header.name) {
profile = profile.renamedUniquely(withLastUpdate: true)
}
profileManager.saveProfile(profile, isActive: nil)
}
} else {
pp_log.info("Nothing to migrate!")
}
isDoingMigrations = false
didMigrateToV2 = true
return
}
isDoingMigrations = false
}
// MARK: Current state
public var preferences: AppPreferences {
return DefaultAppPreferences(
activeProfileId: activeProfileId,
logFormat: logFormat,
tunnelLogFormat: tunnelLogFormat,
masksPrivateData: masksPrivateData
)
}
}
extension AppManager: AppPreferences {
public var activeProfileId: UUID? {
get {
guard let uuidString = defaults.string(forKey: DefaultKey.activeProfileId.rawValue) else {
return nil
}
return UUID(uuidString: uuidString)
}
set {
defaults.set(newValue?.uuidString, forKey: DefaultKey.activeProfileId.rawValue)
defaults.synchronize()
objectWillChange.send()
}
}
public var logFormat: String? {
get {
defaults.string(forKey: DefaultKey.logFormat.rawValue)
}
set {
defaults.set(newValue, forKey: DefaultKey.logFormat.rawValue)
objectWillChange.send()
}
}
public var tunnelLogFormat: String? {
get {
defaults.string(forKey: DefaultKey.tunnelLogFormat.rawValue)
}
set {
defaults.set(newValue, forKey: DefaultKey.tunnelLogFormat.rawValue)
objectWillChange.send()
}
}
public var masksPrivateData: Bool {
get {
defaults.bool(forKey: DefaultKey.masksPrivateData.rawValue)
}
set {
defaults.set(newValue, forKey: DefaultKey.masksPrivateData.rawValue)
CoreConfiguration.masksPrivateData = newValue
objectWillChange.send()
}
}
// MARK: Internal use (readonly)
public private(set) var persistenceAuthor: String? {
get {
defaults.string(forKey: DefaultKey.persistenceAuthor.rawValue)
}
set {
defaults.set(newValue, forKey: DefaultKey.persistenceAuthor.rawValue)
}
}
public internal(set) var didMigrateToV2: Bool {
get {
defaults.bool(forKey: DefaultKey.didMigrateToV2.rawValue)
}
set {
defaults.set(newValue, forKey: DefaultKey.didMigrateToV2.rawValue)
}
}
}
private extension UserDefaults {
func register(keyedDefaults: [AppManager.DefaultKey: Any?]) {
let mapped = keyedDefaults.reduce(into: [String: Any]()) {
$0[$1.key.rawValue] = $1.value
}
register(defaults: mapped)
}
}

View File

@ -0,0 +1,60 @@
//
// LogManager.swift
// Passepartout
//
// Created by Davide De Rosa on 6/15/22.
// Copyright (c) 2022 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 Foundation
import SwiftyBeaver
public class LogManager {
public let logFile: URL?
public var logLevel: SwiftyBeaver.Level = .info
public var logFormat: String?
public init(logFile: URL?) {
self.logFile = logFile
}
public func configureLogging() {
let console = ConsoleDestination()
console.minLevel = logLevel
// console.useNSLog = true
if let logFormat = logFormat {
console.format = logFormat
}
SwiftyBeaver.addDestination(console)
if let fileURL = logFile {
let file = FileDestination()
file.minLevel = logLevel
file.logFileURL = fileURL
if let logFormat = logFormat {
file.format = logFormat
}
_ = file.deleteLogFile()
SwiftyBeaver.addDestination(file)
}
}
}

View File

@ -25,21 +25,50 @@
import Foundation import Foundation
import CoreData import CoreData
import PassepartoutUtils
public class PersistenceManager { public class PersistenceManager {
private let author: String? private let store: KeyValueStore
public init(author: String?) { public init(store: KeyValueStore) {
self.author = author self.store = store
// set once
if persistenceAuthor == nil {
persistenceAuthor = UUID().uuidString
}
} }
public func profilesPersistence(withName containerName: String) -> Persistence { public func profilesPersistence(withName containerName: String) -> Persistence {
let model = PassepartoutDataModels.profiles let model = PassepartoutDataModels.profiles
return Persistence(withCloudKitName: containerName, model: model, author: author) return Persistence(withCloudKitName: containerName, model: model, author: persistenceAuthor)
} }
public func providersPersistence(withName containerName: String) -> Persistence { public func providersPersistence(withName containerName: String) -> Persistence {
let model = PassepartoutDataModels.providers let model = PassepartoutDataModels.providers
return Persistence(withLocalName: containerName, model: model, author: author) return Persistence(withLocalName: containerName, model: model, author: persistenceAuthor)
}
}
// MARK: KeyValueStore
extension PersistenceManager {
public private(set) var persistenceAuthor: String? {
get {
store.value(forLocation: StoreKey.persistenceAuthor)
}
set {
store.setValue(newValue, forLocation: StoreKey.persistenceAuthor)
}
}
}
private extension PersistenceManager {
private enum StoreKey: String, KeyStoreDomainLocation {
case persistenceAuthor
var domain: String {
"PersistenceManager"
}
} }
} }

View File

@ -1,5 +1,5 @@
// //
// AppManager+Migrations.swift // UpgradeManager+Migrations.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 3/20/22. // Created by Davide De Rosa on 3/20/22.
@ -28,12 +28,34 @@ import GenericJSON
import TunnelKitCore import TunnelKitCore
import TunnelKitOpenVPNCore import TunnelKitOpenVPNCore
import TunnelKitManager import TunnelKitManager
import PassepartoutUtils
private typealias Map = [String: Any] private typealias Map = [String: Any]
// MARK: Migrate old store
extension UpgradeManager {
fileprivate enum LegacyStoreKey: String, KeyStoreLocation {
case didMigrateToV2
var key: String {
rawValue
}
}
func doMigrateStore(_ store: KeyValueStore) {
if !didMigrateToV2 {
guard let legacyDidMigrateToV2: Bool = store.value(forLocation: LegacyStoreKey.didMigrateToV2) else {
return
}
didMigrateToV2 = legacyDidMigrateToV2
}
}
}
// MARK: Migrate to version 2 // MARK: Migrate to version 2
extension AppManager { extension UpgradeManager {
fileprivate enum MigrationError: Error { fileprivate enum MigrationError: Error {
case json case json
@ -321,7 +343,7 @@ private extension URL {
func asJSON() throws -> Map { func asJSON() throws -> Map {
let data = try Data(contentsOf: self) let data = try Data(contentsOf: self)
guard let json = try JSONSerialization.jsonObject(with: data) as? Map else { guard let json = try JSONSerialization.jsonObject(with: data) as? Map else {
throw AppManager.MigrationError.json throw UpgradeManager.MigrationError.json
} }
return json return json
} }

View File

@ -0,0 +1,95 @@
//
// UpgradeManager.swift
// Passepartout
//
// Created by Davide De Rosa on 2/8/22.
// Copyright (c) 2022 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 Foundation
import CoreData
import SwiftyBeaver
import PassepartoutUtils
@MainActor
public class UpgradeManager: ObservableObject {
// MARK: Initialization
private let store: KeyValueStore
// MARK: State
@Published public private(set) var isDoingMigrations = true
public init(store: KeyValueStore) {
self.store = store
}
public func doMigrations(_ profileManager: ProfileManager) {
doMigrateStore(store)
// profileManager.removeAllProfiles()
guard didMigrateToV2 else {
isDoingMigrations = true
let migrated = doMigrateToV2()
if !migrated.isEmpty {
pp_log.info("Migrating \(migrated.count) profiles")
migrated.forEach {
var profile = $0
if profileManager.isExistingProfile(withName: profile.header.name) {
profile = profile.renamedUniquely(withLastUpdate: true)
}
profileManager.saveProfile(profile, isActive: nil)
}
} else {
pp_log.info("Nothing to migrate!")
}
isDoingMigrations = false
didMigrateToV2 = true
return
}
isDoingMigrations = false
}
}
// MARK: KeyValueStore
extension UpgradeManager {
public internal(set) var didMigrateToV2: Bool {
get {
store.value(forLocation: StoreKey.didMigrateToV2) ?? false
}
set {
store.setValue(newValue, forLocation: StoreKey.didMigrateToV2)
}
}
}
private extension UpgradeManager {
private enum StoreKey: String, KeyStoreDomainLocation {
case didMigrateToV2
var domain: String {
"UpgradeManager"
}
}
}

View File

@ -26,6 +26,13 @@
import Foundation import Foundation
extension VPNManager { extension VPNManager {
private var vpnPreferences: VPNPreferences {
DefaultVPNPreferences(
tunnelLogFormat: tunnelLogFormat,
masksPrivateData: masksPrivateData
)
}
func vpnConfigurationWithCurrentProfile() -> VPNConfiguration? { func vpnConfigurationWithCurrentProfile() -> VPNConfiguration? {
do { do {
guard profileManager.isCurrentProfileActive() else { guard profileManager.isCurrentProfileActive() else {
@ -52,7 +59,7 @@ extension VPNManager {
let parameters = VPNConfigurationParameters( let parameters = VPNConfigurationParameters(
profile, profile,
appGroup: profileManager.appGroup, appGroup: profileManager.appGroup,
preferences: appManager.preferences, preferences: vpnPreferences,
passwordReference: profileManager.passwordReference(forProfile: profile), passwordReference: profileManager.passwordReference(forProfile: profile),
withNetworkSettings: isNetworkSettingsSupported(), withNetworkSettings: isNetworkSettingsSupported(),
withCustomRules: isOnDemandRulesSupported() withCustomRules: isOnDemandRulesSupported()

View File

@ -25,8 +25,9 @@
import Foundation import Foundation
import Combine import Combine
import TunnelKitCore
import TunnelKitManager import TunnelKitManager
import TunnelKitOpenVPNManager import PassepartoutUtils
@MainActor @MainActor
public class VPNManager: ObservableObject { public class VPNManager: ObservableObject {
@ -34,7 +35,7 @@ public class VPNManager: ObservableObject {
// MARK: Initialization // MARK: Initialization
let appManager: AppManager private let store: KeyValueStore
let profileManager: ProfileManager let profileManager: ProfileManager
@ -68,12 +69,12 @@ public class VPNManager: ObservableObject {
private var cancellables: Set<AnyCancellable> = [] private var cancellables: Set<AnyCancellable> = []
public init( public init(
appManager: AppManager, store: KeyValueStore,
profileManager: ProfileManager, profileManager: ProfileManager,
providerManager: ProviderManager, providerManager: ProviderManager,
strategy: VPNManagerStrategy strategy: VPNManagerStrategy
) { ) {
self.appManager = appManager self.store = store
self.profileManager = profileManager self.profileManager = profileManager
self.providerManager = providerManager self.providerManager = providerManager
self.strategy = strategy self.strategy = strategy
@ -81,6 +82,8 @@ public class VPNManager: ObservableObject {
isOnDemandRulesSupported = { true } isOnDemandRulesSupported = { true }
currentState = ObservableState() currentState = ObservableState()
CoreConfiguration.masksPrivateData = masksPrivateData
} }
public func toggle() -> Bool { public func toggle() -> Bool {
@ -152,7 +155,7 @@ extension VPNManager {
} }
private func observeProfileManager() { private func observeProfileManager() {
profileManager.$activeProfileId profileManager.activeProfileIdPublisher
.dropFirst() .dropFirst()
.removeDuplicates() .removeDuplicates()
.sink { newId in .sink { newId in
@ -252,3 +255,39 @@ extension VPNManager {
} }
} }
} }
// MARK: KeyValueStore
extension VPNManager {
public var tunnelLogFormat: String? {
get {
store.value(forLocation: StoreKey.tunnelLogFormat)
}
set {
store.setValue(newValue, forLocation: StoreKey.tunnelLogFormat)
}
}
public var masksPrivateData: Bool {
get {
store.value(forLocation: StoreKey.masksPrivateData) ?? true
}
set {
store.setValue(newValue, forLocation: StoreKey.masksPrivateData)
CoreConfiguration.masksPrivateData = masksPrivateData
}
}
}
private extension VPNManager {
private enum StoreKey: String, KeyStoreDomainLocation {
case tunnelLogFormat
case masksPrivateData
var domain: String {
"VPNManager"
}
}
}

View File

@ -39,7 +39,7 @@ struct VPNConfigurationParameters {
let appGroup: String let appGroup: String
let preferences: AppPreferences let preferences: VPNPreferences
let networkSettings: Profile.NetworkSettings let networkSettings: Profile.NetworkSettings
@ -54,7 +54,7 @@ struct VPNConfigurationParameters {
init( init(
_ profile: Profile, _ profile: Profile,
appGroup: String, appGroup: String,
preferences: AppPreferences, preferences: VPNPreferences,
passwordReference: Data?, passwordReference: Data?,
withNetworkSettings: Bool, withNetworkSettings: Bool,
withCustomRules: Bool withCustomRules: Bool

View File

@ -1,8 +1,8 @@
// //
// AppPreferences.swift // VPNPreferences.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 2/10/22. // Created by Davide De Rosa on 6/6/22.
// Copyright (c) 2022 Davide De Rosa. All rights reserved. // Copyright (c) 2022 Davide De Rosa. All rights reserved.
// //
// https://github.com/passepartoutvpn // https://github.com/passepartoutvpn
@ -25,23 +25,14 @@
import Foundation import Foundation
@MainActor public protocol VPNPreferences {
public protocol AppPreferences {
var activeProfileId: UUID? { get }
var logFormat: String? { get }
var tunnelLogFormat: String? { get } var tunnelLogFormat: String? { get }
var masksPrivateData: Bool { get } var masksPrivateData: Bool { get }
} }
public struct DefaultAppPreferences: AppPreferences { struct DefaultVPNPreferences: VPNPreferences {
public let activeProfileId: UUID? let tunnelLogFormat: String?
public let logFormat: String? let masksPrivateData: Bool
public let tunnelLogFormat: String?
public let masksPrivateData: Bool
} }

View File

@ -35,6 +35,8 @@ public class ProfileManager: ObservableObject {
// MARK: Initialization // MARK: Initialization
private let store: KeyValueStore
private let providerManager: ProviderManager private let providerManager: ProviderManager
public let appGroup: String public let appGroup: String
@ -47,7 +49,7 @@ public class ProfileManager: ObservableObject {
// MARK: Observables // MARK: Observables
@Published public private(set) var activeProfileId: UUID? { @Published private var internalActiveProfileId: UUID? {
willSet { willSet {
pp_log.debug("Setting active profile: \(newValue?.uuidString ?? "nil")") pp_log.debug("Setting active profile: \(newValue?.uuidString ?? "nil")")
} }
@ -59,6 +61,10 @@ public class ProfileManager: ObservableObject {
} }
} }
public var activeProfileIdPublisher: Published<UUID?>.Publisher {
$internalActiveProfileId
}
public var currentProfileId: UUID? { public var currentProfileId: UUID? {
get { get {
internalCurrentProfileId internalCurrentProfileId
@ -83,6 +89,7 @@ public class ProfileManager: ObservableObject {
private var cancellables: Set<AnyCancellable> = [] private var cancellables: Set<AnyCancellable> = []
public init( public init(
store: KeyValueStore,
providerManager: ProviderManager, providerManager: ProviderManager,
appGroup: String, appGroup: String,
keychainLabel: @escaping (String, VPNProtocolType) -> String, keychainLabel: @escaping (String, VPNProtocolType) -> String,
@ -91,6 +98,7 @@ public class ProfileManager: ObservableObject {
guard let _ = UserDefaults(suiteName: appGroup) else { guard let _ = UserDefaults(suiteName: appGroup) else {
fatalError("No entitlements for group '\(appGroup)'") fatalError("No entitlements for group '\(appGroup)'")
} }
self.store = store
self.providerManager = providerManager self.providerManager = providerManager
self.appGroup = appGroup self.appGroup = appGroup
self.keychainLabel = keychainLabel self.keychainLabel = keychainLabel
@ -99,14 +107,6 @@ public class ProfileManager: ObservableObject {
currentProfile = ObservableProfile() currentProfile = ObservableProfile()
} }
public func setActiveProfileId(_ id: UUID) {
guard isExistingProfile(withId: id) else {
pp_log.warning("Active profile \(id) does not exist, ignoring")
return
}
activeProfileId = id
}
} }
// MARK: Index // MARK: Index
@ -450,3 +450,41 @@ extension ProfileManager {
} }
} }
} }
// MARK: KeyValueStore
extension ProfileManager {
public private(set) var activeProfileId: UUID? {
get {
guard let idString: String = store.value(forLocation: StoreKey.activeProfileId) else {
return nil
}
guard let id = UUID(uuidString: idString) else {
pp_log.warning("Active profile id is malformed, ignoring")
return nil
}
guard isExistingProfile(withId: id) else {
pp_log.warning("Active profile \(id) does not exist, ignoring")
return nil
}
return id
}
set {
// trigger publisher
internalActiveProfileId = newValue
store.setValue(newValue?.uuidString, forLocation: StoreKey.activeProfileId)
}
}
}
private extension ProfileManager {
private enum StoreKey: String, KeyStoreDomainLocation {
case activeProfileId
var domain: String {
"ProfileManager"
}
}
}

View File

@ -0,0 +1,46 @@
//
// KeyValueStore.swift
// Passepartout
//
// Created by Davide De Rosa on 6/15/22.
// Copyright (c) 2022 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 Foundation
public protocol KeyStoreLocation: RawRepresentable where RawValue == String {
var key: String { get }
}
public protocol KeyStoreDomainLocation: KeyStoreLocation {
var domain: String { get }
}
extension KeyStoreDomainLocation {
public var key: String {
"\(domain).\(rawValue)"
}
}
public protocol KeyValueStore {
func setValue<L: KeyStoreLocation, V>(_ value: V, forLocation location: L)
func value<L: KeyStoreLocation, V>(forLocation location: L) -> V?
}

View File

@ -0,0 +1,42 @@
//
// UserDefaultsStore.swift
// Passepartout
//
// Created by Davide De Rosa on 6/15/22.
// Copyright (c) 2022 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 Foundation
public struct UserDefaultsStore: KeyValueStore {
private let defaults: UserDefaults
public init(defaults: UserDefaults) {
self.defaults = defaults
}
public func setValue<L: KeyStoreLocation, V>(_ value: V, forLocation location: L) {
defaults.setValue(value, forKey: location.key)
}
public func value<L: KeyStoreLocation, V>(forLocation location: L) -> V? {
defaults.value(forKey: location.key) as? V
}
}