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/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 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 c83cc9d1..1743fab5 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 { @@ -107,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) { @@ -118,61 +125,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 } } } @@ -201,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 @@ -213,12 +233,16 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit return true } - override fun dispose() { - cs.cancel() + override fun onPrevious() { + super.onPrevious() + logger.info("Going back to Workspace view") + cs.launch { + ideResolvingJob.cancelAndJoin() + } } - companion object { - val logger = Logger.getInstance(CoderLocateRemoteProjectStepView::class.java.simpleName) + override fun dispose() { + cs.cancel() } private class IDEComboBox(model: ComboBoxModel) : ComboBox(model) { @@ -258,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 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