From 646217784a0f0c297dac05c73a475a86cd3fb779 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 10 Apr 2025 16:42:34 +1000 Subject: [PATCH 01/10] feat: use the deployment's hostname suffix in the UI --- .../Coder-Desktop/Coder_DesktopApp.swift | 4 +- Coder-Desktop/Coder-Desktop/State.swift | 46 +++++++++++++++++-- .../Coder-Desktop/Views/VPN/VPNMenuItem.swift | 11 +++-- Coder-Desktop/CoderSDK/Deployment.swift | 14 ++++++ 4 files changed, 67 insertions(+), 8 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift index 30ea7e7..cf0624e 100644 --- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift +++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift @@ -65,10 +65,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { title: "Coder Desktop", image: "MenuBarIcon", onAppear: { - // If the VPN is enabled, it's likely the token isn't expired + // If the VPN is enabled, it's likely the token hasn't expired, + // and the deployment config is up to date. guard case .disabled = self.vpn.state, self.state.hasSession else { return } Task { @MainActor in await self.state.handleTokenExpiry() + await self.state.refreshDeploymentConfig() } }, content: { VPNMenu().frame(width: 256) diff --git a/Coder-Desktop/Coder-Desktop/State.swift b/Coder-Desktop/Coder-Desktop/State.swift index aea2fe9..b1aba95 100644 --- a/Coder-Desktop/Coder-Desktop/State.swift +++ b/Coder-Desktop/Coder-Desktop/State.swift @@ -25,6 +25,15 @@ class AppState: ObservableObject { } } + @Published private(set) var hostnameSuffix: String { + didSet { + guard persistent else { return } + UserDefaults.standard.set(hostnameSuffix, forKey: Keys.hostnameSuffix) + } + } + + static let defaultHostnameSuffix: String = "coder" + // Stored in Keychain @Published private(set) var sessionToken: String? { didSet { @@ -33,6 +42,8 @@ class AppState: ObservableObject { } } + var client: Client? + @Published var useLiteralHeaders: Bool = UserDefaults.standard.bool(forKey: Keys.useLiteralHeaders) { didSet { reconfigure() @@ -80,7 +91,7 @@ class AppState: ObservableObject { private let keychain: Keychain private let persistent: Bool - let onChange: ((NETunnelProviderProtocol?) -> Void)? + private let onChange: ((NETunnelProviderProtocol?) -> Void)? // reconfigure must be called when any property used to configure the VPN changes public func reconfigure() { @@ -94,6 +105,10 @@ class AppState: ObservableObject { self.onChange = onChange keychain = Keychain(service: Bundle.main.bundleIdentifier!) _hasSession = Published(initialValue: persistent ? UserDefaults.standard.bool(forKey: Keys.hasSession) : false) + _hostnameSuffix = Published( + initialValue: persistent ? UserDefaults.standard + .string(forKey: Keys.hostnameSuffix) ?? Self.defaultHostnameSuffix : Self.defaultHostnameSuffix + ) _baseAccessURL = Published( initialValue: persistent ? UserDefaults.standard.url(forKey: Keys.baseAccessURL) : nil ) @@ -107,6 +122,11 @@ class AppState: ObservableObject { if sessionToken == nil || sessionToken!.isEmpty == true { clearSession() } + client = Client( + url: baseAccessURL!, + token: sessionToken!, + headers: useLiteralHeaders ? literalHeaders.map { $0.toSDKHeader() } : [] + ) } } @@ -114,14 +134,18 @@ class AppState: ObservableObject { hasSession = true self.baseAccessURL = baseAccessURL self.sessionToken = sessionToken + client = Client( + url: baseAccessURL, + token: sessionToken, + headers: useLiteralHeaders ? literalHeaders.map { $0.toSDKHeader() } : [] + ) reconfigure() } public func handleTokenExpiry() async { if hasSession { - let client = Client(url: baseAccessURL!, token: sessionToken!) do { - _ = try await client.user("me") + _ = try await client!.user("me") } catch let SDKError.api(apiErr) { // Expired token if apiErr.statusCode == 401 { @@ -135,9 +159,24 @@ class AppState: ObservableObject { } } + public func refreshDeploymentConfig() async { + if hasSession { + do { + let config = try await client!.sshConfiguration() + hostnameSuffix = config.hostname_suffix ?? Self.defaultHostnameSuffix + } catch { + // If fetching the config fails, there's likely a bigger issue. + // We'll show an error in the UI if they try and do something + logger.error("failed to refresh deployment config: \(error)") + return + } + } + } + public func clearSession() { hasSession = false sessionToken = nil + client = nil reconfigure() } @@ -159,6 +198,7 @@ class AppState: ObservableObject { static let hasSession = "hasSession" static let baseAccessURL = "baseAccessURL" static let sessionToken = "sessionToken" + static let hostnameSuffix = "hostnameSuffix" static let useLiteralHeaders = "UseLiteralHeaders" static let literalHeaders = "LiteralHeaders" diff --git a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift index af7e6bb..804828a 100644 --- a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift +++ b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift @@ -42,6 +42,8 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable { } struct MenuItemView: View { + @EnvironmentObject var state: AppState + let item: VPNMenuItem let baseAccessURL: URL @State private var nameIsSelected: Bool = false @@ -49,14 +51,15 @@ struct MenuItemView: View { private var itemName: AttributedString { let name = switch item { - case let .agent(agent): agent.primaryHost ?? "\(item.wsName).coder" - case .offlineWorkspace: "\(item.wsName).coder" + case let .agent(agent): agent.primaryHost ?? "\(item.wsName).\(state.hostnameSuffix)" + case .offlineWorkspace: "\(item.wsName).\(state.hostnameSuffix)" } var formattedName = AttributedString(name) formattedName.foregroundColor = .primary - if let range = formattedName.range(of: ".coder") { - formattedName[range].foregroundColor = .secondary + + if let lastDot = formattedName.range(of: ".", options: .backwards) { + formattedName[lastDot.lowerBound ..< formattedName.endIndex].foregroundColor = .secondary } return formattedName } diff --git a/Coder-Desktop/CoderSDK/Deployment.swift b/Coder-Desktop/CoderSDK/Deployment.swift index b88029f..cca6f04 100644 --- a/Coder-Desktop/CoderSDK/Deployment.swift +++ b/Coder-Desktop/CoderSDK/Deployment.swift @@ -8,6 +8,14 @@ public extension Client { } return try decode(BuildInfoResponse.self, from: res.data) } + + func sshConfiguration() async throws(SDKError) -> SSHConfigResponse { + let res = try await request("/api/v2/deployment/ssh", method: .get) + guard res.resp.statusCode == 200 else { + throw responseAsError(res) + } + return try decode(SSHConfigResponse.self, from: res.data) + } } public struct BuildInfoResponse: Codable, Equatable, Sendable { @@ -20,3 +28,9 @@ public struct BuildInfoResponse: Codable, Equatable, Sendable { .flatMap { Range($0.range(at: 1), in: version).map { String(version[$0]) } } } } + +public struct SSHConfigResponse: Codable, Equatable, Sendable { + public let hostname_prefix: String? + public let hostname_suffix: String? + public let ssh_config_options: [String: String] +} From f141a7478b2d5e4e1406e82faf434604d6e7e372 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 10 Apr 2025 16:58:08 +1000 Subject: [PATCH 02/10] fixup --- Coder-Desktop/Coder-Desktop/State.swift | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/State.swift b/Coder-Desktop/Coder-Desktop/State.swift index b1aba95..ee3c047 100644 --- a/Coder-Desktop/Coder-Desktop/State.swift +++ b/Coder-Desktop/Coder-Desktop/State.swift @@ -25,12 +25,7 @@ class AppState: ObservableObject { } } - @Published private(set) var hostnameSuffix: String { - didSet { - guard persistent else { return } - UserDefaults.standard.set(hostnameSuffix, forKey: Keys.hostnameSuffix) - } - } + @Published private(set) var hostnameSuffix: String = defaultHostnameSuffix static let defaultHostnameSuffix: String = "coder" @@ -105,10 +100,6 @@ class AppState: ObservableObject { self.onChange = onChange keychain = Keychain(service: Bundle.main.bundleIdentifier!) _hasSession = Published(initialValue: persistent ? UserDefaults.standard.bool(forKey: Keys.hasSession) : false) - _hostnameSuffix = Published( - initialValue: persistent ? UserDefaults.standard - .string(forKey: Keys.hostnameSuffix) ?? Self.defaultHostnameSuffix : Self.defaultHostnameSuffix - ) _baseAccessURL = Published( initialValue: persistent ? UserDefaults.standard.url(forKey: Keys.baseAccessURL) : nil ) @@ -127,6 +118,7 @@ class AppState: ObservableObject { token: sessionToken!, headers: useLiteralHeaders ? literalHeaders.map { $0.toSDKHeader() } : [] ) + Task { await refreshDeploymentConfig() } } } @@ -139,6 +131,7 @@ class AppState: ObservableObject { token: sessionToken, headers: useLiteralHeaders ? literalHeaders.map { $0.toSDKHeader() } : [] ) + Task { await refreshDeploymentConfig() } reconfigure() } From 5dea75fdc1f238466667fa925832bf9228425448 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 10 Apr 2025 17:09:00 +1000 Subject: [PATCH 03/10] fixup --- Coder-Desktop/Coder-Desktop/State.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/State.swift b/Coder-Desktop/Coder-Desktop/State.swift index ee3c047..fade6cc 100644 --- a/Coder-Desktop/Coder-Desktop/State.swift +++ b/Coder-Desktop/Coder-Desktop/State.swift @@ -37,7 +37,7 @@ class AppState: ObservableObject { } } - var client: Client? + private var client: Client? @Published var useLiteralHeaders: Bool = UserDefaults.standard.bool(forKey: Keys.useLiteralHeaders) { didSet { @@ -118,7 +118,10 @@ class AppState: ObservableObject { token: sessionToken!, headers: useLiteralHeaders ? literalHeaders.map { $0.toSDKHeader() } : [] ) - Task { await refreshDeploymentConfig() } + Task { + await handleTokenExpiry() + await refreshDeploymentConfig() + } } } From e04f61ae99667db24ab92e93ac140928f593e1ed Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Fri, 11 Apr 2025 12:30:45 +1000 Subject: [PATCH 04/10] thinking --- Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift | 11 +++++++---- Coder-Desktop/Coder-Desktop/VPN/VPNService.swift | 3 +++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift index cf0624e..369c48b 100644 --- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift +++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift @@ -41,10 +41,15 @@ class AppDelegate: NSObject, NSApplicationDelegate { override init() { vpn = CoderVPNService() - state = AppState(onChange: vpn.configureTunnelProviderProtocol) + let state = AppState(onChange: vpn.configureTunnelProviderProtocol) + vpn.onStart = { + // We don't need this to have finished before the VPN actually starts + Task { await state.refreshDeploymentConfig() } + } if state.startVPNOnLaunch { vpn.startWhenReady = true } + self.state = state vpn.installSystemExtension() #if arch(arm64) let mutagenBinary = "mutagen-darwin-arm64" @@ -65,12 +70,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { title: "Coder Desktop", image: "MenuBarIcon", onAppear: { - // If the VPN is enabled, it's likely the token hasn't expired, - // and the deployment config is up to date. + // If the VPN is enabled, it's likely the token isn't expired guard case .disabled = self.vpn.state, self.state.hasSession else { return } Task { @MainActor in await self.state.handleTokenExpiry() - await self.state.refreshDeploymentConfig() } }, content: { VPNMenu().frame(width: 256) diff --git a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift index 50078d5..f1b6ef5 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift @@ -76,6 +76,7 @@ final class CoderVPNService: NSObject, VPNService { // Whether the VPN should start as soon as possible var startWhenReady: Bool = false + var onStart: (() -> Void)? // 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 @@ -96,6 +97,8 @@ final class CoderVPNService: NSObject, VPNService { return } + onStart?() + menuState.clear() await startTunnel() logger.debug("network extension enabled") From cbd8ce499972dead256b9f7580bf10b7c820d31f Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Fri, 11 Apr 2025 15:21:37 +1000 Subject: [PATCH 05/10] move on start --- Coder-Desktop/Coder-Desktop/VPN/VPNService.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift index f1b6ef5..211bb28 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift @@ -97,8 +97,6 @@ final class CoderVPNService: NSObject, VPNService { return } - onStart?() - menuState.clear() await startTunnel() logger.debug("network extension enabled") @@ -185,8 +183,11 @@ extension CoderVPNService { // Connected -> Connected: no-op case (.connected, .connected): break - // Non-connecting -> Connecting: Establish XPC + // Non-connecting -> Connecting: + // - Establish XPC + // - Run `onStart` closure case (_, .connecting): + onStart?() xpc.connect() xpc.ping() tunnelState = .connecting From a354ebebc32e6704048bb67c81db552c19f251c9 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 14 Apr 2025 12:25:37 +1000 Subject: [PATCH 06/10] review --- Coder-Desktop/Coder-Desktop/State.swift | 13 +++------- .../Coder-Desktop/Views/VPN/VPNMenuItem.swift | 4 +-- Coder-Desktop/CoderSDK/Deployment.swift | 16 +----------- Coder-Desktop/CoderSDK/Util.swift | 25 +++++++++++++++++++ Coder-Desktop/CoderSDK/WorkspaceAgents.swift | 16 ++++++++++++ 5 files changed, 48 insertions(+), 26 deletions(-) create mode 100644 Coder-Desktop/CoderSDK/Util.swift create mode 100644 Coder-Desktop/CoderSDK/WorkspaceAgents.swift diff --git a/Coder-Desktop/Coder-Desktop/State.swift b/Coder-Desktop/Coder-Desktop/State.swift index fade6cc..b617940 100644 --- a/Coder-Desktop/Coder-Desktop/State.swift +++ b/Coder-Desktop/Coder-Desktop/State.swift @@ -157,15 +157,11 @@ class AppState: ObservableObject { public func refreshDeploymentConfig() async { if hasSession { - do { - let config = try await client!.sshConfiguration() - hostnameSuffix = config.hostname_suffix ?? Self.defaultHostnameSuffix - } catch { - // If fetching the config fails, there's likely a bigger issue. - // We'll show an error in the UI if they try and do something - logger.error("failed to refresh deployment config: \(error)") - return + let res = try? await retry(floor: .milliseconds(100), ceil: .seconds(10)) { + let config = try await client!.agentConnectionInfoGeneric() + return config.hostname_suffix } + hostnameSuffix = res ?? Self.defaultHostnameSuffix } } @@ -194,7 +190,6 @@ class AppState: ObservableObject { static let hasSession = "hasSession" static let baseAccessURL = "baseAccessURL" static let sessionToken = "sessionToken" - static let hostnameSuffix = "hostnameSuffix" static let useLiteralHeaders = "UseLiteralHeaders" static let literalHeaders = "LiteralHeaders" diff --git a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift index 804828a..0b231de 100644 --- a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift +++ b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift @@ -58,8 +58,8 @@ struct MenuItemView: View { var formattedName = AttributedString(name) formattedName.foregroundColor = .primary - if let lastDot = formattedName.range(of: ".", options: .backwards) { - formattedName[lastDot.lowerBound ..< formattedName.endIndex].foregroundColor = .secondary + if let range = formattedName.range(of: ".\(state.hostnameSuffix)", options: .backwards) { + formattedName[range].foregroundColor = .secondary } return formattedName } diff --git a/Coder-Desktop/CoderSDK/Deployment.swift b/Coder-Desktop/CoderSDK/Deployment.swift index cca6f04..d39a2a7 100644 --- a/Coder-Desktop/CoderSDK/Deployment.swift +++ b/Coder-Desktop/CoderSDK/Deployment.swift @@ -8,14 +8,6 @@ public extension Client { } return try decode(BuildInfoResponse.self, from: res.data) } - - func sshConfiguration() async throws(SDKError) -> SSHConfigResponse { - let res = try await request("/api/v2/deployment/ssh", method: .get) - guard res.resp.statusCode == 200 else { - throw responseAsError(res) - } - return try decode(SSHConfigResponse.self, from: res.data) - } } public struct BuildInfoResponse: Codable, Equatable, Sendable { @@ -27,10 +19,4 @@ public struct BuildInfoResponse: Codable, Equatable, Sendable { .firstMatch(in: version, range: NSRange(version.startIndex ..< version.endIndex, in: version)) .flatMap { Range($0.range(at: 1), in: version).map { String(version[$0]) } } } -} - -public struct SSHConfigResponse: Codable, Equatable, Sendable { - public let hostname_prefix: String? - public let hostname_suffix: String? - public let ssh_config_options: [String: String] -} +} \ No newline at end of file diff --git a/Coder-Desktop/CoderSDK/Util.swift b/Coder-Desktop/CoderSDK/Util.swift new file mode 100644 index 0000000..4eab2db --- /dev/null +++ b/Coder-Desktop/CoderSDK/Util.swift @@ -0,0 +1,25 @@ +import Foundation + +public func retry( + floor: Duration, + ceil: Duration, + rate: Double = 1.618, + operation: @Sendable () async throws -> T +) async throws -> T { + var delay = floor + + while !Task.isCancelled { + do { + return try await operation() + } catch let error as CancellationError { + throw error + } catch { + try Task.checkCancellation() + + delay = min(ceil, delay * rate) + try await Task.sleep(for: delay) + } + } + + throw CancellationError() +} diff --git a/Coder-Desktop/CoderSDK/WorkspaceAgents.swift b/Coder-Desktop/CoderSDK/WorkspaceAgents.swift new file mode 100644 index 0000000..3420af7 --- /dev/null +++ b/Coder-Desktop/CoderSDK/WorkspaceAgents.swift @@ -0,0 +1,16 @@ +import Foundation + + +public extension Client { + func agentConnectionInfoGeneric() async throws(SDKError) -> AgentConnectionInfo { + let res = try await request("/api/v2/workspaceagents/connection", method: .get) + guard res.resp.statusCode == 200 else { + throw responseAsError(res) + } + return try decode(AgentConnectionInfo.self, from: res.data) + } +} + +public struct AgentConnectionInfo: Codable, Sendable { + public let hostname_suffix: String? +} From 75f17caaac1786cfdca642d2205350a7ea820a7c Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 14 Apr 2025 12:26:46 +1000 Subject: [PATCH 07/10] fmt --- Coder-Desktop/CoderSDK/Deployment.swift | 2 +- Coder-Desktop/CoderSDK/WorkspaceAgents.swift | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Coder-Desktop/CoderSDK/Deployment.swift b/Coder-Desktop/CoderSDK/Deployment.swift index d39a2a7..b88029f 100644 --- a/Coder-Desktop/CoderSDK/Deployment.swift +++ b/Coder-Desktop/CoderSDK/Deployment.swift @@ -19,4 +19,4 @@ public struct BuildInfoResponse: Codable, Equatable, Sendable { .firstMatch(in: version, range: NSRange(version.startIndex ..< version.endIndex, in: version)) .flatMap { Range($0.range(at: 1), in: version).map { String(version[$0]) } } } -} \ No newline at end of file +} diff --git a/Coder-Desktop/CoderSDK/WorkspaceAgents.swift b/Coder-Desktop/CoderSDK/WorkspaceAgents.swift index 3420af7..4144a58 100644 --- a/Coder-Desktop/CoderSDK/WorkspaceAgents.swift +++ b/Coder-Desktop/CoderSDK/WorkspaceAgents.swift @@ -1,8 +1,7 @@ import Foundation - public extension Client { - func agentConnectionInfoGeneric() async throws(SDKError) -> AgentConnectionInfo { + func agentConnectionInfoGeneric() async throws(SDKError) -> AgentConnectionInfo { let res = try await request("/api/v2/workspaceagents/connection", method: .get) guard res.resp.statusCode == 200 else { throw responseAsError(res) From ba2d732bb092457795a82060cdd8276ffe5edda8 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 14 Apr 2025 18:02:59 +1000 Subject: [PATCH 08/10] fixup --- Coder-Desktop/Coder-Desktop/VPN/VPNService.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift index 211bb28..c3c1773 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift @@ -183,16 +183,16 @@ extension CoderVPNService { // Connected -> Connected: no-op case (.connected, .connected): break - // Non-connecting -> Connecting: - // - Establish XPC - // - Run `onStart` closure + // Non-connecting -> Connecting: Establish XPC case (_, .connecting): - onStart?() xpc.connect() xpc.ping() tunnelState = .connecting - // Non-connected -> Connected: Retrieve Peers + // Non-connected -> Connected: + // - Retrieve Peers + // - Run `onStart` closure case (_, .connected): + onStart?() xpc.connect() xpc.getPeerState() tunnelState = .connected From 26d2eb8169a967a445788ce71bd2efa916b3acf8 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 14 Apr 2025 21:17:25 +1000 Subject: [PATCH 09/10] single cancelable refresh task --- Coder-Desktop/Coder-Desktop/State.swift | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/State.swift b/Coder-Desktop/Coder-Desktop/State.swift index b617940..3c1d1d3 100644 --- a/Coder-Desktop/Coder-Desktop/State.swift +++ b/Coder-Desktop/Coder-Desktop/State.swift @@ -155,19 +155,28 @@ class AppState: ObservableObject { } } + private var refreshTask: Task? public func refreshDeploymentConfig() async { - if hasSession { - let res = try? await retry(floor: .milliseconds(100), ceil: .seconds(10)) { - let config = try await client!.agentConnectionInfoGeneric() - return config.hostname_suffix + // Client is non-nil if there's a sesssion + if hasSession, let client { + refreshTask?.cancel() + + refreshTask = Task { + let res = try? await retry(floor: .milliseconds(100), ceil: .seconds(10)) { + let config = try await client.agentConnectionInfoGeneric() + return config.hostname_suffix + } + return res } - hostnameSuffix = res ?? Self.defaultHostnameSuffix + + self.hostnameSuffix = await refreshTask?.value ?? Self.defaultHostnameSuffix } } public func clearSession() { hasSession = false sessionToken = nil + refreshTask?.cancel() client = nil reconfigure() } From b6218fc55d1f64ca8c7de849d6c410075fef2922 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 14 Apr 2025 21:21:49 +1000 Subject: [PATCH 10/10] logs --- Coder-Desktop/Coder-Desktop/State.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/State.swift b/Coder-Desktop/Coder-Desktop/State.swift index 3c1d1d3..3aa8842 100644 --- a/Coder-Desktop/Coder-Desktop/State.swift +++ b/Coder-Desktop/Coder-Desktop/State.swift @@ -163,13 +163,18 @@ class AppState: ObservableObject { refreshTask = Task { let res = try? await retry(floor: .milliseconds(100), ceil: .seconds(10)) { - let config = try await client.agentConnectionInfoGeneric() - return config.hostname_suffix + do { + let config = try await client.agentConnectionInfoGeneric() + return config.hostname_suffix + } catch { + logger.error("failed to get agent connection info (retrying): \(error)") + throw error + } } return res } - self.hostnameSuffix = await refreshTask?.value ?? Self.defaultHostnameSuffix + hostnameSuffix = await refreshTask?.value ?? Self.defaultHostnameSuffix } }