Skip to content

Commit 72f7132

Browse files
vrubezhnyadietish
authored andcommitted
fix: Gateway: Cancelling the connect operation fails and leaks the port forwarding
Fixes: eclipse-che/che#23614 Signed-off-by: Victor Rubezhny <[email protected]>
1 parent 9cc3819 commit 72f7132

File tree

6 files changed

+211
-103
lines changed

6 files changed

+211
-103
lines changed

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

Lines changed: 81 additions & 21 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,20 +11,28 @@
1111
*/
1212
package com.redhat.devtools.gateway
1313

14+
import com.intellij.openapi.diagnostic.thisLogger
1415
import com.jetbrains.gateway.thinClientLink.LinkedClientManager
1516
import com.jetbrains.gateway.thinClientLink.ThinClientHandle
1617
import com.jetbrains.rd.util.lifetime.Lifetime
1718
import com.redhat.devtools.gateway.openshift.DevWorkspaces
1819
import com.redhat.devtools.gateway.openshift.Pods
1920
import com.redhat.devtools.gateway.server.RemoteIDEServer
21+
import com.redhat.devtools.gateway.server.RemoteIDEServerStatus
2022
import io.kubernetes.client.openapi.ApiException
2123
import kotlinx.coroutines.CoroutineScope
2224
import kotlinx.coroutines.Dispatchers
2325
import kotlinx.coroutines.launch
26+
import kotlinx.coroutines.runBlocking
27+
import kotlinx.coroutines.delay
28+
import kotlinx.coroutines.withTimeoutOrNull
2429
import java.io.Closeable
2530
import java.io.IOException
2631
import java.net.ServerSocket
2732
import java.net.URI
33+
import java.util.concurrent.CancellationException
34+
import java.util.concurrent.atomic.AtomicBoolean
35+
import kotlin.time.Duration.Companion.seconds
2836

2937
class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
3038
@Throws(Exception::class)
@@ -33,42 +41,65 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
3341
onConnected: () -> Unit,
3442
onDisconnected: () -> Unit,
3543
onDevWorkspaceStopped: () -> Unit,
36-
): ThinClientHandle {
37-
try {
38-
return doConnect(onConnected, onDevWorkspaceStopped, onDisconnected)
39-
} catch (e: Exception) {
40-
throw e
41-
}
44+
onProgress: ((message: String) -> Unit)? = null,
45+
checkCancelled: (() -> Unit)? = null
46+
): ThinClientHandle = runBlocking {
47+
doConnect(onConnected, onDevWorkspaceStopped, onDisconnected, onProgress, checkCancelled)
4248
}
4349

