Skip to content

Commit 7dd1828

Browse files
committed
Jetbrains Gateway cannot connect after 3.5h IDLE period
Fixes eclipse-che/che#23485 Signed-off-by: Victor Rubezhny <[email protected]>
1 parent 98be0a6 commit 7dd1828

File tree

6 files changed

+304
-70
lines changed

6 files changed

+304
-70
lines changed
Lines changed: 171 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Red Hat, Inc.
2+
* Copyright (c) 2024-2025 Red Hat, Inc.
33
* This program and the accompanying materials are made
44
* available under the terms of the Eclipse Public License 2.0
55
* which is available at https://www.eclipse.org/legal/epl-2.0/
@@ -11,81 +11,178 @@
1111
*/
1212
package com.redhat.devtools.gateway
1313

14-
import com.redhat.devtools.gateway.openshift.DevWorkspaces
15-
import com.redhat.devtools.gateway.openshift.Pods
16-
import com.redhat.devtools.gateway.server.RemoteIDEServer
14+
import com.intellij.openapi.application.ApplicationManager
15+
import com.intellij.openapi.diagnostic.thisLogger
16+
import com.intellij.openapi.ui.Messages
1717
import com.jetbrains.gateway.thinClientLink.LinkedClientManager
1818
import com.jetbrains.gateway.thinClientLink.ThinClientHandle
1919
import com.jetbrains.rd.util.lifetime.Lifetime
20+
import com.redhat.devtools.gateway.openshift.DevWorkspaces
21+
import com.redhat.devtools.gateway.openshift.Pods
22+
import com.redhat.devtools.gateway.server.RemoteIDEServer
23+
import com.redhat.devtools.gateway.server.RemoteIDEServerStatus
2024
import io.kubernetes.client.openapi.ApiException
25+
import okio.Closeable
2126
import java.io.IOException
2227
import java.net.URI
28+
import java.util.concurrent.CancellationException
29+
import java.util.concurrent.atomic.AtomicInteger
2330

2431
class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
25-
@Throws(Exception::class)
32+
@Throws(Exception::class, CancellationException::class)
2633
@Suppress("UnstableApiUsage")
2734
fun connect(
2835
onConnected: () -> Unit,
2936
onDisconnected: () -> Unit,
3037
onDevWorkspaceStopped: () -> Unit,
38+
onProgress: ((message: String) -> Unit)? = null,
39+
isCancelled: (() -> Boolean)? = null
3140
): ThinClientHandle {
3241
if (devSpacesContext.isConnected)
3342
throw IOException(String.format("Already connected to %s", devSpacesContext.devWorkspace.metadata.name))
3443

3544
devSpacesContext.isConnected = true
3645
try {
37-
return doConnection(onConnected, onDevWorkspaceStopped, onDisconnected)
46+
return doConnection(onConnected, onDevWorkspaceStopped, onDisconnected, onProgress, isCancelled)
3847
} catch (e: Exception) {
3948
devSpacesContext.isConnected = false
4049
throw e
4150
}
4251
}
4352

