Update Tunnel on the main thread

Also updated it to use the new Swift Observable macro
This commit is contained in:
Conrad Kramer 2024-01-20 09:39:30 -08:00
parent b008762a5b
commit 436a67b352
10 changed files with 167 additions and 69 deletions

View file

@ -15,7 +15,7 @@ struct BurrowApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
TunnelView() TunnelView(tunnel: Self.tunnel)
} }
} }
} }

View file

@ -8,7 +8,7 @@
import SwiftUI import SwiftUI
struct MenuItemToggleView: View { struct MenuItemToggleView: View {
@ObservedObject var tunnel: Tunnel var tunnel: Tunnel
var body: some View { var body: some View {
HStack { HStack {
@ -23,7 +23,6 @@ struct MenuItemToggleView: View {
.padding(.horizontal, 4) .padding(.horizontal, 4)
.padding(10) .padding(10)
.frame(minWidth: 300, minHeight: 32, maxHeight: 32) .frame(minWidth: 300, minHeight: 32, maxHeight: 32)
.task { await tunnel.update() }
} }
} }

View file

@ -2,13 +2,13 @@ import NetworkExtension
extension NEVPNManager { extension NEVPNManager {
func remove() async throws { func remove() async throws {
let _: Void = try await withUnsafeThrowingContinuation { continuation in _ = try await withUnsafeThrowingContinuation { continuation in
removeFromPreferences(completionHandler: completion(continuation)) removeFromPreferences(completionHandler: completion(continuation))
} }
} }
func save() async throws { func save() async throws {
let _: Void = try await withUnsafeThrowingContinuation { continuation in _ = try await withUnsafeThrowingContinuation { continuation in
saveToPreferences(completionHandler: completion(continuation)) saveToPreferences(completionHandler: completion(continuation))
} }
} }
@ -18,13 +18,7 @@ extension NETunnelProviderManager {
class var managers: [NETunnelProviderManager] { class var managers: [NETunnelProviderManager] {
get async throws { get async throws {
try await withUnsafeThrowingContinuation { continuation in try await withUnsafeThrowingContinuation { continuation in
loadAllFromPreferences { managers, error in loadAllFromPreferences(completionHandler: completion(continuation))
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: managers ?? [])
}
}
} }
} }
} }
@ -32,10 +26,20 @@ extension NETunnelProviderManager {
private func completion(_ continuation: UnsafeContinuation<Void, Error>) -> (Error?) -> Void { private func completion(_ continuation: UnsafeContinuation<Void, Error>) -> (Error?) -> Void {
return { error in return { error in
if let error = error { if let error {
continuation.resume(throwing: error) continuation.resume(throwing: error)
} else { } else {
continuation.resume(returning: ()) continuation.resume(returning: ())
} }
} }
} }
private func completion<T>(_ continuation: UnsafeContinuation<T, Error>) -> (T?, Error?) -> Void {
return { value, error in
if let error {
continuation.resume(throwing: error)
} else if let value {
continuation.resume(returning: value)
}
}
}

View file

@ -2,15 +2,16 @@ import Combine
import NetworkExtension import NetworkExtension
import SwiftUI import SwiftUI
@MainActor @Observable class Tunnel {
class Tunnel: ObservableObject { private(set) var status: Status = .unknown
@Published private(set) var status: Status = .unknown private var error: NEVPNError?
@Published private var error: NEVPNError?
private let bundleIdentifier: String private let bundleIdentifier: String
private let configure: (NETunnelProviderManager, NETunnelProviderProtocol) -> Void private let configure: (NETunnelProviderManager, NETunnelProviderProtocol) -> Void
private var tasks: [Task<Void, Error>] = [] private var tasks: [Task<Void, Error>] = []
// Each manager corresponds to one entry in the Settings app.
// Our goal is to maintain a single manager, so we create one if none exist and delete extra if there are any.
private var managers: [NEVPNManager]? { private var managers: [NEVPNManager]? {
didSet { status = currentStatus } didSet { status = currentStatus }
} }
@ -48,24 +49,31 @@ class Tunnel: ObservableObject {
self.bundleIdentifier = bundleIdentifier self.bundleIdentifier = bundleIdentifier
self.configure = configure self.configure = configure
listenForUpdates()
Task { await update() }
}
private func listenForUpdates() {
let center = NotificationCenter.default
let statusTask = Task { let statusTask = Task {
for try await _ in NotificationCenter.default.notifications(named: .NEVPNStatusDidChange) { for try await _ in center.notifications(named: .NEVPNStatusDidChange).map({ _ in () }) {
status = currentStatus status = currentStatus
} }
} }
let configurationTask = Task { let configurationTask = Task {
for try await _ in NotificationCenter.default.notifications(named: .NEVPNConfigurationChange) { for try await _ in center.notifications(named: .NEVPNConfigurationChange).map({ _ in () }) {
await update() await update()
} }
} }
tasks = [statusTask, configurationTask] tasks = [statusTask, configurationTask]
} }
func update() async { private func update() async {
do { do {
managers = try await NETunnelProviderManager.managers let updated = try await NETunnelProviderManager.managers
} catch let error as NEVPNError { await MainActor.run { managers = updated }
self.error = error } catch let vpnError as NEVPNError {
error = vpnError
} catch { } catch {
print(error) print(error)
} }
@ -109,7 +117,9 @@ class Tunnel: ObservableObject {
} }
deinit { deinit {
tasks.forEach { $0.cancel() } for task in tasks {
task.cancel()
}
} }
} }

View file

@ -1,36 +1,34 @@
import SwiftUI import SwiftUI
struct TunnelView: View { struct TunnelView: View {
// @ObservedObject var tunnel: Tunnel var tunnel: Tunnel
var body: some View { var body: some View {
EmptyView() VStack {
// VStack { Text(verbatim: tunnel.status.description)
// Text(verbatim: tunnel.status.description) switch tunnel.status {
// switch tunnel.status { case .connected:
// case .connected: Button("Disconnect", action: stop)
// Button("Disconnect", action: stop) case .permissionRequired:
// case .permissionRequired: Button("Allow", action: configure)
// Button("Allow", action: configure) case .disconnected:
// case .disconnected: Button("Start", action: start)
// Button("Start", action: start) default:
// default: EmptyView()
// EmptyView() }
// } }
// } .padding()
// .task { await tunnel.update() }
// .padding()
} }
// private func start() { private func start() {
// try? tunnel.start() try? tunnel.start()
// } }
//
// private func stop() { private func stop() {
// tunnel.stop() tunnel.stop()
// } }
//
// private func configure() { private func configure() {
// Task { try await tunnel.configure() } Task { try await tunnel.configure() }
// } }
} }

View file

@ -196,7 +196,7 @@
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
D0BCC6122A0B328800AD070D /* PBXTargetDependency */, D08252712B5C3E2E005DA378 /* PBXTargetDependency */,
); );
name = NetworkExtension; name = NetworkExtension;
productName = BurrowNetworkExtension; productName = BurrowNetworkExtension;
@ -215,7 +215,7 @@
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
D0BCC6142A0B329200AD070D /* PBXTargetDependency */, D08252732B5C3E33005DA378 /* PBXTargetDependency */,
D020F65C29E4A697002790F6 /* PBXTargetDependency */, D020F65C29E4A697002790F6 /* PBXTargetDependency */,
); );
name = App; name = App;
@ -231,7 +231,7 @@
attributes = { attributes = {
BuildIndependentTargetsInParallel = 1; BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1430; LastSwiftUpdateCheck = 1430;
LastUpgradeCheck = 1430; LastUpgradeCheck = 1510;
TargetAttributes = { TargetAttributes = {
D020F65229E4A697002790F6 = { D020F65229E4A697002790F6 = {
CreatedOnToolsVersion = 14.3; CreatedOnToolsVersion = 14.3;
@ -251,6 +251,7 @@
); );
mainGroup = D05B9F6929E39EEC008CB1F9; mainGroup = D05B9F6929E39EEC008CB1F9;
packageReferences = ( packageReferences = (
D082526F2B5C3E23005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */,
); );
productRefGroup = D05B9F7329E39EEC008CB1F9 /* Products */; productRefGroup = D05B9F7329E39EEC008CB1F9 /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -337,13 +338,13 @@
target = D020F65229E4A697002790F6 /* NetworkExtension */; target = D020F65229E4A697002790F6 /* NetworkExtension */;
targetProxy = D020F65B29E4A697002790F6 /* PBXContainerItemProxy */; targetProxy = D020F65B29E4A697002790F6 /* PBXContainerItemProxy */;
}; };
D0BCC6122A0B328800AD070D /* PBXTargetDependency */ = { D08252712B5C3E2E005DA378 /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
productRef = D0BCC6112A0B328800AD070D /* SwiftLintPlugin */; productRef = D08252702B5C3E2E005DA378 /* SwiftLintPlugin */;
}; };
D0BCC6142A0B329200AD070D /* PBXTargetDependency */ = { D08252732B5C3E33005DA378 /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
productRef = D0BCC6132A0B329200AD070D /* SwiftLintPlugin */; productRef = D08252722B5C3E33005DA378 /* SwiftLintPlugin */;
}; };
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
@ -423,25 +424,25 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
D0BCC6102A0B327700AD070D /* XCRemoteSwiftPackageReference "SwiftLint" */ = { D082526F2B5C3E23005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/realm/SwiftLint.git"; repositoryURL = "https://github.com/realm/SwiftLint.git";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 0.51.0; minimumVersion = 0.54.0;
}; };
}; };
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
D0BCC6112A0B328800AD070D /* SwiftLintPlugin */ = { D08252702B5C3E2E005DA378 /* SwiftLintPlugin */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = D0BCC6102A0B327700AD070D /* XCRemoteSwiftPackageReference "SwiftLint" */; package = D082526F2B5C3E23005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */;
productName = "plugin:SwiftLintPlugin"; productName = "plugin:SwiftLintPlugin";
}; };
D0BCC6132A0B329200AD070D /* SwiftLintPlugin */ = { D08252722B5C3E33005DA378 /* SwiftLintPlugin */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = D0BCC6102A0B327700AD070D /* XCRemoteSwiftPackageReference "SwiftLint" */; package = D082526F2B5C3E23005DA378 /* XCRemoteSwiftPackageReference "SwiftLint" */;
productName = "plugin:SwiftLintPlugin"; productName = "plugin:SwiftLintPlugin";
}; };
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */

