From ae34c5945617f30fef6c7d0b91899d3d8e212a1a Mon Sep 17 00:00:00 2001 From: Caleb Jasik Date: Wed, 19 Feb 2025 13:29:54 -0600 Subject: [PATCH 1/8] Enable `StrictConcurrency=complete` swift feature flag This causes several warnings. I'm going to start by trying to silence them by reflecting the current state of the project, rather than fixing them directly. --- ios/Runner.xcodeproj/project.pbxproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a53cf3e..682fd6d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -563,6 +563,7 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES; SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES; SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES; @@ -784,6 +785,7 @@ MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES; SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES; SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES; @@ -849,6 +851,7 @@ SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES; SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES; SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES; From 500e49edc31d490bd0c4c21022a24e28c1942ab6 Mon Sep 17 00:00:00 2001 From: Caleb Jasik Date: Wed, 19 Feb 2025 15:49:29 -0600 Subject: [PATCH 2/8] Mark Flutter import as `@preconcurrency` --- ios/Runner/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 885fffc..c9d31d5 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,4 +1,4 @@ -import Flutter +@preconcurrency import Flutter import MobileNebula import NetworkExtension import SwiftyJSON From bc67c06ef7fa22bfc953f72dc63b324f6368f359 Mon Sep 17 00:00:00 2001 From: Caleb Jasik Date: Wed, 19 Feb 2025 15:50:20 -0600 Subject: [PATCH 3/8] Mark SiteUpdater as `@unchecked Sendable` --- ios/Runner/Sites.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Runner/Sites.swift b/ios/Runner/Sites.swift index ed7b198..6c43185 100644 --- a/ios/Runner/Sites.swift +++ b/ios/Runner/Sites.swift @@ -78,14 +78,14 @@ class Sites { } } -class SiteUpdater: NSObject, FlutterStreamHandler { +class SiteUpdater: NSObject, FlutterStreamHandler, @unchecked Sendable { private var eventSink: FlutterEventSink? private var eventChannel: FlutterEventChannel private var site: Site private var notification: Any? public var startFunc: (() -> Void)? - private var configFd: Int32? = nil - private var configObserver: (any DispatchSourceFileSystemObject)? = nil + private var configFd: Int32? + private var configObserver: (any DispatchSourceFileSystemObject)? init(messenger: any FlutterBinaryMessenger, site: Site) { do { From f3882997be2cd40aa3048b09d49f172c23835d68 Mon Sep 17 00:00:00 2001 From: Caleb Jasik Date: Tue, 25 Feb 2025 12:13:02 -0600 Subject: [PATCH 4/8] Convert dict to Sendable string before throwing --- ios/NebulaNetworkExtension/Site.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/NebulaNetworkExtension/Site.swift b/ios/NebulaNetworkExtension/Site.swift index 52bfec5..9de8166 100644 --- a/ios/NebulaNetworkExtension/Site.swift +++ b/ios/NebulaNetworkExtension/Site.swift @@ -6,7 +6,7 @@ import os.log let log = Logger(subsystem: "net.defined.mobileNebula", category: "Site") enum SiteError: Error { - case nonConforming(site: [String: Any]?) + case nonConforming(site: String) case noCertificate case keyLoad case keySave @@ -22,7 +22,7 @@ extension SiteError: CustomStringConvertible { public var description: String { switch self { case .nonConforming(let site): - return String("Non-conforming site \(String(describing: site))") + return String("Non-conforming site \(site)") case .noCertificate: return "No certificate found" case .keyLoad: @@ -208,7 +208,7 @@ class Site: Codable { let id = dict?["id"] as? String ?? nil if id == nil { - throw SiteError.nonConforming(site: dict) + throw SiteError.nonConforming(site: String(describing: dict)) } try self.init(path: SiteList.getSiteConfigFile(id: id!, createDir: false)) From 8e9b5fcc4a2a1e0146b572d6509ddd9d7fa57db4 Mon Sep 17 00:00:00 2001 From: Caleb Jasik Date: Tue, 25 Feb 2025 12:14:33 -0600 Subject: [PATCH 5/8] Mark `PacketTunnelProvider` as `@unchecked Sendable` --- ios/NebulaNetworkExtension/PacketTunnelProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/NebulaNetworkExtension/PacketTunnelProvider.swift b/ios/NebulaNetworkExtension/PacketTunnelProvider.swift index e047f1e..270d443 100644 --- a/ios/NebulaNetworkExtension/PacketTunnelProvider.swift +++ b/ios/NebulaNetworkExtension/PacketTunnelProvider.swift @@ -23,7 +23,7 @@ extension AppMessageError: LocalizedError { } } -class PacketTunnelProvider: NEPacketTunnelProvider { +class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { private var networkMonitor: NWPathMonitor? private var site: Site? From df4c3a51b828e8a63574524951ea5d1043ace726 Mon Sep 17 00:00:00 2001 From: Caleb Jasik Date: Tue, 25 Feb 2025 13:15:24 -0600 Subject: [PATCH 6/8] Mark closures in `DNUpdate` as `@Sendable` --- ios/Runner/DNUpdate.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/Runner/DNUpdate.swift b/ios/Runner/DNUpdate.swift index c59652f..e968bac 100644 --- a/ios/Runner/DNUpdate.swift +++ b/ios/Runner/DNUpdate.swift @@ -6,7 +6,7 @@ class DNUpdater { private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes private let log = Logger(subsystem: "net.defined.mobileNebula", category: "DNUpdater") - func updateAll(onUpdate: @escaping (Site) -> Void) { + func updateAll(onUpdate: @Sendable @escaping (Site) -> Void) { _ = SiteList { (sites, _) -> Void in // NEVPN seems to force us onto the main thread and we are about to make network calls that // could block for a while. Push ourselves onto another thread to avoid blocking the UI. @@ -23,21 +23,21 @@ class DNUpdater { } } - func updateAllLoop(onUpdate: @escaping (Site) -> Void) { + func updateAllLoop(onUpdate: @Sendable @escaping (Site) -> Void) { timer.eventHandler = { self.updateAll(onUpdate: onUpdate) } timer.resume() } - func updateSingleLoop(site: Site, onUpdate: @escaping (Site) -> Void) { + func updateSingleLoop(site: Site, onUpdate: @Sendable @escaping (Site) -> Void) { timer.eventHandler = { self.updateSite(site: site, onUpdate: onUpdate) } timer.resume() } - func updateSite(site: Site, onUpdate: @escaping (Site) -> Void) { + func updateSite(site: Site, onUpdate: @Sendable @escaping (Site) -> Void) { do { if !site.managed { return From a32d17705c43c4ea5ed5334800149f94e30c7063 Mon Sep 17 00:00:00 2001 From: Caleb Jasik Date: Tue, 25 Feb 2025 13:19:39 -0600 Subject: [PATCH 7/8] Use an AsyncStream to avoid sending `@MainActor` restricted AppDelegate into other isolation domains --- ios/NebulaNetworkExtension/Site.swift | 2 +- ios/Runner/APIClient.swift | 4 ++-- ios/Runner/AppDelegate.swift | 22 ++++++++++++---------- ios/Runner/DNUpdate.swift | 14 +++++++++++++- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/ios/NebulaNetworkExtension/Site.swift b/ios/NebulaNetworkExtension/Site.swift index 9de8166..bbfb8f9 100644 --- a/ios/NebulaNetworkExtension/Site.swift +++ b/ios/NebulaNetworkExtension/Site.swift @@ -150,7 +150,7 @@ let statusString: [NEVPNStatus: String] = [ ] // Represents a site that was pulled out of the system configuration -class Site: Codable { +class Site: Codable, @unchecked Sendable { // Stored in manager var name: String var id: String diff --git a/ios/Runner/APIClient.swift b/ios/Runner/APIClient.swift index 4fe01d3..e7b91e5 100644 --- a/ios/Runner/APIClient.swift +++ b/ios/Runner/APIClient.swift @@ -1,10 +1,10 @@ -import MobileNebula +@preconcurrency import MobileNebula enum APIClientError: Error { case invalidCredentials } -class APIClient { +struct APIClient: Sendable { let apiClient: MobileNebulaAPIClient let json = JSONDecoder() diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index c9d31d5..4f26776 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -25,17 +25,19 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError { ) -> Bool { GeneratedPluginRegistrant.register(with: self) - dnUpdater.updateAllLoop { site in - // Signal the site has changed in case the current site details screen is active - let container = self.sites?.getContainer(id: site.id) - if container != nil { - // Update references to the site with the new site config - container!.site = site - container!.updater.update(connected: site.connected ?? false, replaceSite: site) - } + Task { + for await site in dnUpdater.siteUpdates { + // Signal the site has changed in case the current site details screen is active + let container = self.sites?.getContainer(id: site.id) + if container != nil { + // Update references to the site with the new site config + container!.site = site + container!.updater.update(connected: site.connected ?? false, replaceSite: site) + } - // Signal to the main screen to reload - self.ui?.invokeMethod("refreshSites", arguments: nil) + // Signal to the main screen to reload + self.ui?.invokeMethod("refreshSites", arguments: nil) + } } guard let controller = window?.rootViewController as? FlutterViewController else { diff --git a/ios/Runner/DNUpdate.swift b/ios/Runner/DNUpdate.swift index e968bac..6f585d5 100644 --- a/ios/Runner/DNUpdate.swift +++ b/ios/Runner/DNUpdate.swift @@ -1,7 +1,7 @@ import Foundation import os.log -class DNUpdater { +class DNUpdater: @unchecked Sendable { private let apiClient = APIClient() private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes private let log = Logger(subsystem: "net.defined.mobileNebula", category: "DNUpdater") @@ -30,6 +30,18 @@ class DNUpdater { timer.resume() } + // Site updates provides an async/await alternative to `.updateAllLoop` that doesn't require a sendable closure. + // https://developer.apple.com/documentation/swift/asyncstream + var siteUpdates: AsyncStream { + AsyncStream { continuation in + self.updateAllLoop(onUpdate: { site in + continuation.yield(site) + + }) + + } + } + func updateSingleLoop(site: Site, onUpdate: @Sendable @escaping (Site) -> Void) { timer.eventHandler = { self.updateSite(site: site, onUpdate: onUpdate) From 281d690ef30b42707c443be595d2749fe8560756 Mon Sep 17 00:00:00 2001 From: Caleb Jasik Date: Tue, 25 Feb 2025 15:17:49 -0600 Subject: [PATCH 8/8] Use Swift 6 --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 682fd6d..f958398 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -575,7 +575,7 @@ SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES; SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES; SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -797,7 +797,7 @@ SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES; SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES; SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -863,7 +863,7 @@ SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES; SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES; SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; };