Skip to content

Commit b0d1ed8

Browse files
committed
fix: stop workspace if server process was stopped (#23579)
Signed-off-by: Andre Dietisheim <[email protected]>
1 parent f605008 commit b0d1ed8

File tree

3 files changed

+65
-35
lines changed

3 files changed

+65
-35
lines changed

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

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ import com.redhat.devtools.gateway.openshift.DevWorkspaces
1818
import com.redhat.devtools.gateway.openshift.Pods
1919
import com.redhat.devtools.gateway.server.RemoteIDEServer
2020
import io.kubernetes.client.openapi.ApiException
21+
import kotlinx.coroutines.CoroutineScope
22+
import kotlinx.coroutines.Dispatchers
23+
import kotlinx.coroutines.launch
24+
import java.io.Closeable
2125
import java.io.IOException
2226
import java.net.ServerSocket
2327
import java.net.URI
28+
import java.util.concurrent.TimeUnit
2429

2530
class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
2631
@Throws(Exception::class)
@@ -53,12 +58,11 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
5358
?: throw IOException("Could not connect, remote IDE is not ready. No join link present.")
5459

5560
val pods = Pods(devSpacesContext.client)
56-
// ✅ Dynamically find a free local port
5761
val localPort = findFreePort()
5862
val forwarder = pods.forward(remoteIdeServer.pod, localPort, 5990)
5963
pods.waitForForwardReady(localPort)
6064
val effectiveJoinLink = joinLink.replace(":5990", ":$localPort")
61-
65+
6266
val client = LinkedClientManager
6367
.getInstance()
6468
.startNewClient(
@@ -69,30 +73,34 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
6973
false
7074
)
7175

72-
client.run {
73-
lifetime.onTermination {
74-
try {
75-
forwarder.close()
76-
} catch (_: Exception) {
77-
// Ignore cleanup errors
78-
}
79-
}
80-
lifetime.onTermination {
81-
if (remoteIdeServer.waitServerTerminated())
82-
DevWorkspaces(devSpacesContext.client)
83-
.stop(
84-
devSpacesContext.devWorkspace.namespace,
85-
devSpacesContext.devWorkspace.name
86-
)
87-
.also { onDevWorkspaceStopped() }
88-
}
89-
lifetime.onTermination { devSpacesContext.isConnected = false }
90-
lifetime.onTermination(onDisconnected)
76+
client.clientClosed.advise(client.lifetime) {
77+
onClientClosed(onDisconnected, remoteIdeServer, onDevWorkspaceStopped, forwarder)
9178
}
9279

9380
return client
9481
}
9582

83+
private fun onClientClosed(
84+
onDisconnected: () -> Unit,
85+
remoteIdeServer: RemoteIDEServer,
86+
onDevWorkspaceStopped: () -> Unit,
87+
forwarder: Closeable
88+
) {
89+
CoroutineScope(Dispatchers.IO).launch {
90+
onDisconnected.invoke()
91+
if (remoteIdeServer.waitServerTerminated()) {
92+
DevWorkspaces(devSpacesContext.client)
93+
.stop(
94+
devSpacesContext.devWorkspace.namespace,
95+
devSpacesContext.devWorkspace.name
96+
)
97+
.also { onDevWorkspaceStopped() }
98+
}
99+
forwarder.close()
100+
devSpacesContext.isConnected = false
101+
}
102+
}
103+
96104
private fun findFreePort(): Int {
97105
ServerSocket(0).use { socket ->
98106
socket.reuseAddress = true

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ class Pods(private val client: ApiClient) {
170170
logger.info("Attempt #${attempt + 1}: Connecting $localPort -> $remotePort...")
171171
val portForward = PortForward(client)
172172
forwardResult = portForward.forward(pod, listOf(remotePort))
173+
logger.info("forward successful: $localPort -> $remotePort...")
173174
copyStreams(clientSocket, forwardResult, remotePort)
174175
return
175176
} catch (e: Exception) {

src/main/kotlin/com/redhat/devtools/gateway/server/RemoteIDEServer.kt

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import com.redhat.devtools.gateway.DevSpacesContext
1919
import com.redhat.devtools.gateway.openshift.Pods
2020
import io.kubernetes.client.openapi.models.V1Container
2121
import io.kubernetes.client.openapi.models.V1Pod
22-
import org.bouncycastle.util.Arrays
22+
import kotlinx.coroutines.delay
23+
import kotlinx.coroutines.withTimeoutOrNull
2324
import java.io.IOException
25+
import kotlin.time.Duration.Companion.seconds
2426

2527
/**
2628
* Represent an IDE server running in a CDE.
@@ -68,8 +70,8 @@ class RemoteIDEServer(private val devSpacesContext: DevSpacesContext) {
6870
}
6971

7072
@Throws(IOException::class)
71-
fun waitServerReady() {
72-
doWaitServerState(true)
73+
suspend fun waitServerReady() {
74+
doWaitServerProjectExists(true)
7375
.also {
7476
if (!it) throw IOException(
7577
"Remote IDE server is not ready after $readyTimeout seconds.",
@@ -78,19 +80,38 @@ class RemoteIDEServer(private val devSpacesContext: DevSpacesContext) {
7880
}
7981

8082
@Throws(IOException::class)
81-
fun waitServerTerminated(): Boolean {
82-
return doWaitServerState(false)
83+
suspend fun waitServerTerminated(): Boolean {
84+
return doWaitServerProjectExists(false)
8385
}
8486

85-
@Throws(IOException::class)
86-
fun doWaitServerState(isReadyState: Boolean): Boolean {
87-
return try {
88-
val status = getStatus()
89-
isReadyState == !Arrays.isNullOrEmpty(status.projects)
90-
} catch (e: Exception) {
91-
thisLogger().debug("Failed to check remote IDE server state.", e)
92-
false
93-
}
87+
/**
88+
* Waits for the server to have or not have projects according to the given parameter.
89+
* Times out the wait if the expected state is not reached within 10 seconds.
90+
*
91+
* @param expected True if projects are expected, False otherwise,
92+
* @return True if the expected state is achieved within the timeout, False otherwise.
93+
*/
94+
private suspend fun doWaitServerProjectExists(expected: Boolean): Boolean {
95+
val timeout = 10.seconds
96+
97+
return withTimeoutOrNull(timeout) {
98+
while (true) {
99+
val hasProjects = try {
100+
val status = getStatus()
101+
status.projects.isNotEmpty()
102+
} catch (e: Exception) {
103+
thisLogger().debug("Failed to check remote IDE server state.", e)
104+
null
105+
}
106+
107+
if (expected == hasProjects) {
108+
return@withTimeoutOrNull true
109+
}
110+
111+
delay(500L)
112+
}
113+
true
114+
} ?: false
94115
}
95116

96117
@Throws(IOException::class)

0 commit comments

Comments
 (0)