Skip to content

Improve IDE resolving resiliency #156

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

## Unreleased

### Fixed
- improved resiliency and error handling when resolving installed IDE's

## 2.1.5 - 2023-01-24

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -37,4 +39,8 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
override fun isApplicable(parameters: Map<String, String>): Boolean {
return parameters.areCoderType()
}

companion object {
val logger = Logger.getInstance(CoderGatewayConnectionProvider::class.java.simpleName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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<IdeWithStatus>() {
override fun customizeCellRenderer(list: JList<out IdeWithStatus>, 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<IdeWithStatus>() {
override fun customizeCellRenderer(list: JList<out IdeWithStatus>, 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
}
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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<IdeWithStatus>) : ComboBox<IdeWithStatus>(model) {
Expand Down Expand Up @@ -258,4 +282,8 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
}
}
}

companion object {
val logger = Logger.getInstance(CoderLocateRemoteProjectStepView::class.java.simpleName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/messages/CoderGatewayBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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. <a href='https://coder.com/docs/coder-oss/latest/ides/gateway#creating-a-new-jetbrains-gateway-connection'>Connect to a Coder workspace manually</a>
gateway.connector.view.coder.workspaces.unsupported.coder.version=Coder version {0} might not be compatible with this plugin version. <a href='https://coder.com/docs/coder-oss/latest/ides/gateway#creating-a-new-jetbrains-gateway-connection'>Connect to a Coder workspace manually</a>
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
Expand Down