@@ -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
@@ -27,7 +28,6 @@ import com.intellij.openapi.util.Disposer
27
28
import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager
28
29
import com.intellij.remote.AuthType
29
30
import com.intellij.remote.RemoteCredentialsHolder
30
- import com.intellij.ssh.SshException
31
31
import com.intellij.ui.AnimatedIcon
32
32
import com.intellij.ui.ColoredListCellRenderer
33
33
import com.intellij.ui.DocumentAdapter
@@ -52,31 +52,34 @@ import com.jetbrains.gateway.ssh.HighLevelHostAccessor
52
52
import com.jetbrains.gateway.ssh.IdeStatus
53
53
import com.jetbrains.gateway.ssh.IdeWithStatus
54
54
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
55
+ import com.jetbrains.gateway.ssh.deploy.DeployException
55
56
import com.jetbrains.gateway.ssh.util.validateRemotePath
56
- import kotlinx.coroutines.CancellationException
57
57
import kotlinx.coroutines.CoroutineScope
58
58
import kotlinx.coroutines.Dispatchers
59
59
import kotlinx.coroutines.Job
60
- import kotlinx.coroutines.TimeoutCancellationException
61
60
import kotlinx.coroutines.async
62
61
import kotlinx.coroutines.cancel
63
62
import kotlinx.coroutines.cancelAndJoin
64
63
import kotlinx.coroutines.launch
65
64
import kotlinx.coroutines.runBlocking
66
- import kotlinx.coroutines.time.withTimeout
67
65
import kotlinx.coroutines.withContext
66
+ import net.schmizz.sshj.common.SSHException
67
+ import net.schmizz.sshj.connection.ConnectionException
68
68
import java.awt.Component
69
69
import java.awt.FlowLayout
70
- import java.time.Duration
71
70
import java.util.Locale
71
+ import java.util.concurrent.TimeUnit
72
+ import java.util.concurrent.TimeoutException
72
73
import javax.swing.ComboBoxModel
73
74
import javax.swing.DefaultComboBoxModel
75
+ import javax.swing.Icon
74
76
import javax.swing.JLabel
75
77
import javax.swing.JList
76
78
import javax.swing.JPanel
77
79
import javax.swing.ListCellRenderer
78
80
import javax.swing.SwingConstants
79
81
import javax.swing.event.DocumentEvent
82
+ import kotlin.coroutines.cancellation.CancellationException
80
83
81
84
class CoderLocateRemoteProjectStepView (private val setNextButtonEnabled : (Boolean ) -> Unit ) : CoderWorkspacesWizardStep, Disposable {
82
85
private val cs = CoroutineScope (Dispatchers .Main )
@@ -102,11 +105,11 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
102
105
row {
103
106
label(" IDE:" )
104
107
cbIDE = cell(IDEComboBox (ideComboBoxModel).apply {
105
- renderer = IDECellRenderer ()
106
108
addActionListener {
107
109
setNextButtonEnabled(this .selectedItem != null )
108
110
ApplicationManager .getApplication().invokeLater {
109
111
logger.info(" Selected IDE: ${this .selectedItem} " )
112
+ cbIDEComment.foreground = UIUtil .getContextHelpForeground()
110
113
when (this .selectedItem?.status) {
111
114
IdeStatus .ALREADY_INSTALLED ->
112
115
cbIDEComment.text =
@@ -131,7 +134,7 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
131
134
CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.ide.none.comment" ),
132
135
false , - 1 , true
133
136
)
134
- ).component
137
+ ).resizableColumn().align( AlignX . FILL ). component
135
138
}.topGap(TopGap .NONE ).bottomGap(BottomGap .NONE ).layout(RowLayout .PARENT_GRID )
136
139
row {
137
140
label(" Project directory:" )
@@ -149,15 +152,19 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
149
152
gap(RightGap .SMALL )
150
153
}.apply {
151
154
background = WelcomeScreenUIManager .getMainAssociatedComponentBackground()
152
- border = JBUI .Borders .empty(0 , 16 , 0 , 16 )
155
+ border = JBUI .Borders .empty(0 , 16 )
153
156
}
154
157
155
158
override val previousActionText = IdeBundle .message(" button.back" )
156
159
override val nextActionText = CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.next.text" )
157
160
158
161
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" )
160
165
ideComboBoxModel.removeAllElements()
166
+ setNextButtonEnabled(false )
167
+
161
168
val deploymentURL = wizardModel.coderURL.toURL()
162
169
val selectedWorkspace = wizardModel.selectedWorkspace
163
170
if (selectedWorkspace == null ) {
@@ -171,53 +178,60 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
171
178
terminalLink.url = coderClient.coderURL.withPath(" /@${coderClient.me.username} /${selectedWorkspace.name} /terminal" ).toString()
172
179
173
180
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" ))
199
188
}
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())
210
209
}
211
210
}
212
211
}
212
+ null
213
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
214
228
}
215
229
}
216
230
}
217
231
}
218
232
219
233
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)
221
235
ComponentValidator (disposable).installOn(tfProject)
222
236
223
237
tfProject.document.addDocumentListener(object : DocumentAdapter () {
@@ -258,7 +272,7 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
258
272
)
259
273
}
260
274
261
- private suspend fun retrieveIDES (executor : HighLevelHostAccessor , selectedWorkspace : WorkspaceAgentModel ) {
275
+ private suspend fun retrieveIDEs (executor : HighLevelHostAccessor , selectedWorkspace : WorkspaceAgentModel ): List < IdeWithStatus > {
262
276
logger.info(" Retrieving available IDE's for ${selectedWorkspace.name} workspace..." )
263
277
val workspaceOS = if (selectedWorkspace.agentOS != null && selectedWorkspace.agentArch != null ) toDeployedOS(selectedWorkspace.agentOS, selectedWorkspace.agentArch) else withContext(Dispatchers .IO ) {
264
278
executor.guessOs()
@@ -279,21 +293,11 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
279
293
val idesWithStatus = idesWithStatusJob.await()
280
294
if (installedIdes.isEmpty()) {
281
295
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
- }
287
296
}
288
-
289
297
if (idesWithStatus.isEmpty()) {
290
298
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
- }
296
299
}
300
+ return installedIdes + idesWithStatus
297
301
}
298
302
299
303
private fun toDeployedOS (os : OS , arch : Arch ): DeployTargetOS {
@@ -363,12 +367,12 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
363
367
}
364
368
}
365
369
366
- private class IDECellRenderer : ListCellRenderer <IdeWithStatus > {
370
+ private class IDECellRenderer ( message : String , cellIcon : Icon = AnimatedIcon . Default . INSTANCE ) : ListCellRenderer<IdeWithStatus> {
367
371
private val loadingComponentRenderer: ListCellRenderer <IdeWithStatus > = object : ColoredListCellRenderer <IdeWithStatus >() {
368
372
override fun customizeCellRenderer (list : JList <out IdeWithStatus >, value : IdeWithStatus ? , index : Int , isSelected : Boolean , cellHasFocus : Boolean ) {
369
373
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)
372
376
}
373
377
}
374
378
0 commit comments