diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift index a8d0c946..091a1c25 100644 --- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift +++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift @@ -37,6 +37,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { vpn = CoderVPNService() state = AppState(onChange: vpn.configureTunnelProviderProtocol) fileSyncDaemon = MutagenDaemon() + if state.startVPNOnLaunch { + vpn.startWhenReady = true + } + vpn.installSystemExtension() } func applicationDidFinishLaunching(_: Notification) { @@ -68,9 +72,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { if await !vpn.loadNetworkExtensionConfig() { state.reconfigure() } - if state.startVPNOnLaunch { - await vpn.start() - } } // TODO: Start the daemon only once a file sync is configured Task { @@ -78,6 +79,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + deinit { + NotificationCenter.default.removeObserver(self) + } + // This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)` // or return `.terminateNow` func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply { diff --git a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift index ca0a8ff3..22a3ad8b 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift @@ -18,6 +18,16 @@ enum VPNServiceState: Equatable { case disconnecting case connected case failed(VPNServiceError) + + var canBeStarted: Bool { + switch self { + // A tunnel failure should not prevent a reconnect attempt + case .disabled, .failed: + true + default: + false + } + } } enum VPNServiceError: Error, Equatable { @@ -54,11 +64,18 @@ final class CoderVPNService: NSObject, VPNService { guard neState == .enabled || neState == .disabled else { return .failed(.networkExtensionError(neState)) } + if startWhenReady, tunnelState.canBeStarted { + startWhenReady = false + Task { await start() } + } return tunnelState } @Published var menuState: VPNMenuState = .init() + // Whether the VPN should start as soon as possible + var startWhenReady: Bool = false + // systemExtnDelegate holds a reference to the SystemExtensionDelegate so that it doesn't get // garbage collected while the OSSystemExtensionRequest is in flight, since the OS framework // only stores a weak reference to the delegate. @@ -68,11 +85,6 @@ final class CoderVPNService: NSObject, VPNService { override init() { super.init() - installSystemExtension() - } - - deinit { - NotificationCenter.default.removeObserver(self) } func start() async { diff --git a/Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift b/Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift index a07ced3f..26f5883d 100644 --- a/Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift @@ -107,6 +107,12 @@ struct LoginTests { data: [.get: Client.encoder.encode(buildInfo)] ).register() + try Mock( + url: url.appendingPathComponent("/api/v2/users/me"), + statusCode: 200, + data: [.get: Client.encoder.encode(User(id: UUID(), username: "username"))] + ).register() + try await ViewHosting.host(view) { try await sut.inspection.inspect { view in try view.find(ViewType.TextField.self).setInput(url.absoluteString)