refactor(swift): replace @unchecked Sendable (#10970)

VPNConfigurationManager now uses `@MainActor` isolation instead of
`@unchecked Sendable`. This aligns with Apple's documented behaviour
where NEVPNManager callbacks arrive on the main thread.

- Made `VPNConfigurationManager` final and `@MainActor`
- Added `@MainActor` to `LogExporter.export(to:session:)` on macOS
- Marked `legacyConfiguration()` as `nonisolated` (pure function, called
from network extension)
- Removed redundant `@MainActor` from `maybeMigrateConfiguration()`
This commit is contained in:
Mariusz Klochowicz
2025-11-26 14:11:17 +10:30
committed by GitHub
parent 13d9a2bbcd
commit 994de0fe2a
2 changed files with 9 additions and 6 deletions

View File

@@ -27,6 +27,7 @@ import System
case invalidFileHandle
}
@MainActor
static func export(
to archiveURL: URL,
session: NETunnelProviderSession

View File

@@ -21,10 +21,10 @@ enum VPNConfigurationManagerError: Error {
}
}
/// Thread-safe: Immutable wrapper around NETunnelProviderManager.
/// Only contains a 'let' property. NETunnelProviderManager handles its own synchronisation.
public class VPNConfigurationManager: @unchecked Sendable {
// Persists our tunnel settings
// NEVPNManager callbacks are documented to arrive on main thread;
// we isolate to @MainActor to align with this design.
@MainActor
public final class VPNConfigurationManager {
let manager: NETunnelProviderManager
public static let bundleIdentifier: String = "\(Bundle.main.bundleIdentifier!).network-extension"
@@ -51,7 +51,10 @@ public class VPNConfigurationManager: @unchecked Sendable {
self.manager = manager
}
public static func legacyConfiguration(protocolConfiguration: NETunnelProviderProtocol?)
// Pure function - doesn't access actor-isolated state.
nonisolated public static func legacyConfiguration(
protocolConfiguration: NETunnelProviderProtocol?
)
-> [String: String]?
{
guard let protocolConfiguration = protocolConfiguration,
@@ -89,7 +92,6 @@ public class VPNConfigurationManager: @unchecked Sendable {
// Firezone 1.4.14 and below stored some app configuration in the VPN provider configuration fields. This has since
// been moved to a dedicated UserDefaults-backed persistent store.
@MainActor
func maybeMigrateConfiguration() async throws {
guard
let legacyConfiguration = Self.legacyConfiguration(