From 2edebd22bcd8bc09d93ddce6ac8d8478453def97 Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Thu, 30 Apr 2026 20:56:15 +0530 Subject: [PATCH] Added auto reconnect functionality Signed-off-by: msivasubramaniaan --- .../devtools/gateway/DevSpacesConnection.kt | 2 +- .../devworkspace/DevWorkspaceRestart.kt | 124 ++++++++++++++---- .../gateway/openshift/DevWorkspacePods.kt | 2 +- .../service/DevWorkspaceConnectionService.kt | 60 +++++++++ 4 files changed, 164 insertions(+), 24 deletions(-) create mode 100644 src/main/kotlin/com/redhat/devtools/gateway/service/DevWorkspaceConnectionService.kt diff --git a/src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnection.kt b/src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnection.kt index 58381a16..3c6bb239 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnection.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnection.kt @@ -204,7 +204,7 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) { private fun onRestartAnnotated(namespace: String, workspaceName: String, thinClient: ThinClientHandle): () -> Job { return { CoroutineScope(Dispatchers.IO).launch { - val restartHandler = DevWorkspaceRestart(namespace, workspaceName, devSpacesContext.client) + val restartHandler = DevWorkspaceRestart(devSpacesContext) restartHandler.execute(thinClient) } } diff --git a/src/main/kotlin/com/redhat/devtools/gateway/devworkspace/DevWorkspaceRestart.kt b/src/main/kotlin/com/redhat/devtools/gateway/devworkspace/DevWorkspaceRestart.kt index b8e12468..aebcc821 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/devworkspace/DevWorkspaceRestart.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/devworkspace/DevWorkspaceRestart.kt @@ -11,10 +11,13 @@ */ package com.redhat.devtools.gateway.devworkspace +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.thisLogger import com.jetbrains.gateway.thinClientLink.ThinClientHandle +import com.redhat.devtools.gateway.DevSpacesContext import com.redhat.devtools.gateway.openshift.DevWorkspacePods -import io.kubernetes.client.openapi.ApiClient +import com.redhat.devtools.gateway.server.RemoteIDEServer +import com.redhat.devtools.gateway.service.DevSpacesConnectionService import kotlinx.coroutines.delay import kotlin.time.Duration.Companion.seconds @@ -29,20 +32,23 @@ import kotlin.time.Duration.Companion.seconds * 5. Clean up the restart annotation */ class DevWorkspaceRestart( - private val namespace: String, - private val workspaceName: String, - private val client: ApiClient, - private val workspaces: DevWorkspaces = DevWorkspaces(client), - private val pods: DevWorkspacePods = DevWorkspacePods(client) + private val devSpacesContext: DevSpacesContext, + private val workspaces: DevWorkspaces = DevWorkspaces(devSpacesContext.client), + private val pods: DevWorkspacePods = DevWorkspacePods(devSpacesContext.client) ) { + private val namespace get() = devSpacesContext.devWorkspace.namespace + private val workspaceName get() = devSpacesContext.devWorkspace.name + @Suppress("UnstableApiUsage") suspend fun execute(thinClient: ThinClientHandle) { try { close(thinClient) - stopWorkspace() + stopWorkspaceAndWait() waitForPodsDeleted() - startWorkspace() + startWorkspaceAndWait() + waitForIDEReady() + triggerIDEConnect() removeAnnotation() } catch (e: Exception) { thisLogger().error("Workspace restart failed for $namespace/$workspaceName", e) @@ -51,6 +57,40 @@ class DevWorkspaceRestart( } } + private suspend fun waitForIDEReady() { + thisLogger().debug("Waiting for IDE server to be ready...") + + try { + RemoteIDEServer(devSpacesContext).waitServerReady() + thisLogger().debug("IDE server is ready") + } catch (e: Exception) { + thisLogger().error("IDE server did not become ready", e) + throw e + } + } + + /** + * Triggers IDE reconnect safely on UI thread. + */ + private fun triggerIDEConnect() { + try { + ApplicationManager.getApplication().invokeLater { + try { + DevSpacesConnectionService + .getInstance() + .connect(devSpacesContext) + + thisLogger().info("IDE reconnect triggered successfully") + + } catch (e: Exception) { + thisLogger().error("Failed to execute IDE reconnect", e) + } + } + } catch (e: Exception) { + thisLogger().error("Failed to schedule IDE reconnect", e) + } + } + @Suppress("UnstableApiUsage") private suspend fun close(thinClient: ThinClientHandle) { thisLogger().debug("Closing thin client for $namespace/$workspaceName") @@ -58,27 +98,67 @@ class DevWorkspaceRestart( delay(1.seconds) // Give time for port forwarder cleanup } - private fun stopWorkspace() { - workspaces.stop(namespace, workspaceName) - thisLogger().debug("workspace $namespace/$workspaceName stop requested.") + private fun stopWorkspaceAndWait() { + thisLogger().debug("Stopping workspace and waiting...") + + workspaces.stopAndWait( + namespace, + workspaceName, + DevWorkspaces.RUNNING_TIMEOUT + ) + + thisLogger().debug("Workspace stopped") } - private suspend fun waitForPodsDeleted() { - val podsDeleted = pods.waitForPodsDeleted( + private fun startWorkspaceAndWait() { + thisLogger().debug("Starting workspace and waiting...") + + workspaces.startAndWait( namespace, workspaceName, - 20 + DevWorkspaces.RUNNING_TIMEOUT ) - if (podsDeleted) { - thisLogger().debug("All pods for $namespace/$workspaceName have been deleted.") - } else { - thisLogger().warn("Pods for $namespace/$workspaceName were not deleted within timeout, proceeding anyway.") - } + + thisLogger().debug("Workspace started and running") } - private fun startWorkspace() { - workspaces.start(namespace, workspaceName) - thisLogger().debug("workspace $namespace/$workspaceName start requested.") + private suspend fun waitForPodsDeleted() { + thisLogger().debug("Waiting until all pods are deleted for $namespace/$workspaceName") + + val labelSelector = "controller.devfile.io/devworkspace_name=$workspaceName" + + val timeoutSeconds = 120L + val startTime = System.currentTimeMillis() + + while (true) { + val podList = try { + pods.doList(namespace, labelSelector) + } catch (e: Exception) { + thisLogger().warn("Failed to list pods, retrying...", e) + delay(1000) + continue + } + + val activePods = podList.items.filter { it.metadata?.deletionTimestamp == null } + val podsCount = activePods.size + + if (podsCount == 0) { + thisLogger().debug("All pods deleted for $namespace/$workspaceName") + return + } + + thisLogger().debug("Still waiting... $podsCount pods remaining") + + val elapsed = (System.currentTimeMillis() - startTime) / 1000 + if (elapsed >= timeoutSeconds) { + val podNames = activePods.map { it.metadata?.name } + throw IllegalStateException( + "Timeout waiting for pods deletion. Remaining pods: $podNames" + ) + } + + delay(1000) + } } private fun removeAnnotation() { diff --git a/src/main/kotlin/com/redhat/devtools/gateway/openshift/DevWorkspacePods.kt b/src/main/kotlin/com/redhat/devtools/gateway/openshift/DevWorkspacePods.kt index 566851a7..eb6c41c1 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/openshift/DevWorkspacePods.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/openshift/DevWorkspacePods.kt @@ -394,7 +394,7 @@ class DevWorkspacePods(private val client: ApiClient) { } @Throws(ApiException::class) - private fun doList(namespace: String, labelSelector: String = ""): V1PodList { + fun doList(namespace: String, labelSelector: String = ""): V1PodList { return CoreV1Api(client) .listNamespacedPod(namespace) .labelSelector(labelSelector) diff --git a/src/main/kotlin/com/redhat/devtools/gateway/service/DevWorkspaceConnectionService.kt b/src/main/kotlin/com/redhat/devtools/gateway/service/DevWorkspaceConnectionService.kt new file mode 100644 index 00000000..b4a4f1f6 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/gateway/service/DevWorkspaceConnectionService.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package com.redhat.devtools.gateway.service + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.progress.ProgressManager +import com.redhat.devtools.gateway.DevSpacesBundle +import com.redhat.devtools.gateway.DevSpacesConnection +import com.redhat.devtools.gateway.DevSpacesContext +import com.redhat.devtools.gateway.util.messageWithoutPrefix +import com.redhat.devtools.gateway.view.ui.Dialogs + +@Service +class DevSpacesConnectionService { + + fun connect(devSpacesContext: DevSpacesContext) { + ProgressManager.getInstance().runProcessWithProgressSynchronously( + { + try { + DevSpacesConnection(devSpacesContext).connect( + { + thisLogger().debug("IDE connected successfully") + }, + { + thisLogger().warn("IDE connection failed") + }, + { + thisLogger().debug("Workspace stopped after connection") + } + ) + } catch (e: Exception) { + thisLogger().error("Workspace IDE connection failed.", e) + Dialogs.error( + e.messageWithoutPrefix() ?: "Could not connect to workspace IDE", + "Connection Error" + ) + } + }, + DevSpacesBundle.message("connector.loader.devspaces.connecting.text"), + true, + null + ) + } + + companion object { + fun getInstance(): DevSpacesConnectionService = + ApplicationManager.getApplication().getService(DevSpacesConnectionService::class.java) + } +} \ No newline at end of file