diff --git a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift index 1814c11..f434e31 100644 --- a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift +++ b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift @@ -50,11 +50,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { object: nil ) Task { - // If there's no NE config, then the user needs to sign in. - // However, they might have a session from a previous install, so we - // need to clear it. + // If there's no NE config, but the user is logged in, such as + // from a previous install, then we need to reconfigure. if await !vpn.loadNetworkExtensionConfig() { - state.clearSession() + state.reconfigure() } } } diff --git a/Coder Desktop/Coder Desktop/MenuBarIconController.swift b/Coder Desktop/Coder Desktop/MenuBarIconController.swift index 867e183..09c7381 100644 --- a/Coder Desktop/Coder Desktop/MenuBarIconController.swift +++ b/Coder Desktop/Coder Desktop/MenuBarIconController.swift @@ -13,6 +13,8 @@ class MenuBarController { init(menuBarExtra: FluidMenuBarExtra) { self.menuBarExtra = menuBarExtra + // Off by default, as `vpnDidUpdate` isn't called until the VPN is configured + menuBarExtra.setOpacity(offOpacity) } func vpnDidUpdate(_ connection: NETunnelProviderSession) { diff --git a/Coder Desktop/Coder Desktop/State.swift b/Coder Desktop/Coder Desktop/State.swift index ae63f4c..a8404ff 100644 --- a/Coder Desktop/Coder Desktop/State.swift +++ b/Coder Desktop/Coder Desktop/State.swift @@ -32,7 +32,7 @@ class AppState: ObservableObject { @Published var useLiteralHeaders: Bool = UserDefaults.standard.bool(forKey: Keys.useLiteralHeaders) { didSet { - if let onChange { onChange(tunnelProviderProtocol()) } + reconfigure() guard persistent else { return } UserDefaults.standard.set(useLiteralHeaders, forKey: Keys.useLiteralHeaders) } @@ -40,7 +40,7 @@ class AppState: ObservableObject { @Published var literalHeaders: [LiteralHeader] { didSet { - if let onChange { onChange(tunnelProviderProtocol()) } + reconfigure() guard persistent else { return } try? UserDefaults.standard.set(JSONEncoder().encode(literalHeaders), forKey: Keys.literalHeaders) } @@ -70,9 +70,13 @@ class AppState: ObservableObject { private let keychain: Keychain private let persistent: Bool - // This closure must be called when any property used to configure the VPN changes let onChange: ((NETunnelProviderProtocol?) -> Void)? + // reconfigure must be called when any property used to configure the VPN changes + public func reconfigure() { + if let onChange { onChange(tunnelProviderProtocol()) } + } + public init(onChange: ((NETunnelProviderProtocol?) -> Void)? = nil, persistent: Bool = true) { @@ -97,13 +101,13 @@ class AppState: ObservableObject { hasSession = true self.baseAccessURL = baseAccessURL self.sessionToken = sessionToken - if let onChange { onChange(tunnelProviderProtocol()) } + reconfigure() } public func clearSession() { hasSession = false sessionToken = nil - if let onChange { onChange(tunnelProviderProtocol()) } + reconfigure() } private func keychainGet(for key: String) -> String? { diff --git a/Coder Desktop/Coder Desktop/Views/LoginForm.swift b/Coder Desktop/Coder Desktop/Views/LoginForm.swift index acebb07..881c1a8 100644 --- a/Coder Desktop/Coder Desktop/Views/LoginForm.swift +++ b/Coder Desktop/Coder Desktop/Views/LoginForm.swift @@ -38,7 +38,7 @@ struct LoginForm: View { .animation(.easeInOut, value: currentPage) .onAppear { baseAccessURL = state.baseAccessURL?.absoluteString ?? baseAccessURL - sessionToken = "" + sessionToken = state.sessionToken ?? sessionToken } .alert("Error", isPresented: Binding( get: { loginError != nil }, @@ -122,7 +122,7 @@ struct LoginForm: View { ).disabled(true) } Section { - SecureField("Session Token", text: $sessionToken, prompt: Text("●●●●●●●●")) + SecureField("Session Token", text: $sessionToken) .autocorrectionDisabled() .privacySensitive() .focused($focusedField, equals: .sessionToken) diff --git a/Coder Desktop/VPN/Manager.swift b/Coder Desktop/VPN/Manager.swift index f1e5cdf..f074abb 100644 --- a/Coder Desktop/VPN/Manager.swift +++ b/Coder Desktop/VPN/Manager.swift @@ -30,6 +30,10 @@ actor Manager { let sessionConfig = URLSessionConfiguration.default // The tunnel might be asked to start before the network interfaces have woken up from sleep sessionConfig.waitsForConnectivity = true + // URLSession's waiting for connectivity sometimes hangs even when + // the network is up so this is deliberately short (15s) to avoid a + // poor UX where it appears stuck. + sessionConfig.timeoutIntervalForResource = 15 try await download(src: dylibPath, dest: dest, urlSession: URLSession(configuration: sessionConfig)) } catch { throw .download(error)