1
1
/*
2
- * Copyright (c) 2024 Red Hat, Inc.
2
+ * Copyright (c) 2024-2025 Red Hat, Inc.
3
3
* This program and the accompanying materials are made
4
4
* available under the terms of the Eclipse Public License 2.0
5
5
* which is available at https://www.eclipse.org/legal/epl-2.0/
11
11
*/
12
12
package com.redhat.devtools.gateway
13
13
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
17
17
import com.jetbrains.gateway.thinClientLink.LinkedClientManager
18
18
import com.jetbrains.gateway.thinClientLink.ThinClientHandle
19
19
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
20
24
import io.kubernetes.client.openapi.ApiException
25
+ import okio.Closeable
21
26
import java.io.IOException
22
27
import java.net.URI
28
+ import java.util.concurrent.CancellationException
29
+ import java.util.concurrent.atomic.AtomicInteger
23
30
24
31
class DevSpacesConnection (private val devSpacesContext : DevSpacesContext ) {
25
- @Throws(Exception ::class )
32
+ @Throws(Exception ::class , CancellationException :: class )
26
33
@Suppress(" UnstableApiUsage" )
27
34
fun connect (
28
35
onConnected : () -> Unit ,
29
36
onDisconnected : () -> Unit ,
30
37
onDevWorkspaceStopped : () -> Unit ,
38
+ onProgress : ((message: String ) -> Unit )? = null,
39
+ isCancelled : (() -> Boolean )? = null
31
40
): ThinClientHandle {
32
41
if (devSpacesContext.isConnected)
33
42
throw IOException (String .format(" Already connected to %s" , devSpacesContext.devWorkspace.metadata.name))
34
43
35
44
devSpacesContext.isConnected = true
36
45
try {
37
- return doConnection(onConnected, onDevWorkspaceStopped, onDisconnected)
46
+ return doConnection(onConnected, onDevWorkspaceStopped, onDisconnected, onProgress, isCancelled )
38
47
} catch (e: Exception ) {
39
48
devSpacesContext.isConnected = false
40
49
throw e
41
50
}
42
51
}
43
52
44
- @Throws(Exception ::class )
53
+ @Throws(Exception ::class , CancellationException :: class )
45
54
@Suppress(" UnstableApiUsage" )
46
55
private fun doConnection (
47
56
onConnected : () -> Unit ,
48
57
onDevWorkspaceStopped : () -> Unit ,
49
- onDisconnected : () -> Unit
58
+ onDisconnected : () -> Unit ,
59
+ onProgress : ((message: String ) -> Unit )? = null,
60
+ isCancelled : (() -> Boolean )? = null
50
61
): 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
+ }
52
109
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
+ }
55
114
56
115
val client = LinkedClientManager
57
116
.getInstance()
58
117
.startNewClient(
59
118
Lifetime .Eternal ,
60
- URI (remoteIdeServerStatus.joinLink),
119
+ URI (remoteIdeServerStatus.joinLink!! ),
61
120
" " ,
62
121
onConnected,
63
122
false
64
123
)
65
124
66
125
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
+ }
78
131
}
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
81
135
}
82
136
83
137
return client
84
138
}
85
139
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) {
89
186
DevWorkspaces (devSpacesContext.client)
90
187
.start(
91
188
devSpacesContext.devWorkspace.metadata.namespace,
@@ -94,11 +191,17 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
94
191
}
95
192
96
193
if (! DevWorkspaces (devSpacesContext.client)
97
- .waitPhase (
194
+ .waitForPhase (
98
195
devSpacesContext.devWorkspace.metadata.namespace,
99
196
devSpacesContext.devWorkspace.metadata.name,
100
197
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 }
102
205
)
103
206
) throw IOException (
104
207
String .format(
@@ -108,4 +211,42 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
108
211
)
109
212
)
110
213
}
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
+ }
111
252
}
0 commit comments