44-
@Throws(Exception::class)
53+
@Throws(Exception::class, CancellationException::class)
4554
@Suppress("UnstableApiUsage")
4655
private fun doConnection(
4756
onConnected: () -> Unit,
4857
onDevWorkspaceStopped: () -> Unit,
49-
onDisconnected: () -> Unit
58+
onDisconnected: () -> Unit,
59+
onProgress: ((message: String) -> Unit)? = null,
60+
isCancelled: (() -> Boolean)? = null
5061
): ThinClientHandle {
51-
startAndWaitDevWorkspace()
62+
startAndWaitDevWorkspace(onProgress)
63+
if (isCancelled?.invoke() == true) {
64+
throw CancellationException("User cancelled the operation")
65+
}
66+
67+
onProgress?.invoke("Waiting for the Remote IDE server to get ready...")
68+
val (remoteIdeServer, remoteIdeServerStatus) =
69+
try {
70+
val remoteIdeServer = RemoteIDEServer(devSpacesContext).apply {
71+
waitRemoteIDEServerReady()
72+
}
73+
remoteIdeServer to remoteIdeServer.getStatus()
74+
} catch (_: IOException) {
75+
null to RemoteIDEServerStatus.empty()
76+
}
77+
78+
if (isCancelled?.invoke() == true) {
79+
throw CancellationException("User cancelled the operation")
80+
}
81+
82+
if (remoteIdeServer == null || !remoteIdeServerStatus.isReady) {
83+
thisLogger().debug("Remote IDE server is in an invalid state. Please restart the pod and try again. ")
84+
val result = AtomicInteger(-1)
85+
ApplicationManager.getApplication().invokeAndWait {
86+
result.set(
87+
Messages.showDialog(
88+
"The Remote IDE Server is not responding properly.\n" +
89+
"Would you like to try restarting the Pod or cancel the connection?",
90+
"Remote IDE Server Issue",
91+
arrayOf("Cancel Connection", "Restart Pod and try again"),
92+
0, // default selected index
93+
Messages.getWarningIcon()
94+
)
95+
)
96+
}
97+
98+
when (result.get()) {
99+
1 -> {
100+
// User chose "Restart Pod"
101+
thisLogger().info("User chose to restart the pod.")
102+
stopAndWaitDevWorkspace(onProgress)
103+
if (isCancelled?.invoke() == true) {
104+
throw CancellationException("User cancelled the operation")
105+
}
106+
return doConnection(onConnected, onDevWorkspaceStopped, onDisconnected, onProgress, isCancelled)
107+
}
108+
}
52109

53-
val remoteIdeServer = RemoteIDEServer(devSpacesContext)
54-
val remoteIdeServerStatus = remoteIdeServer.getStatus()
110+
// User chose "Cancel Connection"
111+
thisLogger().info("User cancelled the remote IDE connection.")
112+
throw IllegalStateException("Remote IDE server is not responding properly. Try restarting the pod and reconnecting.")
113+
}
55114

56115
val client = LinkedClientManager
57116
.getInstance()
58117
.startNewClient(
59118
Lifetime.Eternal,
60-
URI(remoteIdeServerStatus.joinLink),
119+
URI(remoteIdeServerStatus.joinLink!!),
61120
"",
62121
onConnected,
63122
false
64123
)
65124

66125
val forwarder = Pods(devSpacesContext.client).forward(remoteIdeServer.pod, 5990, 5990)
67-
68-
client.run {
69-
lifetime.onTermination { forwarder.close() }
70-
lifetime.onTermination {
71-
if (remoteIdeServer.waitServerTerminated())
72-
DevWorkspaces(devSpacesContext.client)
73-
.stop(
74-
devSpacesContext.devWorkspace.metadata.namespace,
75-
devSpacesContext.devWorkspace.metadata.name
76-
)
77-
.also { onDevWorkspaceStopped() }
126+
try {
127+
client.run {
128+
lifetime.onTermination {
129+
cleanup(forwarder, remoteIdeServer, devSpacesContext, onDevWorkspaceStopped, onDisconnected)
130+
}
78131
}
79-
lifetime.onTermination { devSpacesContext.isConnected = false }
80-
lifetime.onTermination(onDisconnected)
132+
} catch (e: Exception) {
133+
cleanup(forwarder, remoteIdeServer, devSpacesContext, onDevWorkspaceStopped, onDisconnected)
134+
throw e // rethrow so caller can handle the original problem
81135
}
82136

83137
return client
84138
}
85139

86-
@Throws(IOException::class, ApiException::class)
87-
private fun startAndWaitDevWorkspace() {
88-
if (!devSpacesContext.devWorkspace.spec.started) {
140+
private fun cleanup(
141+
forwarder: Closeable?,
142+
remoteIdeServer: RemoteIDEServer?,
143+
devSpacesContext: DevSpacesContext,
144+
onDevWorkspaceStopped: () -> Unit,
145+
onDisconnected: () -> Unit
146+
) {
147+
try {
148+
forwarder?.close()
149+
thisLogger().info("Closed port forwarder")
150+
} catch (e: Exception) {
151+
thisLogger().debug("Failed to close port forwarder", e)
152+
}
153+
154+
try {
155+
if (remoteIdeServer?.isRemoteIdeServerState(false) == true) {
156+
DevWorkspaces(devSpacesContext.client)
157+
.stop(
158+
devSpacesContext.devWorkspace.metadata.namespace,
159+
devSpacesContext.devWorkspace.metadata.name
160+
)
161+
.also { onDevWorkspaceStopped() }
162+
}
163+
} catch (e: Exception) {
164+
thisLogger().debug("Failed to stop DevWorkspace", e)
165+
}
166+
167+
devSpacesContext.isConnected = false
168+
169+
try {
170+
onDisconnected()
171+
} catch (e: Exception) {
172+
thisLogger().debug("onDisconnected handler failed", e)
173+
}
174+
}
175+
176+
177+
@Throws(IOException::class, ApiException::class, CancellationException::class)
178+
private fun startAndWaitDevWorkspace(onProgress: ((message: String) -> Unit)? = null,
179+
isCancelled: (() -> Boolean)? = null) {
180+
// We really need a refreshed DevWorkspace here
181+
val devWorkspace = DevWorkspaces(devSpacesContext.client).get(
182+
devSpacesContext.devWorkspace.metadata.namespace,
183+
devSpacesContext.devWorkspace.metadata.name)
184+
185+
if (!devWorkspace.spec.started) {
89186
DevWorkspaces(devSpacesContext.client)
90187
.start(
91188
devSpacesContext.devWorkspace.metadata.namespace,
@@ -94,11 +191,17 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
94191
}
95192

96193
if (!DevWorkspaces(devSpacesContext.client)
97-
.waitPhase(
194+
.waitForPhase(
98195
devSpacesContext.devWorkspace.metadata.namespace,
99196
devSpacesContext.devWorkspace.metadata.name,
100197
DevWorkspaces.RUNNING,
101-
DevWorkspaces.RUNNING_TIMEOUT
198+
onProgress = { phase, message ->
199+
onProgress?.invoke(buildString {
200+
append("Phase: $phase")
201+
if (message.isNotBlank()) append("$message")
202+
})
203+
},
204+
isCancelled = { isCancelled?.invoke() ?: false }
102205
)
103206
) throw IOException(
104207
String.format(
@@ -108,4 +211,42 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
108211
)
109212
)
110213
}
214+
215+
@Throws(IOException::class, ApiException::class, CancellationException::class)
216+
private fun stopAndWaitDevWorkspace(onProgress: ((message: String) -> Unit)? = null,
217+
isCancelled: (() -> Boolean)? = null) {
218+
// We really need a refreshed DevWorkspace here
219+
val devWorkspace = DevWorkspaces(devSpacesContext.client).get(
220+
devSpacesContext.devWorkspace.metadata.namespace,
221+
devSpacesContext.devWorkspace.metadata.name)
222+
223+
if (devWorkspace.spec.started) {
224+
DevWorkspaces(devSpacesContext.client)
225+
.stop(
226+
devSpacesContext.devWorkspace.metadata.namespace,
227+
devSpacesContext.devWorkspace.metadata.name
228+
)
229+
}
230+
231+
if (!DevWorkspaces(devSpacesContext.client)
232+
.waitForPhase(
233+
devSpacesContext.devWorkspace.metadata.namespace,
234+
devSpacesContext.devWorkspace.metadata.name,
235+
DevWorkspaces.STOPPED,
236+
onProgress = { phase, message ->
237+
onProgress?.invoke(buildString {
238+
append("Phase: $phase")
239+
if (message.isNotBlank()) append("$message")
240+
})
241+
},
242+
isCancelled = { isCancelled?.invoke() ?: false }
243+
)
244+
) throw IOException(
245+
String.format(
246+
"DevWorkspace '%s' is not stopped after %d seconds",
247+
devSpacesContext.devWorkspace.metadata.name,
248+
DevWorkspaces.RUNNING_TIMEOUT
249+
)
250+
)
251+
}
111252
}

src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnectionProvider.kt

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,8 @@ import kotlinx.coroutines.Dispatchers
3535
import kotlinx.coroutines.withContext
3636
import java.awt.BorderLayout
3737
import java.awt.Dimension
38-
import javax.swing.Action
39-
import javax.swing.Box
40-
import javax.swing.BoxLayout
41-
import javax.swing.JComponent
42-
import javax.swing.JLabel
43-
import javax.swing.JPanel
44-
import javax.swing.JProgressBar
45-
import javax.swing.Timer
38+
import java.util.concurrent.CancellationException
39+
import javax.swing.*
4640

4741
private const val DW_NAMESPACE = "dwNamespace"
4842
private const val DW_NAME = "dwName"
@@ -97,6 +91,12 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
9791
} else {
9892
throw err
9993
}
94+
} catch (e: CancellationException) {
95+
indicator.setText("${e.message}")
96+
Timer(2000) {
97+
indicator.close(DialogWrapper.CANCEL_EXIT_CODE)
98+
}.start()
99+
null
100100
} catch (e: Exception) {
101101
indicator.setText("Unexpected error: ${e.message}")
102102
Timer(2000) {
@@ -189,7 +189,13 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
189189
ctx.devWorkspace = DevWorkspaces(ctx.client).get(dwNamespace, dwName)
190190

191191
indicator?.text2 = "Establishing remote IDE connection…"
192-
val thinClient = DevSpacesConnection(ctx).connect({}, {}, {})
192+
val thinClient = DevSpacesConnection(ctx).connect({}, {}, {},
193+
onProgress = { message ->
194+
if (!message.isEmpty()) {
195+
indicator?.text2 = message
196+
}
197+
},
198+
isCancelled = { indicator?.isShowing == false })
193199

194200
indicator?.text2 = "Connection established successfully."
195201
return DevSpacesConnectionHandle(thinClient.lifetime, thinClient, { createComponent(dwName) }, dwName)

src/main/kotlin/com/redhat/devtools/gateway/openshift/DevWorkspace.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Red Hat, Inc.
2+
* Copyright (c) 2024-2025 Red Hat, Inc.
33
* This program and the accompanying materials are made
44
* available under the terms of the Eclipse Public License 2.0
55
* which is available at https://www.eclipse.org/legal/epl-2.0/
@@ -77,14 +77,22 @@ data class DevWorkspaceSpec(
7777
}
7878

7979
data class DevWorkspaceStatus(
80-
val phase: String
80+
val phase: String,
81+
val message: String
8182
) {
8283
companion object {
8384
fun from(map: Any) = object {
8485
val phase = Utils.getValue(map, arrayOf("phase")) ?: ""
8586

87+
val conditions = Utils.getValue(map, arrayOf("conditions")) as? List<Map<String, Any>>
88+
89+
val notReadyCondition = conditions
90+
?.firstOrNull { it["status"] == "False" }
91+
val message = notReadyCondition?.get("message") as? String ?: ""
92+
8693
val data = DevWorkspaceStatus(
87-
phase as String
94+
phase as String,
95+
message
8896
)
8997
}.data
9098
}

0 commit comments

Comments
 (0)