View file

@ -0,0 +1,86 @@
{
"pins" : [
{
"identity" : "collectionconcurrencykit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git",
"state" : {
"revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95",
"version" : "0.2.0"
}
},
{
"identity" : "cryptoswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
"state" : {
"revision" : "7892a123f7e8d0fe62f9f03728b17bbd4f94df5c",
"version" : "1.8.1"
}
},
{
"identity" : "sourcekitten",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/SourceKitten.git",
"state" : {
"revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56",
"version" : "0.34.1"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a",
"version" : "1.2.2"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036",
"version" : "509.0.2"
}
},
{
"identity" : "swiftlint",
"kind" : "remoteSourceControl",
"location" : "https://github.com/realm/SwiftLint.git",
"state" : {
"revision" : "f17a4f9dfb6a6afb0408426354e4180daaf49cee",
"version" : "0.54.0"
}
},
{
"identity" : "swiftytexttable",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scottrhoyt/SwiftyTextTable.git",
"state" : {
"revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3",
"version" : "0.9.0"
}
},
{
"identity" : "swxmlhash",
"kind" : "remoteSourceControl",
"location" : "https://github.com/drmohundro/SWXMLHash.git",
"state" : {
"revision" : "4d0f62f561458cbe1f732171e625f03195151b60",
"version" : "7.0.1"
}
},
{
"identity" : "yams",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/Yams.git",
"state" : {
"revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3",
"version" : "5.0.6"
}
}
],
"version" : 2
}

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1430" LastUpgradeVersion = "1510"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1430" LastUpgradeVersion = "1510"
version = "2.0"> version = "2.0">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View file

@ -4,8 +4,8 @@ SDKROOT = auto
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES
SUPPORTED_PLATFORMS = iphoneos iphonesimulator macosx SUPPORTED_PLATFORMS = iphoneos iphonesimulator macosx
IPHONEOS_DEPLOYMENT_TARGET = 15.0 IPHONEOS_DEPLOYMENT_TARGET = 17.0
MACOSX_DEPLOYMENT_TARGET = 12.0 MACOSX_DEPLOYMENT_TARGET = 14.0
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO
SUPPORTS_MACCATALYST = NO SUPPORTS_MACCATALYST = NO