From 42dd7c684de73a502c5550dd51fd1b782adb1dae Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 27 Aug 2025 01:29:49 +0300 Subject: [PATCH 1/8] impl: improve coroutine debugging with named contexts This commit add descriptive names to launched coroutines for better stack traces. --- CHANGELOG.md | 4 + .../coder/toolbox/CoderRemoteEnvironment.kt | 25 +-- .../com/coder/toolbox/CoderRemoteProvider.kt | 186 +++++++++--------- .../toolbox/util/CoderProtocolHandler.kt | 12 +- .../toolbox/views/CoderCliSetupWizardPage.kt | 3 +- .../coder/toolbox/views/CoderSettingsPage.kt | 5 +- .../com/coder/toolbox/views/ConnectStep.kt | 3 +- 7 files changed, 125 insertions(+), 113 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66990d7..826e038 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- improved diagnose support + ## 0.6.3 - 2025-08-25 ### Added diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index f8b3a17..d4531a6 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -24,6 +24,7 @@ import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentDescription import com.jetbrains.toolbox.api.remoteDev.states.RemoteEnvironmentState import com.jetbrains.toolbox.api.ui.actions.ActionDescription import com.squareup.moshi.Moshi +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -81,7 +82,7 @@ class CoderRemoteEnvironment( val actions = mutableListOf() if (wsRawStatus.canStop()) { actions.add(Action(context.i18n.ptrl("Open web terminal")) { - context.cs.launch { + context.cs.launch(CoroutineName("Open Web Terminal Action")) { context.desktop.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) { context.ui.showErrorInfoPopup(it) } @@ -90,7 +91,7 @@ class CoderRemoteEnvironment( } actions.add( Action(context.i18n.ptrl("Open in dashboard")) { - context.cs.launch { + context.cs.launch(CoroutineName("Open in Dashboard Action")) { context.desktop.browse( client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString() ) { @@ -100,7 +101,7 @@ class CoderRemoteEnvironment( }) actions.add(Action(context.i18n.ptrl("View template")) { - context.cs.launch { + context.cs.launch(CoroutineName("View Template Action")) { context.desktop.browse(client.url.withPath("/templates/${workspace.templateName}").toString()) { context.ui.showErrorInfoPopup(it) } @@ -110,14 +111,14 @@ class CoderRemoteEnvironment( if (wsRawStatus.canStart()) { if (workspace.outdated) { actions.add(Action(context.i18n.ptrl("Update and start")) { - context.cs.launch { + context.cs.launch(CoroutineName("Update and Start Action")) { val build = client.updateWorkspace(workspace) update(workspace.copy(latestBuild = build), agent) } }) } else { actions.add(Action(context.i18n.ptrl("Start")) { - context.cs.launch { + context.cs.launch(CoroutineName("Start Action")) { val build = client.startWorkspace(workspace) update(workspace.copy(latestBuild = build), agent) @@ -128,14 +129,14 @@ class CoderRemoteEnvironment( if (wsRawStatus.canStop()) { if (workspace.outdated) { actions.add(Action(context.i18n.ptrl("Update and restart")) { - context.cs.launch { + context.cs.launch(CoroutineName("Update and Restart Action")) { val build = client.updateWorkspace(workspace) update(workspace.copy(latestBuild = build), agent) } }) } actions.add(Action(context.i18n.ptrl("Stop")) { - context.cs.launch { + context.cs.launch(CoroutineName("Stop Action")) { tryStopSshConnection() val build = client.stopWorkspace(workspace) @@ -169,7 +170,7 @@ class CoderRemoteEnvironment( pollJob = pollNetworkMetrics() } - private fun pollNetworkMetrics(): Job = context.cs.launch { + private fun pollNetworkMetrics(): Job = context.cs.launch(CoroutineName("Network Metrics Poller")) { context.logger.info("Starting the network metrics poll job for $id") while (isActive) { context.logger.debug("Searching SSH command's PID for workspace $id...") @@ -227,7 +228,7 @@ class CoderRemoteEnvironment( actionsList.update { getAvailableActions() } - context.cs.launch { + context.cs.launch(CoroutineName("Workspace Status Updater")) { state.update { wsRawStatus.toRemoteEnvironmentState(context) } @@ -262,7 +263,7 @@ class CoderRemoteEnvironment( */ fun startSshConnection(): Boolean { if (wsRawStatus.ready() && !isConnected.value) { - context.cs.launch { + context.cs.launch(CoroutineName("SSH Connection Trigger")) { connectionRequest.update { true } @@ -284,7 +285,7 @@ class CoderRemoteEnvironment( } override val deleteActionFlow: StateFlow<(() -> Unit)?> = MutableStateFlow { - context.cs.launch { + context.cs.launch(CoroutineName("Delete Workspace Action")) { try { client.removeWorkspace(workspace) // mark the env as deleting otherwise we will have to @@ -293,7 +294,7 @@ class CoderRemoteEnvironment( WorkspaceAndAgentStatus.DELETING.toRemoteEnvironmentState(context) } - context.cs.launch { + context.cs.launch(CoroutineName("Workspace Deletion Poller")) { withTimeout(5.minutes) { var workspaceStillExists = true while (context.cs.isActive && workspaceStillExists) { diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index eb9997e..215e572 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -26,6 +26,7 @@ import com.jetbrains.toolbox.api.remoteDev.RemoteProvider import com.jetbrains.toolbox.api.ui.actions.ActionDelimiter import com.jetbrains.toolbox.api.ui.actions.ActionDescription import com.jetbrains.toolbox.api.ui.components.UiPage +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel @@ -87,113 +88,114 @@ class CoderRemoteProvider( * workspace is added, reconfigure SSH using the provided cli (including the * first time). */ - private fun poll(client: CoderRestClient, cli: CoderCLIManager): Job = context.cs.launch { - var lastPollTime = TimeSource.Monotonic.markNow() - while (isActive) { - try { - context.logger.debug("Fetching workspace agents from ${client.url}") - val resolvedEnvironments = client.workspaces().flatMap { ws -> - // Agents are not included in workspaces that are off - // so fetch them separately. - when (ws.latestBuild.status) { - WorkspaceStatus.RUNNING -> ws.latestBuild.resources - else -> emptyList() - }.ifEmpty { - client.resources(ws) - }.flatMap { resource -> - resource.agents?.distinctBy { - // There can be duplicates with coder_agent_instance. - // TODO: Can we just choose one or do they hold - // different information? - it.name - }?.map { agent -> - // If we have an environment already, update that. - val env = CoderRemoteEnvironment(context, client, cli, ws, agent) - lastEnvironments.firstOrNull { it == env }?.let { - it.update(ws, agent) - it - } ?: env - } ?: emptyList() - } - }.toSet() + private fun poll(client: CoderRestClient, cli: CoderCLIManager): Job = + context.cs.launch(CoroutineName("Workspace Poller")) { + var lastPollTime = TimeSource.Monotonic.markNow() + while (isActive) { + try { + context.logger.debug("Fetching workspace agents from ${client.url}") + val resolvedEnvironments = client.workspaces().flatMap { ws -> + // Agents are not included in workspaces that are off + // so fetch them separately. + when (ws.latestBuild.status) { + WorkspaceStatus.RUNNING -> ws.latestBuild.resources + else -> emptyList() + }.ifEmpty { + client.resources(ws) + }.flatMap { resource -> + resource.agents?.distinctBy { + // There can be duplicates with coder_agent_instance. + // TODO: Can we just choose one or do they hold + // different information? + it.name + }?.map { agent -> + // If we have an environment already, update that. + val env = CoderRemoteEnvironment(context, client, cli, ws, agent) + lastEnvironments.firstOrNull { it == env }?.let { + it.update(ws, agent) + it + } ?: env + } ?: emptyList() + } + }.toSet() - // In case we logged out while running the query. - if (!isActive) { - return@launch - } + // In case we logged out while running the query. + if (!isActive) { + return@launch + } - // Reconfigure if environments changed. - if (lastEnvironments.size != resolvedEnvironments.size || lastEnvironments != resolvedEnvironments) { - context.logger.info("Workspaces have changed, reconfiguring CLI: $resolvedEnvironments") - cli.configSsh(resolvedEnvironments.map { it.asPairOfWorkspaceAndAgent() }.toSet()) - } + // Reconfigure if environments changed. + if (lastEnvironments.size != resolvedEnvironments.size || lastEnvironments != resolvedEnvironments) { + context.logger.info("Workspaces have changed, reconfiguring CLI: $resolvedEnvironments") + cli.configSsh(resolvedEnvironments.map { it.asPairOfWorkspaceAndAgent() }.toSet()) + } - environments.update { - LoadableState.Value(resolvedEnvironments.toList()) - } - if (!isInitialized.value) { - context.logger.info("Environments for ${client.url} are now initialized") - isInitialized.update { - true + environments.update { + LoadableState.Value(resolvedEnvironments.toList()) + } + if (!isInitialized.value) { + context.logger.info("Environments for ${client.url} are now initialized") + isInitialized.update { + true + } + } + lastEnvironments.apply { + clear() + addAll(resolvedEnvironments.sortedBy { it.id }) } - } - lastEnvironments.apply { - clear() - addAll(resolvedEnvironments.sortedBy { it.id }) - } - if (WorkspaceConnectionManager.shouldEstablishWorkspaceConnections) { - WorkspaceConnectionManager.allConnected().forEach { wsId -> - val env = lastEnvironments.firstOrNull() { it.id == wsId } - if (env != null && !env.isConnected()) { - context.logger.info("Establishing lost SSH connection for workspace with id $wsId") - if (!env.startSshConnection()) { - context.logger.info("Can't establish lost SSH connection for workspace with id $wsId") + if (WorkspaceConnectionManager.shouldEstablishWorkspaceConnections) { + WorkspaceConnectionManager.allConnected().forEach { wsId -> + val env = lastEnvironments.firstOrNull() { it.id == wsId } + if (env != null && !env.isConnected()) { + context.logger.info("Establishing lost SSH connection for workspace with id $wsId") + if (!env.startSshConnection()) { + context.logger.info("Can't establish lost SSH connection for workspace with id $wsId") + } } } + WorkspaceConnectionManager.reset() } - WorkspaceConnectionManager.reset() - } - WorkspaceConnectionManager.collectStatuses(lastEnvironments) - } catch (_: CancellationException) { - context.logger.debug("${client.url} polling loop canceled") - break - } catch (ex: Exception) { - val elapsed = lastPollTime.elapsedNow() - if (elapsed > POLL_INTERVAL * 2) { - context.logger.info("wake-up from an OS sleep was detected") - } else { - context.logger.error(ex, "workspace polling error encountered") - if (ex is APIResponseException && ex.isTokenExpired) { - WorkspaceConnectionManager.shouldEstablishWorkspaceConnections = true - close() - context.envPageManager.showPluginEnvironmentsPage() - errorBuffer.add(ex) - break + WorkspaceConnectionManager.collectStatuses(lastEnvironments) + } catch (_: CancellationException) { + context.logger.debug("${client.url} polling loop canceled") + break + } catch (ex: Exception) { + val elapsed = lastPollTime.elapsedNow() + if (elapsed > POLL_INTERVAL * 2) { + context.logger.info("wake-up from an OS sleep was detected") + } else { + context.logger.error(ex, "workspace polling error encountered") + if (ex is APIResponseException && ex.isTokenExpired) { + WorkspaceConnectionManager.shouldEstablishWorkspaceConnections = true + close() + context.envPageManager.showPluginEnvironmentsPage() + errorBuffer.add(ex) + break + } } } - } - select { - onTimeout(POLL_INTERVAL) { - context.logger.debug("workspace poller waked up by the $POLL_INTERVAL timeout") - } - triggerSshConfig.onReceive { shouldTrigger -> - if (shouldTrigger) { - context.logger.debug("workspace poller waked up because it should reconfigure the ssh configurations") - cli.configSsh(lastEnvironments.map { it.asPairOfWorkspaceAndAgent() }.toSet()) + select { + onTimeout(POLL_INTERVAL) { + context.logger.debug("workspace poller waked up by the $POLL_INTERVAL timeout") } - } - triggerProviderVisible.onReceive { isCoderProviderVisible -> - if (isCoderProviderVisible) { - context.logger.debug("workspace poller waked up by Coder Toolbox which is currently visible, fetching latest workspace statuses") + triggerSshConfig.onReceive { shouldTrigger -> + if (shouldTrigger) { + context.logger.debug("workspace poller waked up because it should reconfigure the ssh configurations") + cli.configSsh(lastEnvironments.map { it.asPairOfWorkspaceAndAgent() }.toSet()) + } + } + triggerProviderVisible.onReceive { isCoderProviderVisible -> + if (isCoderProviderVisible) { + context.logger.debug("workspace poller waked up by Coder Toolbox which is currently visible, fetching latest workspace statuses") + } } } + lastPollTime = TimeSource.Monotonic.markNow() } - lastPollTime = TimeSource.Monotonic.markNow() } - } /** * Stop polling, clear the client and environments, then go back to the @@ -221,7 +223,7 @@ class CoderRemoteProvider( override val additionalPluginActions: StateFlow> = MutableStateFlow( listOf( Action(context.i18n.ptrl("Create workspace")) { - context.cs.launch { + context.cs.launch(CoroutineName("Create Workspace Action")) { context.desktop.browse(client?.url?.withPath("/templates").toString()) { context.ui.showErrorInfoPopup(it) } @@ -299,7 +301,7 @@ class CoderRemoteProvider( visibility } if (visibility.providerVisible) { - context.cs.launch { + context.cs.launch(CoroutineName("Notify Plugin Visibility")) { triggerProviderVisible.send(true) } } diff --git a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt index 76877cf..a4c0b48 100644 --- a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt +++ b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt @@ -17,6 +17,7 @@ import com.coder.toolbox.views.state.CoderCliSetupWizardState import com.coder.toolbox.views.state.WizardStep import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Job import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.delay @@ -354,7 +355,7 @@ open class CoderProtocolHandler( buildNumber: String, projectFolder: String? ) { - context.cs.launch { + context.cs.launch(CoroutineName("Launch Remote IDE")) { val selectedIde = selectAndInstallRemoteIde(productCode, buildNumber, environmentId) ?: return@launch context.logger.info("$productCode-$buildNumber is already on $environmentId. Going to launch JBClient") installJBClient(selectedIde, environmentId).join() @@ -422,10 +423,11 @@ open class CoderProtocolHandler( return "$productCode-$buildNumber" } - private fun installJBClient(selectedIde: String, environmentId: String): Job = context.cs.launch { - context.logger.info("Downloading and installing JBClient counterpart to $selectedIde locally") - context.jbClientOrchestrator.prepareClient(environmentId, selectedIde) - } + private fun installJBClient(selectedIde: String, environmentId: String): Job = + context.cs.launch(CoroutineName("JBClient Installer")) { + context.logger.info("Downloading and installing JBClient counterpart to $selectedIde locally") + context.jbClientOrchestrator.prepareClient(environmentId, selectedIde) + } private fun launchJBClient(selectedIde: String, environmentId: String, projectFolder: String?) { context.logger.info("Launching $selectedIde on $environmentId") diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt index 1e6b152..7ff36bc 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt @@ -9,6 +9,7 @@ import com.coder.toolbox.views.state.WizardStep import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription import com.jetbrains.toolbox.api.ui.components.UiField +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -140,7 +141,7 @@ class CoderCliSetupWizardPage( } else ex.message } else ex.message - context.cs.launch { + context.cs.launch(CoroutineName("Coder Setup Visual Error Reporting")) { context.ui.showSnackbar( UUID.randomUUID().toString(), context.i18n.ptrl("Error encountered while setting up Coder"), diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt index e937600..3444683 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt @@ -12,6 +12,7 @@ import com.jetbrains.toolbox.api.ui.components.ComboBoxField.LabelledValue import com.jetbrains.toolbox.api.ui.components.TextField import com.jetbrains.toolbox.api.ui.components.TextType import com.jetbrains.toolbox.api.ui.components.UiField +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ClosedSendChannelException @@ -134,7 +135,7 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf context.settingsStore.updateEnableSshWildcardConfig(enableSshWildCardConfig.checkedState.value) if (enableSshWildCardConfig.checkedState.value != oldIsSshWildcardConfigEnabled) { - context.cs.launch { + context.cs.launch(CoroutineName("SSH Wildcard Setting")) { try { triggerSshConfig.send(true) context.logger.info("Wildcard settings have been modified from $oldIsSshWildcardConfigEnabled to ${!oldIsSshWildcardConfigEnabled}, ssh config is going to be regenerated...") @@ -211,7 +212,7 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf settings.networkInfoDir } - visibilityUpdateJob = context.cs.launch { + visibilityUpdateJob = context.cs.launch(CoroutineName("Signature Verification Fallback Setting")) { disableSignatureVerificationField.checkedState.collect { state -> signatureFallbackStrategyField.visibility.update { // the fallback checkbox should not be visible diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt index 40db0cb..41c4c25 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt @@ -10,6 +10,7 @@ import com.coder.toolbox.views.state.CoderCliSetupWizardState import com.jetbrains.toolbox.api.ui.components.LabelField import com.jetbrains.toolbox.api.ui.components.RowGroup import com.jetbrains.toolbox.api.ui.components.ValidationErrorField +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -73,7 +74,7 @@ class ConnectStep( return } signInJob?.cancel() - signInJob = context.cs.launch { + signInJob = context.cs.launch(CoroutineName("Http and CLI Setup")) { try { val client = CoderRestClient( context, From 93ab80eee79ba3c1852116a66a2d12a8e12d5b12 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 27 Aug 2025 01:39:20 +0300 Subject: [PATCH 2/8] impl: log when ssh config is done SSH config manipulation is a heavy operation that involves disk writing and can take long time. Logs are added so that we can easily identify when the ssh configuration writing is done. --- src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt b/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt index 67947c3..9b058e5 100644 --- a/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt +++ b/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt @@ -315,6 +315,7 @@ class CoderCLIManager( ) { context.logger.info("Configuring SSH config at ${context.settingsStore.sshConfigPath}") writeSSHConfig(modifySSHConfig(readSSHConfig(), wsWithAgents, feats)) + context.logger.info("Finished configuring SSH config") } /** From 03e2cc3b8a24028d96fc5dfd7b8eab23e90b2899 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 27 Aug 2025 01:43:32 +0300 Subject: [PATCH 3/8] impl: log plugin setup progress During plugin initialization we have a couple of steps that are only reported in the screen. If there is any error happening in between the steps, it is hard to identify between which steps things went wrong without a screenshots provided by the user. --- .../kotlin/com/coder/toolbox/views/ConnectStep.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt index 41c4c25..b97079c 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt @@ -85,7 +85,7 @@ class ConnectStep( // allows interleaving with the back/cancel action yield() client.initializeSession() - statusField.textState.update { (context.i18n.ptrl("Checking Coder CLI...")) } + logAndReportProgress("Checking Coder CLI...") val cli = ensureCLI( context, client.url, client.buildVersion @@ -94,12 +94,12 @@ class ConnectStep( } // We only need to log in if we are using token-based auth. if (context.settingsStore.requireTokenAuth) { - statusField.textState.update { (context.i18n.ptrl("Configuring Coder CLI...")) } + logAndReportProgress("Configuring Coder CLI...") // allows interleaving with the back/cancel action yield() cli.login(client.token!!) } - statusField.textState.update { (context.i18n.ptrl("Successfully configured ${CoderCliSetupContext.url!!.host}...")) } + logAndReportProgress("Successfully configured ${CoderCliSetupContext.url!!.host}...") // allows interleaving with the back/cancel action yield() CoderCliSetupContext.reset() @@ -119,6 +119,11 @@ class ConnectStep( } } + private fun logAndReportProgress(msg: String) { + context.logger.info(msg) + statusField.textState.update { context.i18n.pnotr(msg) } + } + override fun onNext(): Boolean { return false } From 836a74b61425e7febd9371922ba80e7f7cc500ab Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 27 Aug 2025 01:53:30 +0300 Subject: [PATCH 4/8] impl: improve logging during connection setup --- src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt | 4 ++++ src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index 215e572..a7a63c1 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -398,11 +398,15 @@ class CoderRemoteProvider( context.secrets.lastDeploymentURL = client.url.toString() context.secrets.lastToken = client.token ?: "" context.secrets.storeTokenFor(client.url, context.secrets.lastToken) + context.logger.info("Deployment URL and token were stored and will be available for automatic connection") this.client = client pollJob?.cancel() + context.logger.info("Previous poll job was canceled") environments.showLoadingMessage() coderHeaderPage.setTitle(context.i18n.pnotr(client.url.toString())) + context.logger.info("Displaying ${client.url} in the UI") pollJob = poll(client, cli) + context.logger.info("Workspace poller job created with reference $pollJob") context.envPageManager.showPluginEnvironmentsPage() } diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt index b97079c..7c0ff2d 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt @@ -76,6 +76,7 @@ class ConnectStep( signInJob?.cancel() signInJob = context.cs.launch(CoroutineName("Http and CLI Setup")) { try { + context.logger.info("Setting up the HTTP client...") val client = CoderRestClient( context, CoderCliSetupContext.url!!, @@ -104,6 +105,7 @@ class ConnectStep( yield() CoderCliSetupContext.reset() CoderCliSetupWizardState.goToFirstStep() + context.logger.info("Connection setup done, initializing the workspace poller...") onConnect(client, cli) } catch (ex: CancellationException) { if (ex.message != USER_HIT_THE_BACK_BUTTON) { @@ -130,6 +132,7 @@ class ConnectStep( override fun onBack() { try { + context.logger.info("Back button was pressed, cancelling in-progress connection setup...") signInJob?.cancel(CancellationException(USER_HIT_THE_BACK_BUTTON)) } finally { if (shouldAutoLogin.value) { From 7052fd882fe42f053a6cd55696e9d231c6f6a2f6 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 27 Aug 2025 01:54:00 +0300 Subject: [PATCH 5/8] chore: next version 0.6.4 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b10e0c2..3b06eb4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=0.6.3 +version=0.6.4 group=com.coder.toolbox name=coder-toolbox \ No newline at end of file From 5f73723ae7f43109456b28e2a9c1a01760b3524e Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 27 Aug 2025 22:45:20 +0300 Subject: [PATCH 6/8] chore: import coroutines alias for cancellation exception --- src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt index 7c0ff2d..abb9007 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt @@ -10,13 +10,13 @@ import com.coder.toolbox.views.state.CoderCliSetupWizardState import com.jetbrains.toolbox.api.ui.components.LabelField import com.jetbrains.toolbox.api.ui.components.RowGroup import com.jetbrains.toolbox.api.ui.components.ValidationErrorField +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.yield -import java.util.concurrent.CancellationException private const val USER_HIT_THE_BACK_BUTTON = "User hit the back button" From 0df8c5930c98559df6dd82fb87fd2f3c0e844e87 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 28 Aug 2025 00:40:39 +0300 Subject: [PATCH 7/8] fix: rework the exception handler for the connection step Calling onBack() in the exception handler doesn't make sense because: - the coroutine is already interrupted by the exception - onBack() is meant to handle user-initiated back navigation, not error recovery - we still want the proper navigation logic but without the coroutine cancellation part --- .../com/coder/toolbox/views/ConnectStep.kt | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt index abb9007..f005b53 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt @@ -110,12 +110,12 @@ class ConnectStep( } catch (ex: CancellationException) { if (ex.message != USER_HIT_THE_BACK_BUTTON) { notify("Connection to ${CoderCliSetupContext.url!!.host} was configured", ex) - onBack() + handleNavigation() refreshWizard() } } catch (ex: Exception) { notify("Failed to configure ${CoderCliSetupContext.url!!.host}", ex) - onBack() + handleNavigation() refreshWizard() } } @@ -126,6 +126,26 @@ class ConnectStep( statusField.textState.update { context.i18n.pnotr(msg) } } + /** + * Handle navigation logic for both errors and back button + */ + private fun handleNavigation() { + if (shouldAutoLogin.value) { + CoderCliSetupContext.reset() + if (jumpToMainPageOnError) { + context.popupPluginMainPage() + } else { + CoderCliSetupWizardState.goToFirstStep() + } + } else { + if (context.settingsStore.requireTokenAuth) { + CoderCliSetupWizardState.goToPreviousStep() + } else { + CoderCliSetupWizardState.goToFirstStep() + } + } + } + override fun onNext(): Boolean { return false } @@ -135,20 +155,7 @@ class ConnectStep( context.logger.info("Back button was pressed, cancelling in-progress connection setup...") signInJob?.cancel(CancellationException(USER_HIT_THE_BACK_BUTTON)) } finally { - if (shouldAutoLogin.value) { - CoderCliSetupContext.reset() - if (jumpToMainPageOnError) { - context.popupPluginMainPage() - } else { - CoderCliSetupWizardState.goToFirstStep() - } - } else { - if (context.settingsStore.requireTokenAuth) { - CoderCliSetupWizardState.goToPreviousStep() - } else { - CoderCliSetupWizardState.goToFirstStep() - } - } + handleNavigation() } } } From cf2f5bcbb8608c2a1cdb9f4feb56b84946cffb77 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 28 Aug 2025 23:00:28 +0300 Subject: [PATCH 8/8] chore: improved logging around poller canceling --- src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index a7a63c1..cf9e04e 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -400,13 +400,15 @@ class CoderRemoteProvider( context.secrets.storeTokenFor(client.url, context.secrets.lastToken) context.logger.info("Deployment URL and token were stored and will be available for automatic connection") this.client = client - pollJob?.cancel() - context.logger.info("Previous poll job was canceled") + pollJob?.let { + it.cancel() + context.logger.info("Workspace poll job with reference ${pollJob} was canceled") + } environments.showLoadingMessage() coderHeaderPage.setTitle(context.i18n.pnotr(client.url.toString())) context.logger.info("Displaying ${client.url} in the UI") pollJob = poll(client, cli) - context.logger.info("Workspace poller job created with reference $pollJob") + context.logger.info("Workspace poll job created with reference $pollJob") context.envPageManager.showPluginEnvironmentsPage() }