From 08dee37d09a9872154cf59394b40340bfae1f90f Mon Sep 17 00:00:00 2001 From: Jamil Date: Mon, 12 May 2025 07:27:47 -0700 Subject: [PATCH] feat(apple): Poll tunnel for new configuration every 1s (#9083) Similar to how we fetch new resources, we add a Configuration poller that fetches new configuration every 1s. If the configuration is unchanged, we respond to the caller with a cached copy to avoid needing to serialize the data over IPC. Related: #4505 --- .../Sources/FirezoneKit/Stores/Store.swift | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift index 695daec7c..2e091c021 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift @@ -14,6 +14,8 @@ import AppKit #endif @MainActor +// TODO: Move some state logic to view models +// swiftlint:disable:next type_body_length public final class Store: ObservableObject { @Published private(set) var favorites = Favorites() @Published private(set) var resourceList: ResourceList = .loading @@ -36,6 +38,8 @@ public final class Store: ObservableObject { let sessionNotification = SessionNotification() + private var configurationTimer: Timer? + private var configurationUpdateTask: Task? private var resourcesTimer: Timer? private var resourceUpdateTask: Task? @@ -105,6 +109,14 @@ public final class Store: ObservableObject { func handleStatusChange(newStatus: NEVPNStatus) async throws { status = newStatus + if status == .invalid { + // VPN configuration was yanked from system settings + endConfigurationPolling() + } else { + // This is a no-op if the timer is already active + beginConfigurationPolling() + } + if status == .connected { beginUpdatingResources() } else { @@ -273,6 +285,43 @@ public final class Store: ObservableObject { try ipcClient().start(token: token) } + private func beginConfigurationPolling() { + // Ensure we're idempotent if called twice + if self.configurationTimer != nil { + return + } + + let updateConfiguration: @Sendable (Timer) -> Void = { _ in + Task { + await MainActor.run { + self.configurationUpdateTask?.cancel() + self.configurationUpdateTask = Task { + if !Task.isCancelled { + do { + self.configuration = try await self.ipcClient().getConfiguration() + } catch { + Log.error(error) + } + } + } + } + } + } + + let intervalInSeconds: TimeInterval = 1 + let timer = Timer(timeInterval: intervalInSeconds, repeats: true, block: updateConfiguration) + + RunLoop.main.add(timer, forMode: .common) + self.configurationTimer = timer + } + + private func endConfigurationPolling() { + configurationUpdateTask?.cancel() + configurationTimer?.invalidate() + configurationTimer = nil + self.configuration = nil + } + // Network Extensions don't have a 2-way binding up to the GUI process, // so we need to periodically ask the tunnel process for them. private func beginUpdatingResources() {