@@ -8,6 +8,7 @@ import com.coder.gateway.sdk.Arch
88import com.coder.gateway.sdk.CoderCLIManager
99import com.coder.gateway.sdk.CoderRestClientService
1010import com.coder.gateway.sdk.OS
11+ import com.coder.gateway.sdk.suspendingRetryWithExponentialBackOff
1112import com.coder.gateway.sdk.toURL
1213import com.coder.gateway.sdk.withPath
1314import com.coder.gateway.toWorkspaceParams
@@ -27,7 +28,6 @@ import com.intellij.openapi.util.Disposer
2728import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
2829import com.intellij.remote.AuthType
2930import com.intellij.remote.RemoteCredentialsHolder
30- import com.intellij.ssh.SshException
3131import com.intellij.ui.AnimatedIcon
3232import com.intellij.ui.ColoredListCellRenderer
3333import com.intellij.ui.DocumentAdapter
@@ -52,31 +52,34 @@ import com.jetbrains.gateway.ssh.HighLevelHostAccessor
5252import com.jetbrains.gateway.ssh.IdeStatus
5353import com.jetbrains.gateway.ssh.IdeWithStatus
5454import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
55+ import com.jetbrains.gateway.ssh.deploy.DeployException
5556import com.jetbrains.gateway.ssh.util.validateRemotePath
56- import kotlinx.coroutines.CancellationException
5757import kotlinx.coroutines.CoroutineScope
5858import kotlinx.coroutines.Dispatchers
5959import kotlinx.coroutines.Job
60- import kotlinx.coroutines.TimeoutCancellationException
6160import kotlinx.coroutines.async
6261import kotlinx.coroutines.cancel
6362import kotlinx.coroutines.cancelAndJoin
6463import kotlinx.coroutines.launch
6564import kotlinx.coroutines.runBlocking
66- import kotlinx.coroutines.time.withTimeout
6765import kotlinx.coroutines.withContext
66+ import net.schmizz.sshj.common.SSHException
67+ import net.schmizz.sshj.connection.ConnectionException
6868import java.awt.Component
6969import java.awt.FlowLayout
70- import java.time.Duration
7170import java.util.Locale
71+ import java.util.concurrent.TimeUnit
72+ import java.util.concurrent.TimeoutException
7273import javax.swing.ComboBoxModel
7374import javax.swing.DefaultComboBoxModel
75+ import javax.swing.Icon
7476import javax.swing.JLabel
7577import javax.swing.JList
7678import javax.swing.JPanel
7779import javax.swing.ListCellRenderer
7880import javax.swing.SwingConstants
7981import javax.swing.event.DocumentEvent
82+ import kotlin.coroutines.cancellation.CancellationException
8083
8184class CoderLocateRemoteProjectStepView (private val setNextButtonEnabled : (Boolean ) -> Unit ) : CoderWorkspacesWizardStep, Disposable {
8285 private val cs = CoroutineScope (Dispatchers .Main )
@@ -102,11 +105,11 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
102105 row {
103106 label(" IDE:" )
104107 cbIDE = cell(IDEComboBox (ideComboBoxModel).apply {
105- renderer = IDECellRenderer ()
106108 addActionListener {
107109 setNextButtonEnabled(this .selectedItem != null )
108110 ApplicationManager .getApplication().invokeLater {
109111 logger.info(" Selected IDE: ${this .selectedItem} " )
112+ cbIDEComment.foreground = UIUtil .getContextHelpForeground()
110113 when (this .selectedItem?.status) {
111114 IdeStatus .ALREADY_INSTALLED ->
112115 cbIDEComment.text =
@@ -131,7 +134,7 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
131134 CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.ide.none.comment" ),
132135 false , - 1 , true
133136 )
134- ).component
137+ ).resizableColumn().align( AlignX . FILL ). component
135138 }.topGap(TopGap .NONE ).bottomGap(BottomGap .NONE ).layout(RowLayout .PARENT_GRID )
136139 row {
137140 label(" Project directory:" )
@@ -149,15 +152,19 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
149152 gap(RightGap .SMALL )
150153 }.apply {
151154 background = WelcomeScreenUIManager .getMainAssociatedComponentBackground()
152- border = JBUI .Borders .empty(0 , 16 , 0 , 16 )
155+ border = JBUI .Borders .empty(0 , 16 )
153156 }
154157
155158 override val previousActionText = IdeBundle .message(" button.back" )
156159 override val nextActionText = CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.next.text" )
157160
158161 override fun onInit (wizardModel : CoderWorkspacesWizardModel ) {
159- cbIDE.renderer = IDECellRenderer ()
162+ // Clear contents from the last attempt if any.
163+ cbIDEComment.foreground = UIUtil .getContextHelpForeground()
164+ cbIDEComment.text = CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.ide.none.comment" )
160165 ideComboBoxModel.removeAllElements()
166+ setNextButtonEnabled(false )
167+
161168 val deploymentURL = wizardModel.coderURL.toURL()
162169 val selectedWorkspace = wizardModel.selectedWorkspace
163170 if (selectedWorkspace == null ) {
@@ -171,53 +178,60 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
171178 terminalLink.url = coderClient.coderURL.withPath(" /@${coderClient.me.username} /${selectedWorkspace.name} /terminal" ).toString()
172179
173180 ideResolvingJob = cs.launch {
174- try {
175- val executor = withTimeout(Duration .ofSeconds(60 )) {
176- createRemoteExecutor(CoderCLIManager .getHostName(deploymentURL, selectedWorkspace))
177- }
178- retrieveIDES(executor, selectedWorkspace)
179- if (ComponentValidator .getInstance(tfProject).isEmpty) {
180- installRemotePathValidator(executor)
181- }
182- } catch (e: Exception ) {
183- when (e) {
184- is InterruptedException -> Unit
185- is CancellationException -> Unit
186- is TimeoutCancellationException ,
187- is SshException -> {
188- logger.error(" Can't connect to workspace ${selectedWorkspace.name} . Reason: $e " )
189- withContext(Dispatchers .Main ) {
190- setNextButtonEnabled(false )
191- cbIDE.renderer = object : ColoredListCellRenderer <IdeWithStatus >() {
192- override fun customizeCellRenderer (list : JList <out IdeWithStatus >, value : IdeWithStatus ? , index : Int , isSelected : Boolean , cellHasFocus : Boolean ) {
193- background = UIUtil .getListBackground(isSelected, cellHasFocus)
194- icon = UIUtil .getBalloonErrorIcon()
195- append(CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.ssh.error.text" ))
196- }
197- }
198- }
181+ val ides = suspendingRetryWithExponentialBackOff(
182+ action= { attempt ->
183+ // Reset text in the select dropdown.
184+ withContext(Dispatchers .Main ) {
185+ cbIDE.renderer = IDECellRenderer (
186+ if (attempt > 1 ) CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.retry.text" , attempt)
187+ else CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.loading.text" ))
199188 }
200-
201- else -> {
202- logger.error(" Could not resolve any IDE for workspace ${selectedWorkspace.name} . Reason: $e " )
203- withContext(Dispatchers .Main ) {
204- setNextButtonEnabled(false )
205- cbIDE.renderer = object : ColoredListCellRenderer <IdeWithStatus >() {
206- override fun customizeCellRenderer (list : JList <out IdeWithStatus >, value : IdeWithStatus ? , index : Int , isSelected : Boolean , cellHasFocus : Boolean ) {
207- background = UIUtil .getListBackground(isSelected, cellHasFocus)
208- icon = UIUtil .getBalloonErrorIcon()
209- append(CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.ide.error.text" ))
189+ try {
190+ val executor = createRemoteExecutor(CoderCLIManager .getHostName(deploymentURL, selectedWorkspace))
191+ if (ComponentValidator .getInstance(tfProject).isEmpty) {
192+ installRemotePathValidator(executor)
193+ }
194+ retrieveIDEs(executor, selectedWorkspace)
195+ } catch (e: Exception ) {
196+ when (e) {
197+ is InterruptedException -> Unit
198+ is CancellationException -> Unit
199+ // Throw to retry these. The main one is
200+ // DeployException which fires when dd times out.
201+ is ConnectionException , is TimeoutException ,
202+ is SSHException , is DeployException -> throw e
203+ else -> {
204+ withContext(Dispatchers .Main ) {
205+ logger.error(" Failed to retrieve IDEs (attempt $attempt )" , e)
206+ cbIDEComment.foreground = UIUtil .getErrorForeground()
207+ cbIDEComment.text = e.message ? : " The error did not provide any further details"
208+ cbIDE.renderer = IDECellRenderer (CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.error.text" ), UIUtil .getBalloonErrorIcon())
210209 }
211210 }
212211 }
212+ null
213213 }
214+ },
215+ update = { attempt, retryMs, e ->
216+ logger.error(" Failed to retrieve IDEs (attempt $attempt ; will retry in $retryMs ms)" , e)
217+ cbIDEComment.foreground = UIUtil .getErrorForeground()
218+ cbIDEComment.text = e.message ? : " The error did not provide any further details"
219+ val delayS = TimeUnit .MILLISECONDS .toSeconds(retryMs)
220+ val delay = if (delayS < 1 ) " now" else " in $delayS second${if (delayS > 1 ) " s" else " " } "
221+ cbIDE.renderer = IDECellRenderer (CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.retry-error.text" , delay))
222+ },
223+ )
224+ if (ides != null ) {
225+ withContext(Dispatchers .Main ) {
226+ ideComboBoxModel.addAll(ides)
227+ cbIDE.selectedIndex = 0
214228 }
215229 }
216230 }
217231 }
218232
219233 private fun installRemotePathValidator (executor : HighLevelHostAccessor ) {
220- var disposable = Disposer .newDisposable(ApplicationManager .getApplication(), CoderLocateRemoteProjectStepView .javaClass .name)
234+ val disposable = Disposer .newDisposable(ApplicationManager .getApplication(), CoderLocateRemoteProjectStepView :: class .java .name)
221235 ComponentValidator (disposable).installOn(tfProject)
222236
223237 tfProject.document.addDocumentListener(object : DocumentAdapter () {
@@ -258,7 +272,7 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
258272 )
259273 }
260274
261- private suspend fun retrieveIDES (executor : HighLevelHostAccessor , selectedWorkspace : WorkspaceAgentModel ) {
275+ private suspend fun retrieveIDEs (executor : HighLevelHostAccessor , selectedWorkspace : WorkspaceAgentModel ): List < IdeWithStatus > {
262276 logger.info(" Retrieving available IDE's for ${selectedWorkspace.name} workspace..." )
263277 val workspaceOS = if (selectedWorkspace.agentOS != null && selectedWorkspace.agentArch != null ) toDeployedOS(selectedWorkspace.agentOS, selectedWorkspace.agentArch) else withContext(Dispatchers .IO ) {
264278 executor.guessOs()
@@ -279,21 +293,11 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
279293 val idesWithStatus = idesWithStatusJob.await()
280294 if (installedIdes.isEmpty()) {
281295 logger.info(" No IDE is installed in workspace ${selectedWorkspace.name} " )
282- } else {
283- withContext(Dispatchers .Main ) {
284- ideComboBoxModel.addAll(installedIdes)
285- cbIDE.selectedIndex = 0
286- }
287296 }
288-
289297 if (idesWithStatus.isEmpty()) {
290298 logger.warn(" Could not resolve any IDE for workspace ${selectedWorkspace.name} , probably $workspaceOS is not supported by Gateway" )
291- } else {
292- withContext(Dispatchers .Main ) {
293- ideComboBoxModel.addAll(idesWithStatus)
294- cbIDE.selectedIndex = 0
295- }
296299 }
300+ return installedIdes + idesWithStatus
297301 }
298302
299303 private fun toDeployedOS (os : OS , arch : Arch ): DeployTargetOS {
@@ -363,12 +367,12 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
363367 }
364368 }
365369
366- private class IDECellRenderer : ListCellRenderer <IdeWithStatus > {
370+ private class IDECellRenderer ( message : String , cellIcon : Icon = AnimatedIcon . Default . INSTANCE ) : ListCellRenderer<IdeWithStatus> {
367371 private val loadingComponentRenderer: ListCellRenderer <IdeWithStatus > = object : ColoredListCellRenderer <IdeWithStatus >() {
368372 override fun customizeCellRenderer (list : JList <out IdeWithStatus >, value : IdeWithStatus ? , index : Int , isSelected : Boolean , cellHasFocus : Boolean ) {
369373 background = UIUtil .getListBackground(isSelected, cellHasFocus)
370- icon = AnimatedIcon . Default . INSTANCE
371- append(CoderGatewayBundle . message( " gateway.connector.view.coder.remoteproject.loading.text " ) )
374+ icon = cellIcon
375+ append(message)
372376 }
373377 }
374378
0 commit comments