From 1051381d27f831d15673c8683678850f92e26bd6 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 1 Feb 2023 23:37:18 +0200 Subject: [PATCH 1/5] impl: remote path validation - every time text changes on the text field input a job to validate path on the remote host is queued - if path is invalid then an error hint is displayed - IDE combo box renderer is reset each time the IDE&Project panel is displayed - resolves #155 --- .../steps/CoderLocateRemoteProjectStepView.kt | 68 +++++++++++++++---- 1 file changed, 56 insertions(+), 12 deletions(-) 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 1743fab5..6b423c24 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -17,11 +17,15 @@ import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.ComponentValidator +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.util.Disposer import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager import com.intellij.remote.AuthType import com.intellij.remote.RemoteCredentialsHolder import com.intellij.ui.AnimatedIcon import com.intellij.ui.ColoredListCellRenderer +import com.intellij.ui.DocumentAdapter import com.intellij.ui.components.JBTextField import com.intellij.ui.dsl.builder.BottomGap import com.intellij.ui.dsl.builder.RowLayout @@ -30,6 +34,8 @@ import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.gridLayout.HorizontalAlign import com.intellij.util.ui.JBFont import com.intellij.util.ui.UIUtil +import com.intellij.util.ui.update.MergingUpdateQueue +import com.intellij.util.ui.update.Update import com.jetbrains.gateway.api.GatewayUI import com.jetbrains.gateway.ssh.CachingProductsJsonWrapper import com.jetbrains.gateway.ssh.DeployTargetOS @@ -47,6 +53,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.awt.Component import java.awt.FlowLayout @@ -58,6 +65,7 @@ import javax.swing.JList import javax.swing.JPanel import javax.swing.ListCellRenderer import javax.swing.SwingConstants +import javax.swing.event.DocumentEvent class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit) : CoderWorkspacesWizardStep, Disposable { private val cs = CoroutineScope(Dispatchers.Main) @@ -68,10 +76,10 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit private lateinit var titleLabel: JLabel private lateinit var wizard: CoderWorkspacesWizardModel private lateinit var cbIDE: IDEComboBox - private lateinit var tfProject: JBTextField + private var tfProject = JBTextField() private lateinit var terminalLink: LazyBrowserLink - private lateinit var ideResolvingJob: Job + private val pathValidationJobs = MergingUpdateQueue("remote-path-validation", 1000, true, tfProject) override val component = panel { indent { @@ -92,9 +100,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit row { label("Project directory:") - tfProject = textField() - .resizableColumn() - .horizontalAlign(HorizontalAlign.FILL).component + cell(tfProject).resizableColumn().horizontalAlign(HorizontalAlign.FILL).component cell() }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE).layout(RowLayout.PARENT_GRID) row { @@ -113,6 +119,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit override val nextActionText = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.next.text") override fun onInit(wizardModel: CoderWorkspacesWizardModel) { + cbIDE.renderer = IDECellRenderer() ideComboBoxModel.removeAllElements() wizard = wizardModel val selectedWorkspace = wizardModel.selectedWorkspace @@ -127,7 +134,11 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit ideResolvingJob = cs.launch { try { - retrieveIDES(selectedWorkspace) + val executor = withContext(Dispatchers.IO) { createRemoteExecutor() } + retrieveIDES(executor, selectedWorkspace) + if (ComponentValidator.getInstance(tfProject).isEmpty) { + installRemotePathValidator(executor) + } } catch (e: Exception) { when (e) { is InterruptedException -> Unit @@ -150,23 +161,56 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit } } - private suspend fun retrieveIDES(selectedWorkspace: WorkspaceAgentModel) { - logger.info("Retrieving available IDE's for ${selectedWorkspace.name} workspace...") - val hostAccessor = HighLevelHostAccessor.create( + private fun installRemotePathValidator(executor: HighLevelHostAccessor) { + var disposable = Disposer.newDisposable(ApplicationManager.getApplication(), CoderLocateRemoteProjectStepView.javaClass.name) + ComponentValidator(disposable).installOn(tfProject) + + tfProject.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(event: DocumentEvent) { + pathValidationJobs.queue(Update.create("validate-remote-path") { + runBlocking { + try { + val isPathPresent = executor.isPathPresentOnRemote(tfProject.text) + if (!isPathPresent) { + ComponentValidator.getInstance(tfProject).ifPresent { + it.updateInfo(ValidationInfo("Can't find directory: ${tfProject.text}", tfProject)) + } + } else { + ComponentValidator.getInstance(tfProject).ifPresent { + it.updateInfo(null) + } + } + } catch (e: Exception) { + ComponentValidator.getInstance(tfProject).ifPresent { + it.updateInfo(ValidationInfo("Can't validate directory: ${tfProject.text}", tfProject)) + } + } + } + }) + } + }) + } + + private suspend fun createRemoteExecutor(): HighLevelHostAccessor { + return HighLevelHostAccessor.create( RemoteCredentialsHolder().apply { - setHost("coder.${selectedWorkspace.name}") + setHost("coder.${wizard.selectedWorkspace?.name}") userName = "coder" authType = AuthType.OPEN_SSH }, true ) + } + + private suspend fun retrieveIDES(executor: HighLevelHostAccessor, selectedWorkspace: WorkspaceAgentModel) { + logger.info("Retrieving available IDE's for ${selectedWorkspace.name} workspace...") val workspaceOS = if (selectedWorkspace.agentOS != null && selectedWorkspace.agentArch != null) toDeployedOS(selectedWorkspace.agentOS, selectedWorkspace.agentArch) else withContext(Dispatchers.IO) { - hostAccessor.guessOs() + executor.guessOs() } 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) } + executor.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() From efefd3e598e80384655e55210716fd04fb9eda50 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 1 Feb 2023 23:42:45 +0200 Subject: [PATCH 2/5] fix: don't wait too long to connect to host - remote host connection handshake never finishes when Coder agent is down. - this fix caps the handshake to 30seconds --- .../steps/CoderLocateRemoteProjectStepView.kt | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 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 6b423c24..02847ed7 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -23,6 +23,7 @@ import com.intellij.openapi.util.Disposer import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager import com.intellij.remote.AuthType import com.intellij.remote.RemoteCredentialsHolder +import com.intellij.ssh.SshException import com.intellij.ui.AnimatedIcon import com.intellij.ui.ColoredListCellRenderer import com.intellij.ui.DocumentAdapter @@ -49,14 +50,17 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.time.withTimeout import kotlinx.coroutines.withContext import java.awt.Component import java.awt.FlowLayout +import java.time.Duration import java.util.Locale import javax.swing.ComboBoxModel import javax.swing.DefaultComboBoxModel @@ -134,7 +138,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit ideResolvingJob = cs.launch { try { - val executor = withContext(Dispatchers.IO) { createRemoteExecutor() } + val executor = withTimeout(Duration.ofSeconds(30)) { createRemoteExecutor() } retrieveIDES(executor, selectedWorkspace) if (ComponentValidator.getInstance(tfProject).isEmpty) { installRemotePathValidator(executor) @@ -143,6 +147,21 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit when (e) { is InterruptedException -> Unit is CancellationException -> Unit + is TimeoutCancellationException, + is SshException -> { + logger.error("Can't connect to 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("Can't connect to the workspace. Please make sure Coder Agent is running!") + } + } + } + } + else -> { logger.error("Could not resolve any IDE for workspace ${selectedWorkspace.name}. Reason: $e") withContext(Dispatchers.Main) { From 00f2035eef0838fb5568ed03c0efe749dc5427d2 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 1 Feb 2023 23:51:10 +0200 Subject: [PATCH 3/5] chore: simplify error message --- .../gateway/views/steps/CoderLocateRemoteProjectStepView.kt | 2 +- src/main/resources/messages/CoderGatewayBundle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 02847ed7..f3b5d1f9 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -170,7 +170,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit 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)) + append(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.ide.error.text")) } } } diff --git a/src/main/resources/messages/CoderGatewayBundle.properties b/src/main/resources/messages/CoderGatewayBundle.properties index f49a92bc..1d13858e 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. Please check the logs for more details! +gateway.connector.view.coder.remoteproject.ide.error.text=Could not retrieve any IDE 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 d796b03bbd315467401b864539360a94be8e8e46 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 1 Feb 2023 23:54:27 +0200 Subject: [PATCH 4/5] refactor: move error to message bundle --- .../gateway/views/steps/CoderLocateRemoteProjectStepView.kt | 2 +- src/main/resources/messages/CoderGatewayBundle.properties | 1 + 2 files changed, 2 insertions(+), 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 f3b5d1f9..af85f13e 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -156,7 +156,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit override fun customizeCellRenderer(list: JList, value: IdeWithStatus?, index: Int, isSelected: Boolean, cellHasFocus: Boolean) { background = UIUtil.getListBackground(isSelected, cellHasFocus) icon = UIUtil.getBalloonErrorIcon() - append("Can't connect to the workspace. Please make sure Coder Agent is running!") + append(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.ssh.error.text")) } } } diff --git a/src/main/resources/messages/CoderGatewayBundle.properties b/src/main/resources/messages/CoderGatewayBundle.properties index 1d13858e..2c255a90 100644 --- a/src/main/resources/messages/CoderGatewayBundle.properties +++ b/src/main/resources/messages/CoderGatewayBundle.properties @@ -21,6 +21,7 @@ gateway.connector.view.coder.workspaces.invalid.coder.version=Could not parse Co 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 because an error was encountered. Please check the logs for more details! +gateway.connector.view.coder.remoteproject.ssh.error.text=Can't connect to the workspace. Please make sure Coder Agent is running! 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 85e90da18beaa97bcecbb1028fb7001a0f879573 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 1 Feb 2023 23:55:24 +0200 Subject: [PATCH 5/5] fix: increase ssh connect timeout to 1 min --- .../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 af85f13e..4f05b6de 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -138,7 +138,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit ideResolvingJob = cs.launch { try { - val executor = withTimeout(Duration.ofSeconds(30)) { createRemoteExecutor() } + val executor = withTimeout(Duration.ofSeconds(60)) { createRemoteExecutor() } retrieveIDES(executor, selectedWorkspace) if (ComponentValidator.getInstance(tfProject).isEmpty) { installRemotePathValidator(executor)