4450
@Throws(Exception::class)
4551
@Suppress("UnstableApiUsage")
46-
private fun doConnect(
52+
private suspend fun doConnect(
4753
onConnected: () -> Unit,
4854
onDevWorkspaceStopped: () -> Unit,
49-
onDisconnected: () -> Unit
55+
onDisconnected: () -> Unit,
56+
onProgress: ((message: String) -> Unit)? = null,
57+
checkCancelled: (() -> Unit)? = null
5058
): ThinClientHandle {
5159
val workspace = devSpacesContext.devWorkspace
5260
devSpacesContext.addWorkspace(workspace)
5361

5462
var remoteIdeServer: RemoteIDEServer? = null
5563
var forwarder: Closeable? = null
64+
var client: ThinClientHandle? = null
5665

5766
return try {
67+
onProgress?.invoke("Waiting for the Dev Workspace to get ready...")
68+
5869
startAndWaitDevWorkspace()
70+
71+
checkCancelled?.invoke()
72+
onProgress?.invoke("Waiting for the Remote IDE server to get ready...")
73+
5974
remoteIdeServer = RemoteIDEServer(devSpacesContext)
60-
val remoteIdeServerStatus = remoteIdeServer.getStatus()
75+
val remoteIdeServerStatus = runCatching {
76+
val server = remoteIdeServer.apply { waitServerReady(checkCancelled) }
77+
server.getStatus()
78+
}.getOrElse { RemoteIDEServerStatus.empty() }
79+
80+
check(remoteIdeServerStatus.isReady) { "Could not connect, remote IDE is not ready." }
81+
6182
val joinLink = remoteIdeServerStatus.joinLink
6283
?: throw IOException("Could not connect, remote IDE is not ready. No join link present.")
6384

85+
checkCancelled?.invoke()
86+
onProgress?.invoke("Waiting for the IDE client to start up...")
87+
6488
val pods = Pods(devSpacesContext.client)
6589
val localPort = findFreePort()
6690
forwarder = pods.forward(remoteIdeServer.pod, localPort, 5990)
6791
pods.waitForForwardReady(localPort)
6892

6993
val effectiveJoinLink = joinLink.replace(":5990", ":$localPort")
7094

71-
val client = LinkedClientManager
95+
val lifetimeDef = Lifetime.Eternal.createNested()
96+
lifetimeDef.lifetime.onTermination { onClientClosed( client, onDisconnected, onDevWorkspaceStopped, remoteIdeServer, forwarder) }
97+
98+
val finished = AtomicBoolean(false)
99+
100+
checkCancelled?.invoke()
101+
102+
client = LinkedClientManager
72103
.getInstance()
73104
.startNewClient(
74105
Lifetime.Eternal,
@@ -78,27 +109,50 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
78109
false
79110
)
80111

112+
client.onClientPresenceChanged.advise(client.lifetime) { finished.set(true) }
81113
client.clientClosed.advise(client.lifetime) {
82-
onClientClosed(onDisconnected , onDevWorkspaceStopped, remoteIdeServer, forwarder)
114+
onClientClosed(client, onDisconnected, onDevWorkspaceStopped, remoteIdeServer, forwarder)
115+
finished.set(true)
83116
}
117+
client.clientFailedToOpenProject.advise(client.lifetime) {
118+
onClientClosed(client, onDisconnected, onDevWorkspaceStopped, remoteIdeServer, forwarder)
119+
finished.set(true)
120+
}
121+
122+
val success = withTimeoutOrNull(60.seconds) {
123+
while (!finished.get()) {
124+
checkCancelled?.invoke()
125+
delay(200)
126+
}
127+
true
128+
} ?: false
84129

130+
// Check if the thin client has opened
131+
check(success && client.clientPresent) {
132+
"Could not connect, remote IDE client is not ready."
133+
}
134+
135+
onConnected()
85136
client
86137
} catch (e: Exception) {
87-
onClientClosed(onDisconnected, onDevWorkspaceStopped, remoteIdeServer, forwarder)
138+
runCatching { client?.close() }
139+
onClientClosed(client, onDisconnected, onDevWorkspaceStopped, remoteIdeServer, forwarder)
88140
throw e
89141
}
90142
}
91143

144+
@Suppress("UnstableApiUsage")
92145
private fun onClientClosed(
146+
client: ThinClientHandle? = null,
93147
onDisconnected: () -> Unit,
94148
onDevWorkspaceStopped: () -> Unit,
95149
remoteIdeServer: RemoteIDEServer?,
96150
forwarder: Closeable?
97151
) {
98152
CoroutineScope(Dispatchers.IO).launch {
153+
runCatching { client?.close() }
99154
val currentWorkspace = devSpacesContext.devWorkspace
100155
try {
101-
onDisconnected.invoke()
102156
if (true == remoteIdeServer?.waitServerTerminated()) {
103157
DevWorkspaces(devSpacesContext.client)
104158
.stop(
@@ -107,10 +161,14 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
107161
)
108162
.also { onDevWorkspaceStopped() }
109163
}
110-
forwarder?.close()
111164
} finally {
165+
runCatching {
166+
forwarder?.close()
167+
}.onFailure { e ->
168+
thisLogger().debug("Failed to close port forwarder", e)
169+
}
112170
devSpacesContext.removeWorkspace(currentWorkspace)
113-
onDisconnected()
171+
runCatching { onDisconnected() }
114172
}
115173
}
116174
}
@@ -122,23 +180,25 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
122180
}
123181
}
124182

