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.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
1717import com.jetbrains.gateway.thinClientLink.LinkedClientManager
1818import com.jetbrains.gateway.thinClientLink.ThinClientHandle
1919import 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
2024import io.kubernetes.client.openapi.ApiException
25+ import okio.Closeable
2126import java.io.IOException
2227import java.net.URI
28+ import java.util.concurrent.CancellationException
29+ import java.util.concurrent.atomic.AtomicInteger
2330
2431class 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}
0 commit comments