Skip to content

Commit 4460d7d

Browse files
committed
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
1 parent f8baacf commit 4460d7d

File tree

3 files changed

+79
-51
lines changed

3 files changed

+79
-51
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
## Unreleased
66

7+
### Fixed
8+
- improved resiliency and error handling when resolving installed IDE's
9+
710
## 2.1.5 - 2023-01-24
811

912
### Fixed

src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt

Lines changed: 75 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.coder.gateway.views.steps
33
import com.coder.gateway.CoderGatewayBundle
44
import com.coder.gateway.icons.CoderIcons
55
import com.coder.gateway.models.CoderWorkspacesWizardModel
6+
import com.coder.gateway.models.WorkspaceAgentModel
67
import com.coder.gateway.sdk.Arch
78
import com.coder.gateway.sdk.CoderRestClientService
89
import com.coder.gateway.sdk.OS
@@ -38,10 +39,13 @@ import com.jetbrains.gateway.ssh.HighLevelHostAccessor
3839
import com.jetbrains.gateway.ssh.IdeStatus
3940
import com.jetbrains.gateway.ssh.IdeWithStatus
4041
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
42+
import kotlinx.coroutines.CancellationException
4143
import kotlinx.coroutines.CoroutineScope
4244
import kotlinx.coroutines.Dispatchers
45+
import kotlinx.coroutines.Job
4346
import kotlinx.coroutines.async
4447
import kotlinx.coroutines.cancel
48+
import kotlinx.coroutines.cancelAndJoin
4549
import kotlinx.coroutines.launch
4650
import kotlinx.coroutines.withContext
4751
import java.awt.Component
@@ -67,6 +71,8 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
6771
private lateinit var tfProject: JBTextField
6872
private lateinit var terminalLink: LazyBrowserLink
6973