125-
@Throws(IOException::class, ApiException::class)
126-
private fun startAndWaitDevWorkspace() {
183+
@Throws(IOException::class, ApiException::class, CancellationException::class)
184+
private fun startAndWaitDevWorkspace(checkCancelled: (() -> Unit)? = null) {
127185
if (!devSpacesContext.devWorkspace.started) {
186+
checkCancelled?.invoke()
128187
DevWorkspaces(devSpacesContext.client)
129188
.start(
130189
devSpacesContext.devWorkspace.namespace,
131190
devSpacesContext.devWorkspace.name
132191
)
133192
}
134193

135-
if (!DevWorkspaces(devSpacesContext.client)
194+
if (!runBlocking { DevWorkspaces(devSpacesContext.client)
136195
.waitPhase(
137196
devSpacesContext.devWorkspace.namespace,
138197
devSpacesContext.devWorkspace.name,
139198
DevWorkspaces.RUNNING,
140-
DevWorkspaces.RUNNING_TIMEOUT
141-
)
199+
DevWorkspaces.RUNNING_TIMEOUT,
200+
checkCancelled
201+
) }
142202
) throw IOException(
143203
"DevWorkspace '${devSpacesContext.devWorkspace.name}' is not running after ${DevWorkspaces.RUNNING_TIMEOUT} seconds"
144204
)

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

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import io.kubernetes.client.openapi.ApiException
3131
import kotlinx.coroutines.CompletableDeferred
3232
import kotlinx.coroutines.ExperimentalCoroutinesApi
3333
import kotlinx.coroutines.suspendCancellableCoroutine
34+
import java.util.concurrent.CancellationException
3435
import javax.swing.JComponent
3536
import javax.swing.Timer
3637
import kotlin.coroutines.resume
@@ -87,7 +88,7 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
8788
}
8889
} catch (e: ApiException) {
8990
indicator.text = "Connection failed"
90-
runDelayed(2000, { indicator.stop() })
91+
runDelayed(2000, { if (indicator.isRunning) indicator.stop() })
9192
if (!(handleUnauthorizedError(e) || handleNotFoundError(e))) {
9293
Dialogs.error(
9394
e.messageWithoutPrefix() ?: "Could not connect to workspace.",
@@ -97,11 +98,16 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
9798

9899
if (cont.isActive) cont.resume(null)
99100
} catch (e: Exception) {
100-
runDelayed(2000) { indicator.stop() }
101-
Dialogs.error(
102-
e.message ?: "Could not connect to workspace.",
103-
"Connection Error"
104-
)
101+
if (indicator.isCanceled) {
102+
indicator.text2 = "Error: ${e.message}"
103+
runDelayed(2000) { if (indicator.isRunning) indicator.stop() }
104+
} else {
105+
runDelayed(2000) { if (indicator.isRunning) indicator.stop() }
106+
Dialogs.error(
107+
e.message ?: "Could not connect to workspace.",
108+
"Connection Error"
109+
)
110+
}
105111
cont.resume(null)
106112
}
107113
},
@@ -122,8 +128,8 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
122128
indicator.text = "Remote IDE has started successfully"
123129
indicator.text2 = "Opening project window…"
124130
runDelayed(3000) {
125-
indicator.stop()
126-
ready.complete(handle)
131+
if (indicator.isRunning) indicator.stop()
132+
if (ready.isActive) ready.complete(handle)
127133
}
128134
}
129135
}
@@ -137,8 +143,8 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
137143
if (!ready.isCompleted) {
138144
indicator.text = "Failed to open remote project (code: $errorCode)"
139145
runDelayed(2000) {
140-
indicator.stop()
141-
ready.complete(null)
146+
if (indicator.isRunning) indicator.stop()
147+
if (ready.isActive) ready.complete(null)
142148
}
143149
}
144150
}
@@ -152,8 +158,8 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
152158
if (!ready.isCompleted) {
153159
indicator.text = "Remote IDE closed unexpectedly."
154160
runDelayed(2000) {
155-
indicator.stop()
156-
ready.complete(null)
161+
if (indicator.isRunning) indicator.stop()
162+
if (ready.isActive) ready.complete(null)
157163
}
158164
}
159165
}
@@ -193,7 +199,18 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
193199

194200
indicator.text2 = "Establishing remote IDE connection…"
195201
val thinClient = DevSpacesConnection(ctx)
196-
.connect({}, {}, {})
202+
.connect({}, {}, {},
203+
onProgress = { message ->
204+
if (!message.isEmpty()) {
205+
indicator.text2 = message
206+
}
207+
},
208+
checkCancelled = {
209+
if (indicator.isCanceled) {
210+
throw CancellationException("User cancelled the operation")
211+
}
212+
}
213+
)
197214

198215
indicator.text2 = "Connection established successfully."
199216
return DevSpacesConnectionHandle(

0 commit comments

Comments
 (0)