From f8baacfbf1f79241ccf2f9bac331938560be0fe5 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Fri, 27 Jan 2023 01:18:29 +0200 Subject: [PATCH 1/5] chore: increase version to 2.1.6 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2b52d8fe..8231f1b9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ pluginGroup=com.coder.gateway pluginName=coder-gateway # SemVer format -> https://semver.org -pluginVersion=2.1.5 +pluginVersion=2.1.6 # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html # for insight into build numbers and IntelliJ Platform versions. pluginSinceBuild=222.3739.54 From 4460d7d0d2362e274cc969938af0f3d23fddb3ec Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Fri, 27 Jan 2023 01:22:35 +0200 Subject: [PATCH 2/5] fix: improve resiliency when resolving IDEs - catch and treat exceptions - show a nice message instead of the progress icon telling user that IDEs could not be resolved when something bad happens - cancel any remaining job when hitting the Back button - resolves #153 --- CHANGELOG.md | 3 + .../steps/CoderLocateRemoteProjectStepView.kt | 125 +++++++++++------- .../messages/CoderGatewayBundle.properties | 2 +- 3 files changed, 79 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 516ae6e8..9ca935d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ## Unreleased +### Fixed +- improved resiliency and error handling when resolving installed IDE's + ## 2.1.5 - 2023-01-24 ### Fixed diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt index c83cc9d1..5b8118db 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -3,6 +3,7 @@ package com.coder.gateway.views.steps import com.coder.gateway.CoderGatewayBundle import com.coder.gateway.icons.CoderIcons import com.coder.gateway.models.CoderWorkspacesWizardModel +import com.coder.gateway.models.WorkspaceAgentModel import com.coder.gateway.sdk.Arch import com.coder.gateway.sdk.CoderRestClientService import com.coder.gateway.sdk.OS @@ -38,10 +39,13 @@ import com.jetbrains.gateway.ssh.HighLevelHostAccessor import com.jetbrains.gateway.ssh.IdeStatus import com.jetbrains.gateway.ssh.IdeWithStatus import com.jetbrains.gateway.ssh.IntelliJPlatformProduct +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.awt.Component @@ -67,6 +71,8 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit private lateinit var tfProject: JBTextField private lateinit var terminalLink: LazyBrowserLink + private lateinit var ideResolvingJob: Job + override val component = panel { indent { row { @@ -118,61 +124,73 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit titleLabel.text = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.choose.text", selectedWorkspace.name) terminalLink.url = "${coderClient.coderURL}/@${coderClient.me.username}/${selectedWorkspace.name}/terminal" - cs.launch { - logger.info("Retrieving available IDE's for ${selectedWorkspace.name} workspace...") - val hostAccessor = HighLevelHostAccessor.create( - RemoteCredentialsHolder().apply { - setHost("coder.${selectedWorkspace.name}") - userName = "coder" - authType = AuthType.OPEN_SSH - }, - true - ) - val workspaceOS = if (selectedWorkspace.agentOS != null && selectedWorkspace.agentArch != null) toDeployedOS(selectedWorkspace.agentOS, selectedWorkspace.agentArch) else withContext(Dispatchers.IO) { - try { - hostAccessor.guessOs() - } catch (e: Exception) { - logger.error("Could not resolve any IDE for workspace ${selectedWorkspace.name}. Reason: $e") - null - } - } - if (workspaceOS == null) { - disableNextAction() - cbIDE.renderer = object : ColoredListCellRenderer() { - override fun customizeCellRenderer(list: JList, value: IdeWithStatus?, index: Int, isSelected: Boolean, cellHasFocus: Boolean) { - background = UIUtil.getListBackground(isSelected, cellHasFocus) - icon = UIUtil.getBalloonErrorIcon() - append(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.ide.error.text", selectedWorkspace.name)) + ideResolvingJob = cs.launch { + try { + retrieveIDES(selectedWorkspace) + } catch (e: Exception) { + when(e) { + is InterruptedException -> Unit + is CancellationException -> Unit + else -> { + logger.error("Could not resolve any IDE for workspace ${selectedWorkspace.name}. Reason: $e") + withContext(Dispatchers.Main) { + disableNextAction() + cbIDE.renderer = object : ColoredListCellRenderer() { + override fun customizeCellRenderer(list: JList, value: IdeWithStatus?, index: Int, isSelected: Boolean, cellHasFocus: Boolean) { + background = UIUtil.getListBackground(isSelected, cellHasFocus) + icon = UIUtil.getBalloonErrorIcon() + append(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.ide.error.text", selectedWorkspace.name)) + } + } + } } } - } else { - logger.info("Resolved OS and Arch for ${selectedWorkspace.name} is: $workspaceOS") - val installedIdesJob = async(Dispatchers.IO) { - hostAccessor.getInstalledIDEs().map { ide -> IdeWithStatus(ide.product, ide.buildNumber, IdeStatus.ALREADY_INSTALLED, null, ide.pathToIde, ide.presentableVersion, ide.remoteDevType) } - } - val idesWithStatusJob = async(Dispatchers.IO) { - IntelliJPlatformProduct.values() - .filter { it.showInGateway } - .flatMap { CachingProductsJsonWrapper.getInstance().getAvailableIdes(it, workspaceOS) } - .map { ide -> IdeWithStatus(ide.product, ide.buildNumber, IdeStatus.DOWNLOAD, ide.download, null, ide.presentableVersion, ide.remoteDevType) } - } + } + } + } - val installedIdes = installedIdesJob.await() - val idesWithStatus = idesWithStatusJob.await() - if (installedIdes.isEmpty()) { - logger.info("No IDE is installed in workspace ${selectedWorkspace.name}") - } else { - ideComboBoxModel.addAll(installedIdes) - cbIDE.selectedIndex = 0 - } + private suspend fun retrieveIDES(selectedWorkspace: WorkspaceAgentModel) { + logger.info("Retrieving available IDE's for ${selectedWorkspace.name} workspace...") + val hostAccessor = HighLevelHostAccessor.create( + RemoteCredentialsHolder().apply { + setHost("coder.${selectedWorkspace.name}") + userName = "coder" + authType = AuthType.OPEN_SSH + }, + true + ) + val workspaceOS = if (selectedWorkspace.agentOS != null && selectedWorkspace.agentArch != null) toDeployedOS(selectedWorkspace.agentOS, selectedWorkspace.agentArch) else withContext(Dispatchers.IO) { + hostAccessor.guessOs() + } - if (idesWithStatus.isEmpty()) { - logger.warn("Could not resolve any IDE for workspace ${selectedWorkspace.name}, probably $workspaceOS is not supported by Gateway") - } else { + logger.info("Resolved OS and Arch for ${selectedWorkspace.name} is: $workspaceOS") + val installedIdesJob = cs.async(Dispatchers.IO) { + hostAccessor.getInstalledIDEs().map { ide -> IdeWithStatus(ide.product, ide.buildNumber, IdeStatus.ALREADY_INSTALLED, null, ide.pathToIde, ide.presentableVersion, ide.remoteDevType) } + } + val idesWithStatusJob = cs.async(Dispatchers.IO) { + IntelliJPlatformProduct.values() + .filter { it.showInGateway } + .flatMap { CachingProductsJsonWrapper.getInstance().getAvailableIdes(it, workspaceOS) } + .map { ide -> IdeWithStatus(ide.product, ide.buildNumber, IdeStatus.DOWNLOAD, ide.download, null, ide.presentableVersion, ide.remoteDevType) } + } - ideComboBoxModel.addAll(idesWithStatus) - cbIDE.selectedIndex = 0 - } + val installedIdes = installedIdesJob.await() + val idesWithStatus = idesWithStatusJob.await() + if (installedIdes.isEmpty()) { + logger.info("No IDE is installed in workspace ${selectedWorkspace.name}") + } else { + withContext(Dispatchers.Main) { + ideComboBoxModel.addAll(installedIdes) + cbIDE.selectedIndex = 0 + } + } + + if (idesWithStatus.isEmpty()) { + logger.warn("Could not resolve any IDE for workspace ${selectedWorkspace.name}, probably $workspaceOS is not supported by Gateway") + } else { + withContext(Dispatchers.Main) { + ideComboBoxModel.addAll(idesWithStatus) + cbIDE.selectedIndex = 0 } } } @@ -213,6 +231,13 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit return true } + override fun onPrevious() { + super.onPrevious() + cs.launch { + ideResolvingJob.cancelAndJoin() + } + } + override fun dispose() { cs.cancel() } diff --git a/src/main/resources/messages/CoderGatewayBundle.properties b/src/main/resources/messages/CoderGatewayBundle.properties index 784f84cb..f49a92bc 100644 --- a/src/main/resources/messages/CoderGatewayBundle.properties +++ b/src/main/resources/messages/CoderGatewayBundle.properties @@ -20,7 +20,7 @@ gateway.connector.view.coder.workspaces.unsupported.os.info=Gateway supports onl gateway.connector.view.coder.workspaces.invalid.coder.version=Could not parse Coder version {0}. Coder Gateway plugin might not be compatible with this version. Connect to a Coder workspace manually gateway.connector.view.coder.workspaces.unsupported.coder.version=Coder version {0} might not be compatible with this plugin version. Connect to a Coder workspace manually gateway.connector.view.coder.remoteproject.loading.text=Retrieving products... -gateway.connector.view.coder.remoteproject.ide.error.text=Could not retrieve any IDE for workspace {0} because an error was encountered +gateway.connector.view.coder.remoteproject.ide.error.text=Could not retrieve any IDE for workspace {0} because an error was encountered. Please check the logs for more details! gateway.connector.view.coder.remoteproject.next.text=Start IDE and connect gateway.connector.view.coder.remoteproject.choose.text=Choose IDE and project for workspace {0} gateway.connector.recentconnections.title=Recent Coder Workspaces From b93b408287863dcfb471f464ed8baecdd05c6058 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Fri, 27 Jan 2023 01:26:43 +0200 Subject: [PATCH 3/5] fix: don't fill IDE's dropdown with data from previous workspace - resolves #154 --- .../gateway/views/steps/CoderLocateRemoteProjectStepView.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt index 5b8118db..c46cfc9f 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -113,6 +113,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit override val nextActionText = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.next.text") override fun onInit(wizardModel: CoderWorkspacesWizardModel) { + ideComboBoxModel.removeAllElements() wizard = wizardModel val selectedWorkspace = wizardModel.selectedWorkspace if (selectedWorkspace == null) { From 9d8a6e127eee388346e79d5dc015cf7fe93efdba Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Fri, 27 Jan 2023 01:27:08 +0200 Subject: [PATCH 4/5] chore: linting --- .../gateway/views/steps/CoderLocateRemoteProjectStepView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt index c46cfc9f..0fa3d9b9 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -129,7 +129,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit try { retrieveIDES(selectedWorkspace) } catch (e: Exception) { - when(e) { + when (e) { is InterruptedException -> Unit is CancellationException -> Unit else -> { From 3ec4e105b964f0d95caff4f7e45af510d602c340 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Fri, 27 Jan 2023 21:28:48 +0200 Subject: [PATCH 5/5] chore: improve logging when user hits next/back buttons --- .../coder/gateway/CoderGatewayConnectionProvider.kt | 6 ++++++ .../views/steps/CoderLocateRemoteProjectStepView.kt | 10 ++++++---- .../gateway/views/steps/CoderWorkspacesStepView.kt | 2 ++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt index 9ca0d804..ec696796 100644 --- a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt +++ b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt @@ -4,6 +4,7 @@ package com.coder.gateway import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.rd.util.launchUnderBackgroundProgress import com.jetbrains.gateway.api.ConnectionRequestor import com.jetbrains.gateway.api.GatewayConnectionHandle @@ -22,6 +23,7 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider { val clientLifetime = LifetimeDefinition() clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.coder.connection.provider.title"), canBeCancelled = true, isIndeterminate = true, project = null) { val context = SshMultistagePanelContext(parameters.toHostDeployInputs()) + logger.info("Deploying and starting IDE with $context") launch { @Suppress("UnstableApiUsage") SshDeployFlowUtil.fullDeployCycle( clientLifetime, context, Duration.ofMinutes(10) @@ -37,4 +39,8 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider { override fun isApplicable(parameters: Map): Boolean { return parameters.areCoderType() } + + companion object { + val logger = Logger.getInstance(CoderGatewayConnectionProvider::class.java.simpleName) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt index 0fa3d9b9..1743fab5 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -220,6 +220,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit override fun onNext(wizardModel: CoderWorkspacesWizardModel): Boolean { val selectedIDE = cbIDE.selectedItem ?: return false + logger.info("Going to launch the IDE") cs.launch { GatewayUI.getInstance().connect( selectedIDE @@ -234,6 +235,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit override fun onPrevious() { super.onPrevious() + logger.info("Going back to Workspace view") cs.launch { ideResolvingJob.cancelAndJoin() } @@ -243,10 +245,6 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit cs.cancel() } - companion object { - val logger = Logger.getInstance(CoderLocateRemoteProjectStepView::class.java.simpleName) - } - private class IDEComboBox(model: ComboBoxModel) : ComboBox(model) { init { @@ -284,4 +282,8 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit } } } + + companion object { + val logger = Logger.getInstance(CoderLocateRemoteProjectStepView::class.java.simpleName) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt index 5d186741..163edb0e 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt @@ -589,6 +589,7 @@ class CoderWorkspacesStepView(val enableNextButtonCallback: (Boolean) -> Unit) : override fun onPrevious() { super.onPrevious() + logger.info("Going back to the main view") poller?.cancel() } @@ -621,6 +622,7 @@ class CoderWorkspacesStepView(val enableNextButtonCallback: (Boolean) -> Unit) : if (workspace != null) { wizardModel.selectedWorkspace = workspace poller?.cancel() + logger.info("Opening IDE and Project Location window for ${workspace.name}") return true } return false