74+
private lateinit var ideResolvingJob: Job
75+
7076
override val component = panel {
7177
indent {
7278
row {
@@ -118,61 +124,73 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
118124
titleLabel.text = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.choose.text", selectedWorkspace.name)
119125
terminalLink.url = "${coderClient.coderURL}/@${coderClient.me.username}/${selectedWorkspace.name}/terminal"
120126

121-
cs.launch {
122-
logger.info("Retrieving available IDE's for ${selectedWorkspace.name} workspace...")
123-
val hostAccessor = HighLevelHostAccessor.create(
124-
RemoteCredentialsHolder().apply {
125-
setHost("coder.${selectedWorkspace.name}")
126-
userName = "coder"
127-
authType = AuthType.OPEN_SSH
128-
},
129-
true
130-
)
131-
val workspaceOS = if (selectedWorkspace.agentOS != null && selectedWorkspace.agentArch != null) toDeployedOS(selectedWorkspace.agentOS, selectedWorkspace.agentArch) else withContext(Dispatchers.IO) {
132-
try {
133-
hostAccessor.guessOs()
134-
} catch (e: Exception) {
135-
logger.error("Could not resolve any IDE for workspace ${selectedWorkspace.name}. Reason: $e")
136-
null
137-
}
138-
}
139-
if (workspaceOS == null) {
140-
disableNextAction()
141-
cbIDE.renderer = object : ColoredListCellRenderer<IdeWithStatus>() {
142-
override fun customizeCellRenderer(list: JList<out IdeWithStatus>, value: IdeWithStatus?, index: Int, isSelected: Boolean, cellHasFocus: Boolean) {
143-
background = UIUtil.getListBackground(isSelected, cellHasFocus)
144-
icon = UIUtil.getBalloonErrorIcon()
145-
append(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.ide.error.text", selectedWorkspace.name))
127+
ideResolvingJob = cs.launch {
128+
try {
129+
retrieveIDES(selectedWorkspace)
130+
} catch (e: Exception) {
131+
when(e) {
132+
is InterruptedException -> Unit
133+
is CancellationException -> Unit
134+
else -> {
135+
logger.error("Could not resolve any IDE for workspace ${selectedWorkspace.name}. Reason: $e")
136+
withContext(Dispatchers.Main) {
137+
disableNextAction()
138+
cbIDE.renderer = object : ColoredListCellRenderer<IdeWithStatus>() {
139+
override fun customizeCellRenderer(list: JList<out IdeWithStatus>, value: IdeWithStatus?, index: Int, isSelected: Boolean, cellHasFocus: Boolean) {
140+
background = UIUtil.getListBackground(isSelected, cellHasFocus)
141+
icon = UIUtil.getBalloonErrorIcon()
142+
append(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.ide.error.text", selectedWorkspace.name))
143+
}
144+
}
145+
}
146146
}
147147
}
148-
} else {
149-
logger.info("Resolved OS and Arch for ${selectedWorkspace.name} is: $workspaceOS")
150-
val installedIdesJob = async(Dispatchers.IO) {
151-
hostAccessor.getInstalledIDEs().map { ide -> IdeWithStatus(ide.product, ide.buildNumber, IdeStatus.ALREADY_INSTALLED, null, ide.pathToIde, ide.presentableVersion, ide.remoteDevType) }
152-
}
153-
val idesWithStatusJob = async(Dispatchers.IO) {
154-
IntelliJPlatformProduct.values()
155-
.filter { it.showInGateway }
156-
.flatMap { CachingProductsJsonWrapper.getInstance().getAvailableIdes(it, workspaceOS) }
157-
.map { ide -> IdeWithStatus(ide.product, ide.buildNumber, IdeStatus.DOWNLOAD, ide.download, null, ide.presentableVersion, ide.remoteDevType) }
158-
}
148+
}
149+
}
150+
}
159151

160-
val installedIdes = installedIdesJob.await()
161-
val idesWithStatus = idesWithStatusJob.await()
162-
if (installedIdes.isEmpty()) {
163-
logger.info("No IDE is installed in workspace ${selectedWorkspace.name}")
164-
} else {
165-
ideComboBoxModel.addAll(installedIdes)
166-
cbIDE.selectedIndex = 0
167-
}
152+
private suspend fun retrieveIDES(selectedWorkspace: WorkspaceAgentModel) {
153+
logger.info("Retrieving available IDE's for ${selectedWorkspace.name} workspace...")
154+
val hostAccessor = HighLevelHostAccessor.create(
155+
RemoteCredentialsHolder().apply {
156+
setHost("coder.${selectedWorkspace.name}")
157+
userName = "coder"
158+
authType = AuthType.OPEN_SSH
159+
},
160+
true
161+
)
162+
val workspaceOS = if (selectedWorkspace.agentOS != null && selectedWorkspace.agentArch != null) toDeployedOS(selectedWorkspace.agentOS, selectedWorkspace.agentArch) else withContext(Dispatchers.IO) {
163+
hostAccessor.guessOs()
164+
}
168165

169-
if (idesWithStatus.isEmpty()) {
170-
logger.warn("Could not resolve any IDE for workspace ${selectedWorkspace.name}, probably $workspaceOS is not supported by Gateway")
171-
} else {
166+
logger.info("Resolved OS and Arch for ${selectedWorkspace.name} is: $workspaceOS")
167+
val installedIdesJob = cs.async(Dispatchers.IO) {
168+
hostAccessor.getInstalledIDEs().map { ide -> IdeWithStatus(ide.product, ide.buildNumber, IdeStatus.ALREADY_INSTALLED, null, ide.pathToIde, ide.presentableVersion, ide.remoteDevType) }
169+
}
170+
val idesWithStatusJob = cs.async(Dispatchers.IO) {
171+
IntelliJPlatformProduct.values()
172+
.filter { it.showInGateway }
173+
.flatMap { CachingProductsJsonWrapper.getInstance().getAvailableIdes(it, workspaceOS) }
174+
.map { ide -> IdeWithStatus(ide.product, ide.buildNumber, IdeStatus.DOWNLOAD, ide.download, null, ide.presentableVersion, ide.remoteDevType) }
175+
}
172176

173-
ideComboBoxModel.addAll(idesWithStatus)
174-
cbIDE.selectedIndex = 0
175-
}
177+
val installedIdes = installedIdesJob.await()
178+
val idesWithStatus = idesWithStatusJob.await()
179+
if (installedIdes.isEmpty()) {
180+
logger.info("No IDE is installed in workspace ${selectedWorkspace.name}")
181+
} else {
182+
withContext(Dispatchers.Main) {
183+
ideComboBoxModel.addAll(installedIdes)
184+
cbIDE.selectedIndex = 0
185+
}
186+
}
187+
188+
if (idesWithStatus.isEmpty()) {
189+
logger.warn("Could not resolve any IDE for workspace ${selectedWorkspace.name}, probably $workspaceOS is not supported by Gateway")
190+
} else {
191+
withContext(Dispatchers.Main) {
192+
ideComboBoxModel.addAll(idesWithStatus)
193+
cbIDE.selectedIndex = 0
176194
}
177195
}
178196
}
@@ -213,6 +231,13 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
213231
return true
214232
}
215233

234+
override fun onPrevious() {
235+
super.onPrevious()
236+
cs.launch {
237+
ideResolvingJob.cancelAndJoin()
238+
}
239+
}
240+
216241
override fun dispose() {
217242
cs.cancel()
218243
}

src/main/resources/messages/CoderGatewayBundle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ gateway.connector.view.coder.workspaces.unsupported.os.info=Gateway supports onl
2020
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>
2121
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>
2222
gateway.connector.view.coder.remoteproject.loading.text=Retrieving products...
23-
gateway.connector.view.coder.remoteproject.ide.error.text=Could not retrieve any IDE for workspace {0} because an error was encountered
23+
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!
2424
gateway.connector.view.coder.remoteproject.next.text=Start IDE and connect
2525
gateway.connector.view.coder.remoteproject.choose.text=Choose IDE and project for workspace {0}
2626
gateway.connector.recentconnections.title=Recent Coder Workspaces

0 commit comments

Comments
 (0)