@@ -8,6 +8,7 @@ import com.coder.gateway.sdk.Arch
8
8
import com.coder.gateway.sdk.CoderCLIManager
9
9
import com.coder.gateway.sdk.CoderRestClientService
10
10
import com.coder.gateway.sdk.OS
11
+ import com.coder.gateway.sdk.suspendingRetryWithExponentialBackOff
11
12
import com.coder.gateway.sdk.toURL
12
13
import com.coder.gateway.sdk.withPath
13
14
import com.coder.gateway.toWorkspaceParams
@@ -51,8 +52,8 @@ import com.jetbrains.gateway.ssh.HighLevelHostAccessor
51
52
import com.jetbrains.gateway.ssh.IdeStatus
52
53
import com.jetbrains.gateway.ssh.IdeWithStatus
53
54
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
55
+ import com.jetbrains.gateway.ssh.deploy.DeployException
54
56
import com.jetbrains.gateway.ssh.util.validateRemotePath
55
- import kotlinx.coroutines.CancellationException
56
57
import kotlinx.coroutines.CoroutineScope
57
58
import kotlinx.coroutines.Dispatchers
58
59
import kotlinx.coroutines.Job
@@ -61,20 +62,24 @@ import kotlinx.coroutines.cancel
61
62
import kotlinx.coroutines.cancelAndJoin
62
63
import kotlinx.coroutines.launch
63
64
import kotlinx.coroutines.runBlocking
64
- import kotlinx.coroutines.time.withTimeout
65
65
import kotlinx.coroutines.withContext
66
+ import net.schmizz.sshj.common.SSHException
67
+ import net.schmizz.sshj.connection.ConnectionException
66
68
import java.awt.Component
67
69
import java.awt.FlowLayout
68
- import java.time.Duration
69
70
import java.util.Locale
71
+ import java.util.concurrent.TimeUnit
72
+ import java.util.concurrent.TimeoutException
70
73
import javax.swing.ComboBoxModel
71
74
import javax.swing.DefaultComboBoxModel
75
+ import javax.swing.Icon
72
76
import javax.swing.JLabel
73
77
import javax.swing.JList
74
78
import javax.swing.JPanel
75
79
import javax.swing.ListCellRenderer
76
80
import javax.swing.SwingConstants
77
81
import javax.swing.event.DocumentEvent
82
+ import kotlin.coroutines.cancellation.CancellationException
78
83
79
84
class CoderLocateRemoteProjectStepView (private val setNextButtonEnabled : (Boolean ) -> Unit ) : CoderWorkspacesWizardStep, Disposable {
80
85
private val cs = CoroutineScope (Dispatchers .Main )
@@ -100,7 +105,6 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
100
105
row {
101
106
label(" IDE:" )
102
107
cbIDE = cell(IDEComboBox (ideComboBoxModel).apply {
103
- renderer = IDECellRenderer ()
104
108
addActionListener {
105
109
setNextButtonEnabled(this .selectedItem != null )
106
110
ApplicationManager .getApplication().invokeLater {
@@ -148,19 +152,19 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
148
152
gap(RightGap .SMALL )
149
153
}.apply {
150
154
background = WelcomeScreenUIManager .getMainAssociatedComponentBackground()
151
- border = JBUI .Borders .empty(0 , 16 , 0 , 16 )
155
+ border = JBUI .Borders .empty(0 , 16 )
152
156
}
153
157
154
158
override val previousActionText = IdeBundle .message(" button.back" )
155
159
override val nextActionText = CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.next.text" )
156
160
157
161
override fun onInit (wizardModel : CoderWorkspacesWizardModel ) {
158
- // Clear error message as it might still be displaying .
162
+ // Clear contents from the last attempt if any .
159
163
cbIDEComment.foreground = UIUtil .getContextHelpForeground()
160
164
cbIDEComment.text = CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.ide.none.comment" )
161
-
162
- cbIDE.renderer = IDECellRenderer ()
163
165
ideComboBoxModel.removeAllElements()
166
+ setNextButtonEnabled(false )
167
+
164
168
val deploymentURL = wizardModel.coderURL.toURL()
165
169
val selectedWorkspace = wizardModel.selectedWorkspace
166
170
if (selectedWorkspace == null ) {
@@ -174,33 +178,53 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
174
178
terminalLink.url = coderClient.coderURL.withPath(" /@${coderClient.me.username} /${selectedWorkspace.name} /terminal" ).toString()
175
179
176
180
ideResolvingJob = cs.launch {
177
- try {
178
- val executor = withTimeout(Duration .ofSeconds(60 )) {
179
- createRemoteExecutor(CoderCLIManager .getHostName(deploymentURL, selectedWorkspace))
180
- }
181
- retrieveIDES(executor, selectedWorkspace)
182
- if (ComponentValidator .getInstance(tfProject).isEmpty) {
183
- installRemotePathValidator(executor)
184
- }
185
- } catch (e: Exception ) {
186
- when (e) {
187
- is InterruptedException -> Unit
188
- is CancellationException -> Unit
189
- else -> {
190
- logger.error(" Failed to retrieve IDEs for workspace ${selectedWorkspace.name} " , e)
191
- withContext(Dispatchers .Main ) {
192
- setNextButtonEnabled(false )
193
- cbIDEComment.foreground = UIUtil .getErrorForeground()
194
- cbIDEComment.text = e.message ? : " The error did not provide any further details"
195
- cbIDE.renderer = object : ColoredListCellRenderer <IdeWithStatus >() {
196
- override fun customizeCellRenderer (list : JList <out IdeWithStatus >, value : IdeWithStatus ? , index : Int , isSelected : Boolean , cellHasFocus : Boolean ) {
197
- background = UIUtil .getListBackground(isSelected, cellHasFocus)
198
- icon = UIUtil .getBalloonErrorIcon()
199
- append(CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.error.text" ))
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" ))
188
+ }
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())
200
209
}
201
210
}
202
211
}
212
+ null
203
213
}
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
204
228
}
205
229
}
206
230
}
@@ -248,7 +272,7 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
248
272
)
249
273
}
250
274
251
- private suspend fun retrieveIDES (executor : HighLevelHostAccessor , selectedWorkspace : WorkspaceAgentModel ) {
275
+ private suspend fun retrieveIDEs (executor : HighLevelHostAccessor , selectedWorkspace : WorkspaceAgentModel ): List < IdeWithStatus > {
252
276
logger.info(" Retrieving available IDE's for ${selectedWorkspace.name} workspace..." )
253
277
val workspaceOS = if (selectedWorkspace.agentOS != null && selectedWorkspace.agentArch != null ) toDeployedOS(selectedWorkspace.agentOS, selectedWorkspace.agentArch) else withContext(Dispatchers .IO ) {
254
278
executor.guessOs()
@@ -269,21 +293,11 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
269
293
val idesWithStatus = idesWithStatusJob.await()
270
294
if (installedIdes.isEmpty()) {
271
295
logger.info(" No IDE is installed in workspace ${selectedWorkspace.name} " )
272
- } else {
273
- withContext(Dispatchers .Main ) {
274
- ideComboBoxModel.addAll(installedIdes)
275
- cbIDE.selectedIndex = 0
276
- }
277
296
}
278
-
279
297
if (idesWithStatus.isEmpty()) {
280
298
logger.warn(" Could not resolve any IDE for workspace ${selectedWorkspace.name} , probably $workspaceOS is not supported by Gateway" )
281
- } else {
282
- withContext(Dispatchers .Main ) {
283
- ideComboBoxModel.addAll(idesWithStatus)
284
- cbIDE.selectedIndex = 0
285
- }
286
299
}
300
+ return installedIdes + idesWithStatus
287
301
}
288
302
289
303
private fun toDeployedOS (os : OS , arch : Arch ): DeployTargetOS {
@@ -353,12 +367,12 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
353
367
}
354
368
}
355
369
356
- private class IDECellRenderer : ListCellRenderer <IdeWithStatus > {
370
+ private class IDECellRenderer ( message : String , cellIcon : Icon = AnimatedIcon . Default . INSTANCE ) : ListCellRenderer<IdeWithStatus> {
357
371
private val loadingComponentRenderer: ListCellRenderer <IdeWithStatus > = object : ColoredListCellRenderer <IdeWithStatus >() {
358
372
override fun customizeCellRenderer (list : JList <out IdeWithStatus >, value : IdeWithStatus ? , index : Int , isSelected : Boolean , cellHasFocus : Boolean ) {
359
373
background = UIUtil .getListBackground(isSelected, cellHasFocus)
360
- icon = AnimatedIcon . Default . INSTANCE
361
- append(CoderGatewayBundle . message( " gateway.connector.view.coder.remoteproject.loading.text " ) )
374
+ icon = cellIcon
375
+ append(message)
362
376
}
363
377
}
364
378
0 commit comments