Address UI race conditions (#229)
* Make some managers concurrency-safe - IntentsManager: @MainActor, non-shared, continuation - SSIDReader: @MainActor, continuation - Reviewer: main queue, non-shared * Review wrong use of Concurrency framework There were background thread calls e.g. in VPNToggle, because ProfileManager was used inside a VPNManager async call. Annotate @MainActor wherever a Task involves UI. * Make main managers MainActor * Apply MainActor to Mac menus * [ci skip] Update CHANGELOG * Set MainActor consistently on Mac menu view models
This commit is contained in:
parent
54dc8a2556
commit
5627e6c4a9
|
@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Oeck provider is available again to free users.
|
- Oeck provider is available again to free users.
|
||||||
- Randomic crashes on profile updates.
|
- Randomic crashes on profile updates. [#229](https://github.com/passepartoutvpn/passepartout-apple/pull/229)
|
||||||
|
|
||||||
## 2.0.0 (2022-10-02)
|
## 2.0.0 (2022-10-02)
|
||||||
|
|
||||||
|
|
|
@ -32,11 +32,3 @@ extension AppContext {
|
||||||
extension ProductManager {
|
extension ProductManager {
|
||||||
static let shared = AppContext.shared.productManager
|
static let shared = AppContext.shared.productManager
|
||||||
}
|
}
|
||||||
|
|
||||||
extension IntentsManager {
|
|
||||||
static let shared = AppContext.shared.intentsManager
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Reviewer {
|
|
||||||
static let shared = AppContext.shared.reviewer
|
|
||||||
}
|
|
||||||
|
|
|
@ -27,15 +27,14 @@ import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import PassepartoutLibrary
|
import PassepartoutLibrary
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class AppContext {
|
class AppContext {
|
||||||
private let logManager: LogManager
|
private let logManager: LogManager
|
||||||
|
|
||||||
|
private let reviewer: Reviewer
|
||||||
|
|
||||||
let productManager: ProductManager
|
let productManager: ProductManager
|
||||||
|
|
||||||
let intentsManager: IntentsManager
|
|
||||||
|
|
||||||
let reviewer: Reviewer
|
|
||||||
|
|
||||||
private var cancellables: Set<AnyCancellable> = []
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
init(coreContext: CoreContext) {
|
init(coreContext: CoreContext) {
|
||||||
|
@ -45,13 +44,13 @@ class AppContext {
|
||||||
logManager.configureLogging()
|
logManager.configureLogging()
|
||||||
pp_log.info("Logging to: \(logManager.logFile!)")
|
pp_log.info("Logging to: \(logManager.logFile!)")
|
||||||
|
|
||||||
|
reviewer = Reviewer()
|
||||||
|
reviewer.eventCountBeforeRating = Constants.Rating.eventCount
|
||||||
|
|
||||||
productManager = ProductManager(
|
productManager = ProductManager(
|
||||||
appType: Constants.InApp.appType,
|
appType: Constants.InApp.appType,
|
||||||
buildProducts: Constants.InApp.buildProducts
|
buildProducts: Constants.InApp.buildProducts
|
||||||
)
|
)
|
||||||
intentsManager = IntentsManager()
|
|
||||||
reviewer = Reviewer()
|
|
||||||
reviewer.eventCountBeforeRating = Constants.Rating.eventCount
|
|
||||||
|
|
||||||
// post
|
// post
|
||||||
|
|
||||||
|
@ -59,18 +58,19 @@ class AppContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureObjects(coreContext: CoreContext) {
|
private func configureObjects(coreContext: CoreContext) {
|
||||||
|
coreContext.vpnManager.isOnDemandRulesSupported = {
|
||||||
|
self.isEligibleForOnDemandRules()
|
||||||
|
}
|
||||||
|
|
||||||
coreContext.vpnManager.currentState.$vpnStatus
|
coreContext.vpnManager.currentState.$vpnStatus
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.sink {
|
.sink {
|
||||||
if $0 == .connected {
|
if $0 == .connected {
|
||||||
pp_log.info("VPN successful connection, report to Reviewer")
|
pp_log.info("VPN successful connection, report to Reviewer")
|
||||||
self.reviewer.reportEvent()
|
self.reviewer.reportEvent()
|
||||||
}
|
}
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
coreContext.vpnManager.isOnDemandRulesSupported = {
|
|
||||||
self.isEligibleForOnDemandRules()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eligibility: ignore network settings if ineligible
|
// eligibility: ignore network settings if ineligible
|
||||||
|
|
|
@ -28,6 +28,7 @@ import Intents
|
||||||
import IntentsUI
|
import IntentsUI
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class IntentsManager: NSObject, ObservableObject {
|
class IntentsManager: NSObject, ObservableObject {
|
||||||
@Published private(set) var isReloadingShortcuts = false
|
@Published private(set) var isReloadingShortcuts = false
|
||||||
|
|
||||||
|
@ -35,28 +36,26 @@ class IntentsManager: NSObject, ObservableObject {
|
||||||
|
|
||||||
let shouldDismissIntentView = PassthroughSubject<Void, Never>()
|
let shouldDismissIntentView = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
|
private var continuation: CheckedContinuation<[INVoiceShortcut], Never>?
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
reloadShortcuts()
|
Task {
|
||||||
|
await reloadShortcuts()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadShortcuts() {
|
func reloadShortcuts() async {
|
||||||
isReloadingShortcuts = true
|
isReloadingShortcuts = true
|
||||||
INVoiceShortcutCenter.shared.getAllVoiceShortcuts { vs, error in
|
do {
|
||||||
if let error = error {
|
let vs = try await INVoiceShortcutCenter.shared.allVoiceShortcuts()
|
||||||
assertionFailure("Unable to fetch existing shortcuts: \(error)")
|
shortcuts = vs.reduce(into: [UUID: Shortcut]()) {
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isReloadingShortcuts = false
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let shortcuts = (vs ?? []).reduce(into: [UUID: Shortcut]()) {
|
|
||||||
$0[$1.identifier] = Shortcut($1)
|
$0[$1.identifier] = Shortcut($1)
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
isReloadingShortcuts = false
|
||||||
self.shortcuts = shortcuts
|
} catch {
|
||||||
self.isReloadingShortcuts = false
|
assertionFailure("Unable to fetch existing shortcuts: \(error)")
|
||||||
}
|
isReloadingShortcuts = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,9 +92,7 @@ extension IntentsManager: INUIEditVoiceShortcutViewControllerDelegate {
|
||||||
// so damn it, reload manually after a delay
|
// so damn it, reload manually after a delay
|
||||||
Task {
|
Task {
|
||||||
await Task.maybeWait(forMilliseconds: Constants.Delays.xxxReloadEditedShortcut)
|
await Task.maybeWait(forMilliseconds: Constants.Delays.xxxReloadEditedShortcut)
|
||||||
await MainActor.run {
|
await reloadShortcuts()
|
||||||
reloadShortcuts()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ class MacBundle {
|
||||||
|
|
||||||
private lazy var bridgeDelegate = MacBundleDelegate(bundle: self)
|
private lazy var bridgeDelegate = MacBundleDelegate(bundle: self)
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func configure() {
|
func configure() {
|
||||||
guard let bundleURL = Bundle.main.builtInPlugInsURL?.appendingPathComponent(Constants.Plugins.macBridgeName) else {
|
guard let bundleURL = Bundle.main.builtInPlugInsURL?.appendingPathComponent(Constants.Plugins.macBridgeName) else {
|
||||||
fatalError("Unable to find Mac bundle in plugins")
|
fatalError("Unable to find Mac bundle in plugins")
|
||||||
|
|
|
@ -28,14 +28,17 @@ import Foundation
|
||||||
class MacBundleDelegate: MacMenuDelegate {
|
class MacBundleDelegate: MacMenuDelegate {
|
||||||
private weak var bundle: MacBundle?
|
private weak var bundle: MacBundle?
|
||||||
|
|
||||||
|
@MainActor
|
||||||
var profileManager: LightProfileManager {
|
var profileManager: LightProfileManager {
|
||||||
DefaultLightProfileManager()
|
DefaultLightProfileManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
var providerManager: LightProviderManager {
|
var providerManager: LightProviderManager {
|
||||||
DefaultLightProviderManager()
|
DefaultLightProviderManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
var vpnManager: LightVPNManager {
|
var vpnManager: LightVPNManager {
|
||||||
DefaultLightVPNManager()
|
DefaultLightVPNManager()
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,6 @@ class DefaultLightProviderManager: LightProviderManager {
|
||||||
.map(DefaultLightProviderCategory.init)
|
.map(DefaultLightProviderCategory.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func downloadIfNeeded(_ name: String, vpnProtocol: String) {
|
func downloadIfNeeded(_ name: String, vpnProtocol: String) {
|
||||||
guard let vpnProtocolType = VPNProtocolType(rawValue: vpnProtocol) else {
|
guard let vpnProtocolType = VPNProtocolType(rawValue: vpnProtocol) else {
|
||||||
fatalError("Unrecognized VPN protocol: \(vpnProtocol)")
|
fatalError("Unrecognized VPN protocol: \(vpnProtocol)")
|
||||||
|
|
|
@ -64,28 +64,24 @@ class DefaultLightVPNManager: LightVPNManager {
|
||||||
}.store(in: &subscriptions)
|
}.store(in: &subscriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func connect(with profileId: UUID) {
|
func connect(with profileId: UUID) {
|
||||||
Task {
|
Task {
|
||||||
try? await vpnManager.connect(with: profileId)
|
try? await vpnManager.connect(with: profileId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func connect(with profileId: UUID, to serverId: String) {
|
func connect(with profileId: UUID, to serverId: String) {
|
||||||
Task {
|
Task {
|
||||||
try? await vpnManager.connect(with: profileId, toServer: serverId)
|
try? await vpnManager.connect(with: profileId, toServer: serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func disconnect() {
|
func disconnect() {
|
||||||
Task {
|
Task {
|
||||||
await vpnManager.disable()
|
await vpnManager.disable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func toggle() {
|
func toggle() {
|
||||||
Task {
|
Task {
|
||||||
if !isEnabled {
|
if !isEnabled {
|
||||||
|
@ -96,7 +92,6 @@ class DefaultLightVPNManager: LightVPNManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func reconnect() {
|
func reconnect() {
|
||||||
Task {
|
Task {
|
||||||
await vpnManager.reconnect()
|
await vpnManager.reconnect()
|
||||||
|
|
|
@ -159,7 +159,7 @@ extension GenericCreditsView {
|
||||||
guard content == nil else {
|
guard content == nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Task {
|
Task { @MainActor in
|
||||||
withAnimation {
|
withAnimation {
|
||||||
do {
|
do {
|
||||||
content = try String(contentsOf: url)
|
content = try String(contentsOf: url)
|
||||||
|
|
|
@ -52,6 +52,7 @@ extension AddHostView {
|
||||||
profileName = url.normalizedFilename
|
profileName = url.normalizedFilename
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
mutating func processURL(
|
mutating func processURL(
|
||||||
_ url: URL,
|
_ url: URL,
|
||||||
with profileManager: ProfileManager,
|
with profileManager: ProfileManager,
|
||||||
|
@ -96,6 +97,7 @@ extension AddHostView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
mutating func addProcessedProfile(to profileManager: ProfileManager) -> Bool {
|
mutating func addProcessedProfile(to profileManager: ProfileManager) -> Bool {
|
||||||
guard !processedProfile.isPlaceholder else {
|
guard !processedProfile.isPlaceholder else {
|
||||||
assertionFailure("Saving profile without processing first?")
|
assertionFailure("Saving profile without processing first?")
|
||||||
|
|
|
@ -58,9 +58,7 @@ struct AddProfileMenu: View {
|
||||||
} label: {
|
} label: {
|
||||||
Label(L10n.Global.Strings.provider, systemImage: themeProviderImage)
|
Label(L10n.Global.Strings.provider, systemImage: themeProviderImage)
|
||||||
}
|
}
|
||||||
Button {
|
Button(action: presentHostFileImporter) {
|
||||||
presentHostFileImporter()
|
|
||||||
} label: {
|
|
||||||
Label(L10n.Menu.Contextual.AddProfile.fromFiles, systemImage: themeHostFilesImage)
|
Label(L10n.Menu.Contextual.AddProfile.fromFiles, systemImage: themeHostFilesImage)
|
||||||
}
|
}
|
||||||
// Button {
|
// Button {
|
||||||
|
@ -146,7 +144,7 @@ extension AddProfileMenu {
|
||||||
//
|
//
|
||||||
// https://stackoverflow.com/questions/66965471/swiftui-fileimporter-modifier-not-updating-binding-when-dismissed-by-tapping
|
// https://stackoverflow.com/questions/66965471/swiftui-fileimporter-modifier-not-updating-binding-when-dismissed-by-tapping
|
||||||
isHostFileImporterPresented = false
|
isHostFileImporterPresented = false
|
||||||
Task {
|
Task { @MainActor in
|
||||||
await Task.maybeWait(forMilliseconds: Constants.Delays.xxxPresentFileImporter)
|
await Task.maybeWait(forMilliseconds: Constants.Delays.xxxPresentFileImporter)
|
||||||
isHostFileImporterPresented = true
|
isHostFileImporterPresented = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,6 @@ import Foundation
|
||||||
import PassepartoutLibrary
|
import PassepartoutLibrary
|
||||||
|
|
||||||
extension AddProviderView {
|
extension AddProviderView {
|
||||||
|
|
||||||
@MainActor
|
|
||||||
class ViewModel: ObservableObject {
|
class ViewModel: ObservableObject {
|
||||||
enum PendingOperation {
|
enum PendingOperation {
|
||||||
case index
|
case index
|
||||||
|
@ -75,18 +73,16 @@ extension AddProviderView {
|
||||||
metadata.name,
|
metadata.name,
|
||||||
vpnProtocol: selectedVPNProtocol
|
vpnProtocol: selectedVPNProtocol
|
||||||
) else {
|
) else {
|
||||||
Task {
|
selectProviderAfterFetchingInfrastructure(metadata, providerManager)
|
||||||
await selectProviderAfterFetchingInfrastructure(metadata, providerManager)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
doSelectProvider(metadata, server)
|
doSelectProvider(metadata, server)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func selectProviderAfterFetchingInfrastructure(_ metadata: ProviderMetadata, _ providerManager: ProviderManager) async {
|
private func selectProviderAfterFetchingInfrastructure(_ metadata: ProviderMetadata, _ providerManager: ProviderManager) {
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
pendingOperation = .provider(metadata.name)
|
pendingOperation = .provider(metadata.name)
|
||||||
Task {
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
try await providerManager.fetchProviderPublisher(
|
try await providerManager.fetchProviderPublisher(
|
||||||
withName: metadata.name,
|
withName: metadata.name,
|
||||||
|
@ -117,7 +113,7 @@ extension AddProviderView {
|
||||||
func updateIndex(_ providerManager: ProviderManager) {
|
func updateIndex(_ providerManager: ProviderManager) {
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
pendingOperation = .index
|
pendingOperation = .index
|
||||||
Task {
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
try await providerManager.fetchProvidersIndexPublisher(
|
try await providerManager.fetchProvidersIndexPublisher(
|
||||||
priority: .remoteThenBundle
|
priority: .remoteThenBundle
|
||||||
|
@ -153,6 +149,7 @@ extension AddProviderView.NameView {
|
||||||
profileName = metadata.fullName
|
profileName = metadata.fullName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
mutating func addProfile(
|
mutating func addProfile(
|
||||||
_ profile: Profile,
|
_ profile: Profile,
|
||||||
to profileManager: ProfileManager,
|
to profileManager: ProfileManager,
|
||||||
|
|
|
@ -34,11 +34,7 @@ extension OnDemandView {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
EditableTextList(elements: allSSIDs, allowsDuplicates: false, mapping: mapElements) { text in
|
EditableTextList(elements: allSSIDs, allowsDuplicates: false, mapping: mapElements) { text in
|
||||||
reader.requestCurrentSSID {
|
requestSSID(text)
|
||||||
if !withSSIDs.keys.contains($0) {
|
|
||||||
text.wrappedValue = $0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} textField: {
|
} textField: {
|
||||||
ssidRow(callback: $0)
|
ssidRow(callback: $0)
|
||||||
} addLabel: {
|
} addLabel: {
|
||||||
|
@ -74,6 +70,15 @@ extension OnDemandView {
|
||||||
onCommit: callback.onCommit
|
onCommit: callback.onCommit
|
||||||
).themeValidSSID(callback.text.wrappedValue)
|
).themeValidSSID(callback.text.wrappedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func requestSSID(_ text: Binding<String>) {
|
||||||
|
Task { @MainActor in
|
||||||
|
let ssid = try await reader.requestCurrentSSID()
|
||||||
|
if !withSSIDs.keys.contains(ssid) {
|
||||||
|
text.wrappedValue = ssid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,9 +41,8 @@ extension OrganizerView {
|
||||||
if !profileManager.hasProfiles {
|
if !profileManager.hasProfiles {
|
||||||
emptyView
|
emptyView
|
||||||
}
|
}
|
||||||
}.onAppear {
|
}.onAppear(perform: performMigrationsIfNeeded)
|
||||||
performMigrationsIfNeeded()
|
.onReceive(profileManager.didCreateProfile) {
|
||||||
}.onReceive(profileManager.didCreateProfile) {
|
|
||||||
profileManager.currentProfileId = $0.id
|
profileManager.currentProfileId = $0.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,7 +140,7 @@ extension OrganizerView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func performMigrationsIfNeeded() {
|
private func performMigrationsIfNeeded() {
|
||||||
Task {
|
Task { @MainActor in
|
||||||
UpgradeManager.shared.doMigrations(profileManager)
|
UpgradeManager.shared.doMigrations(profileManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,7 +87,7 @@ extension OrganizerView {
|
||||||
switch phase {
|
switch phase {
|
||||||
case .active:
|
case .active:
|
||||||
if productManager.hasRefunded() {
|
if productManager.hasRefunded() {
|
||||||
Task {
|
Task { @MainActor in
|
||||||
await vpnManager.uninstall()
|
await vpnManager.uninstall()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ extension OrganizerView {
|
||||||
assertionFailure("Empty URLs from file importer?")
|
assertionFailure("Empty URLs from file importer?")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Task {
|
Task { @MainActor in
|
||||||
await Task.maybeWait(forMilliseconds: Constants.Delays.xxxPresentFileImporter)
|
await Task.maybeWait(forMilliseconds: Constants.Delays.xxxPresentFileImporter)
|
||||||
addProfileModalType = .addHost(url, false)
|
addProfileModalType = .addHost(url, false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,7 +138,7 @@ extension ProfileView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func uninstallVPN() {
|
private func uninstallVPN() {
|
||||||
Task {
|
Task { @MainActor in
|
||||||
await vpnManager.uninstall()
|
await vpnManager.uninstall()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,7 @@ extension ProfileView {
|
||||||
|
|
||||||
private func refreshInfrastructure() {
|
private func refreshInfrastructure() {
|
||||||
isRefreshingInfrastructure = true
|
isRefreshingInfrastructure = true
|
||||||
Task {
|
Task { @MainActor in
|
||||||
try await providerManager.fetchRemoteProviderPublisher(forProfile: profile).async()
|
try await providerManager.fetchRemoteProviderPublisher(forProfile: profile).async()
|
||||||
isRefreshingInfrastructure = false
|
isRefreshingInfrastructure = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ struct ShortcutsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObservedObject private var intentsManager: IntentsManager
|
@StateObject private var intentsManager = IntentsManager()
|
||||||
|
|
||||||
@Environment(\.presentationMode) private var presentationMode
|
@Environment(\.presentationMode) private var presentationMode
|
||||||
|
|
||||||
|
@ -56,7 +56,6 @@ struct ShortcutsView: View {
|
||||||
@State private var pendingShortcut: INShortcut?
|
@State private var pendingShortcut: INShortcut?
|
||||||
|
|
||||||
init(target: Profile) {
|
init(target: Profile) {
|
||||||
intentsManager = .shared
|
|
||||||
self.target = target
|
self.target = target
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,11 +68,7 @@ struct ShortcutsView: View {
|
||||||
}.toolbar {
|
}.toolbar {
|
||||||
themeCloseItem(presentationMode: presentationMode)
|
themeCloseItem(presentationMode: presentationMode)
|
||||||
}.sheet(item: $modalType, content: presentedModal)
|
}.sheet(item: $modalType, content: presentedModal)
|
||||||
|
.themeAnimation(on: intentsManager.isReloadingShortcuts)
|
||||||
// reloading
|
|
||||||
.onAppear {
|
|
||||||
intentsManager.reloadShortcuts()
|
|
||||||
}.themeAnimation(on: intentsManager.isReloadingShortcuts)
|
|
||||||
|
|
||||||
// IntentsUI
|
// IntentsUI
|
||||||
.onReceive(intentsManager.shouldDismissIntentView) { _ in
|
.onReceive(intentsManager.shouldDismissIntentView) { _ in
|
||||||
|
|
|
@ -77,11 +77,10 @@ struct VPNToggle: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func enableVPN() {
|
private func enableVPN() {
|
||||||
Task {
|
Task { @MainActor in
|
||||||
canToggle = false
|
canToggle = false
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(rateLimit)) {
|
await Task.maybeWait(forMilliseconds: rateLimit)
|
||||||
canToggle = true
|
canToggle = true
|
||||||
}
|
|
||||||
do {
|
do {
|
||||||
let profile = try await vpnManager.connect(with: profileId)
|
let profile = try await vpnManager.connect(with: profileId)
|
||||||
donateIntents(withProfile: profile)
|
donateIntents(withProfile: profile)
|
||||||
|
@ -93,7 +92,7 @@ struct VPNToggle: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func disableVPN() {
|
private func disableVPN() {
|
||||||
Task {
|
Task { @MainActor in
|
||||||
canToggle = false
|
canToggle = false
|
||||||
await vpnManager.disable()
|
await vpnManager.disable()
|
||||||
canToggle = true
|
canToggle = true
|
||||||
|
|
|
@ -27,8 +27,6 @@ import Foundation
|
||||||
import PassepartoutLibrary
|
import PassepartoutLibrary
|
||||||
|
|
||||||
extension CoreContext {
|
extension CoreContext {
|
||||||
|
|
||||||
@MainActor
|
|
||||||
static let shared = CoreContext(store: UserDefaultsStore(defaults: .standard))
|
static let shared = CoreContext(store: UserDefaultsStore(defaults: .standard))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,5 +47,7 @@ extension VPNManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ObservableVPNState {
|
extension ObservableVPNState {
|
||||||
|
|
||||||
|
@MainActor
|
||||||
static let shared = CoreContext.shared.vpnManager.currentState
|
static let shared = CoreContext.shared.vpnManager.currentState
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import PassepartoutLibrary
|
import PassepartoutLibrary
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class CoreContext {
|
class CoreContext {
|
||||||
let store: KeyValueStore
|
let store: KeyValueStore
|
||||||
|
|
||||||
|
@ -52,7 +53,6 @@ class CoreContext {
|
||||||
|
|
||||||
private var cancellables: Set<AnyCancellable> = []
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
@MainActor
|
|
||||||
init(store: KeyValueStore) {
|
init(store: KeyValueStore) {
|
||||||
self.store = store
|
self.store = store
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
@objc
|
@objc
|
||||||
public protocol MacMenu {
|
public protocol MacMenu {
|
||||||
var delegate: MacMenuDelegate? { get set }
|
var delegate: MacMenuDelegate? { get set }
|
||||||
|
|
|
@ -46,6 +46,7 @@ extension LightProfile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
@objc
|
@objc
|
||||||
public protocol LightProfileManager {
|
public protocol LightProfileManager {
|
||||||
var hasProfiles: Bool { get }
|
var hasProfiles: Bool { get }
|
||||||
|
|
|
@ -56,6 +56,7 @@ public protocol LightProviderServer {
|
||||||
var serverId: String { get }
|
var serverId: String { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
@objc
|
@objc
|
||||||
public protocol LightProviderManager {
|
public protocol LightProviderManager {
|
||||||
var delegate: LightProviderManagerDelegate? { get set }
|
var delegate: LightProviderManagerDelegate? { get set }
|
||||||
|
|
|
@ -36,6 +36,7 @@ public enum LightVPNStatus: Int {
|
||||||
case disconnected
|
case disconnected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
@objc
|
@objc
|
||||||
public protocol LightVPNManager {
|
public protocol LightVPNManager {
|
||||||
var isEnabled: Bool { get }
|
var isEnabled: Bool { get }
|
||||||
|
|
|
@ -26,6 +26,8 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension HostProfileItem {
|
extension HostProfileItem {
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class ViewModel {
|
class ViewModel {
|
||||||
let profile: LightProfile
|
let profile: LightProfile
|
||||||
|
|
||||||
|
@ -41,8 +43,10 @@ extension HostProfileItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
Task { @MainActor in
|
||||||
vpnManager.removeDelegate(withIdentifier: profile.id.uuidString)
|
vpnManager.removeDelegate(withIdentifier: profile.id.uuidString)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc func connectTo() {
|
@objc func connectTo() {
|
||||||
vpnManager.connect(with: profile.id)
|
vpnManager.connect(with: profile.id)
|
||||||
|
|
|
@ -28,6 +28,8 @@ import Combine
|
||||||
import ServiceManagement
|
import ServiceManagement
|
||||||
|
|
||||||
extension LaunchOnLoginItem {
|
extension LaunchOnLoginItem {
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class ViewModel: ObservableObject {
|
class ViewModel: ObservableObject {
|
||||||
let title: String
|
let title: String
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,8 @@ import Foundation
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
extension PassepartoutMenu {
|
extension PassepartoutMenu {
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class StatusButton {
|
class StatusButton {
|
||||||
private lazy var statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
private lazy var statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||||
|
|
||||||
|
@ -50,8 +52,10 @@ extension PassepartoutMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
Task { @MainActor in
|
||||||
vpnManager.removeDelegate(withIdentifier: "PassepartoutMenu")
|
vpnManager.removeDelegate(withIdentifier: "PassepartoutMenu")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func install(systemMenu: SystemMenu) {
|
func install(systemMenu: SystemMenu) {
|
||||||
statusItem.menu = systemMenu.asMenu
|
statusItem.menu = systemMenu.asMenu
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class PassepartoutMenu {
|
class PassepartoutMenu {
|
||||||
private let macMenuDelegate: MacMenuDelegate
|
private let macMenuDelegate: MacMenuDelegate
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,8 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension ProviderLocationItem {
|
extension ProviderLocationItem {
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class ViewModel {
|
class ViewModel {
|
||||||
private let profile: LightProfile
|
private let profile: LightProfile
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,8 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension ProviderProfileItem {
|
extension ProviderProfileItem {
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class ViewModel {
|
class ViewModel {
|
||||||
let profile: LightProfile
|
let profile: LightProfile
|
||||||
|
|
||||||
|
@ -44,8 +46,10 @@ extension ProviderProfileItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
Task { @MainActor in
|
||||||
vpnManager.removeDelegate(withIdentifier: profile.id.uuidString)
|
vpnManager.removeDelegate(withIdentifier: profile.id.uuidString)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var providerName: String {
|
private var providerName: String {
|
||||||
guard let providerName = profile.providerName else {
|
guard let providerName = profile.providerName else {
|
||||||
|
|
|
@ -26,6 +26,8 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension ProviderServerItem {
|
extension ProviderServerItem {
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class ViewModel {
|
class ViewModel {
|
||||||
private let profile: LightProfile
|
private let profile: LightProfile
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,8 @@ import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
extension VPNItemGroup {
|
extension VPNItemGroup {
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class ViewModel {
|
class ViewModel {
|
||||||
private let vpnManager: LightVPNManager
|
private let vpnManager: LightVPNManager
|
||||||
|
|
||||||
|
@ -51,8 +53,10 @@ extension VPNItemGroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
Task { @MainActor in
|
||||||
vpnManager.removeDelegate(withIdentifier: "VPNItemGroup")
|
vpnManager.removeDelegate(withIdentifier: "VPNItemGroup")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var toggleTitle: String {
|
var toggleTitle: String {
|
||||||
toggleTitleBlock(vpnManager.isEnabled)
|
toggleTitleBlock(vpnManager.isEnabled)
|
||||||
|
|
|
@ -27,6 +27,8 @@ import Foundation
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
extension VisibilityItem {
|
extension VisibilityItem {
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class ViewModel {
|
class ViewModel {
|
||||||
private let transformer: ObservableProcessTransformer
|
private let transformer: ObservableProcessTransformer
|
||||||
|
|
||||||
|
|
|
@ -32,5 +32,6 @@ class PassepartoutMac: NSObject, MacBridge {
|
||||||
|
|
||||||
let utils: MacUtils = DefaultMacUtils()
|
let utils: MacUtils = DefaultMacUtils()
|
||||||
|
|
||||||
|
@MainActor
|
||||||
let menu: MacMenu = DefaultMacMenu()
|
let menu: MacMenu = DefaultMacMenu()
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
|
@MainActor
|
||||||
protocol ItemGroup {
|
protocol ItemGroup {
|
||||||
func asMenuItems(withParent parent: NSMenu) -> [NSMenuItem]
|
func asMenuItems(withParent parent: NSMenu) -> [NSMenuItem]
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
|
@MainActor
|
||||||
protocol SystemMenu {
|
protocol SystemMenu {
|
||||||
var asMenu: NSMenu { get }
|
var asMenu: NSMenu { get }
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import SwiftyBeaver
|
||||||
import PassepartoutCore
|
import PassepartoutCore
|
||||||
import PassepartoutUtils
|
import PassepartoutUtils
|
||||||
|
|
||||||
|
@MainActor
|
||||||
public final class UpgradeManager: ObservableObject {
|
public final class UpgradeManager: ObservableObject {
|
||||||
|
|
||||||
// MARK: Initialization
|
// MARK: Initialization
|
||||||
|
|
|
@ -30,6 +30,7 @@ import PassepartoutCore
|
||||||
import PassepartoutUtils
|
import PassepartoutUtils
|
||||||
import PassepartoutProviders
|
import PassepartoutProviders
|
||||||
|
|
||||||
|
@MainActor
|
||||||
public final class ProfileManager: ObservableObject {
|
public final class ProfileManager: ObservableObject {
|
||||||
public typealias ProfileEx = (profile: Profile, isReady: Bool)
|
public typealias ProfileEx = (profile: Profile, isReady: Bool)
|
||||||
|
|
||||||
|
@ -292,14 +293,12 @@ extension ProfileManager {
|
||||||
currentProfile.isLoading = true
|
currentProfile.isLoading = true
|
||||||
Task {
|
Task {
|
||||||
try await makeProfileReady(profile)
|
try await makeProfileReady(profile)
|
||||||
await MainActor.run {
|
|
||||||
currentProfile.value = profile
|
currentProfile.value = profile
|
||||||
currentProfile.isLoading = false
|
currentProfile.isLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileManager {
|
extension ProfileManager {
|
||||||
public func observeUpdates() {
|
public func observeUpdates() {
|
||||||
|
|
|
@ -25,48 +25,50 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
import Combine
|
|
||||||
|
|
||||||
public class SSIDReader: NSObject, ObservableObject, CLLocationManagerDelegate {
|
@MainActor
|
||||||
|
public class SSIDReader: NSObject, ObservableObject {
|
||||||
private let manager = CLLocationManager()
|
private let manager = CLLocationManager()
|
||||||
|
|
||||||
private let publisher = PassthroughSubject<String, Never>()
|
private var continuation: CheckedContinuation<String, Error>?
|
||||||
|
|
||||||
private var cancellables: Set<AnyCancellable> = []
|
private func currentSSID() async -> String {
|
||||||
|
await Utils.currentWifiSSID() ?? ""
|
||||||
public func requestCurrentSSID(onSSID: @escaping (String) -> Void) {
|
}
|
||||||
publisher
|
|
||||||
.sink(receiveValue: onSSID)
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
|
public func requestCurrentSSID() async throws -> String {
|
||||||
switch manager.authorizationStatus {
|
switch manager.authorizationStatus {
|
||||||
case .authorizedAlways, .authorizedWhenInUse, .denied:
|
case .authorizedAlways, .authorizedWhenInUse, .denied:
|
||||||
notifyCurrentSSID()
|
return await currentSSID()
|
||||||
return
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
self.continuation = continuation
|
||||||
|
|
||||||
manager.delegate = self
|
manager.delegate = self
|
||||||
manager.requestWhenInUseAuthorization()
|
manager.requestWhenInUseAuthorization()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func notifyCurrentSSID() {
|
|
||||||
Task {
|
|
||||||
let currentSSID = await Utils.currentWifiSSID() ?? ""
|
|
||||||
await MainActor.run {
|
|
||||||
publisher.send(currentSSID)
|
|
||||||
cancellables.removeAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension SSIDReader: CLLocationManagerDelegate {
|
||||||
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||||
switch manager.authorizationStatus {
|
switch manager.authorizationStatus {
|
||||||
case .authorizedWhenInUse, .authorizedAlways, .denied:
|
case .authorizedWhenInUse, .authorizedAlways, .denied:
|
||||||
notifyCurrentSSID()
|
Task {
|
||||||
|
continuation?.resume(returning: await currentSSID())
|
||||||
|
continuation = nil
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
cancellables.removeAll()
|
continuation?.resume(with: .success(""))
|
||||||
|
continuation = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||||
|
continuation?.resume(throwing: error)
|
||||||
|
continuation = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,6 @@ public class TunnelKitVPNManagerStrategy<VPNType: VPN>: VPNManagerStrategy where
|
||||||
|
|
||||||
private var currentBundleIdentifier: String?
|
private var currentBundleIdentifier: String?
|
||||||
|
|
||||||
@MainActor
|
|
||||||
public init(
|
public init(
|
||||||
appGroup: String,
|
appGroup: String,
|
||||||
tunnelBundleIdentifier: @escaping (VPNProtocolType) -> String,
|
tunnelBundleIdentifier: @escaping (VPNProtocolType) -> String,
|
||||||
|
|
|
@ -32,6 +32,7 @@ import PassepartoutProfiles
|
||||||
import PassepartoutProviders
|
import PassepartoutProviders
|
||||||
import PassepartoutUtils
|
import PassepartoutUtils
|
||||||
|
|
||||||
|
@MainActor
|
||||||
public final class VPNManager: ObservableObject {
|
public final class VPNManager: ObservableObject {
|
||||||
|
|
||||||
// MARK: Initialization
|
// MARK: Initialization
|
||||||
|
|
Loading…
Reference in New Issue