From a32d17705c43c4ea5ed5334800149f94e30c7063 Mon Sep 17 00:00:00 2001 From: Caleb Jasik Date: Tue, 25 Feb 2025 13:19:39 -0600 Subject: [PATCH] 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)