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/
1111 */
1212package com.redhat.devtools.gateway
1313
14+ import com.intellij.openapi.diagnostic.thisLogger
1415import com.jetbrains.gateway.thinClientLink.LinkedClientManager
1516import com.jetbrains.gateway.thinClientLink.ThinClientHandle
1617import com.jetbrains.rd.util.lifetime.Lifetime
1718import com.redhat.devtools.gateway.openshift.DevWorkspaces
1819import com.redhat.devtools.gateway.openshift.Pods
1920import com.redhat.devtools.gateway.server.RemoteIDEServer
21+ import com.redhat.devtools.gateway.server.RemoteIDEServerStatus
2022import io.kubernetes.client.openapi.ApiException
2123import kotlinx.coroutines.CoroutineScope
2224import kotlinx.coroutines.Dispatchers
2325import kotlinx.coroutines.launch
26+ import kotlinx.coroutines.runBlocking
27+ import kotlinx.coroutines.delay
28+ import kotlinx.coroutines.withTimeoutOrNull
2429import java.io.Closeable
2530import java.io.IOException
2631import java.net.ServerSocket
2732import java.net.URI
33+ import java.util.concurrent.CancellationException
34+ import java.util.concurrent.atomic.AtomicBoolean
35+ import kotlin.time.Duration.Companion.seconds
2836
2937class 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 )
0 commit comments