diff --git a/src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnection.kt b/src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnection.kt index 58381a16..db5730f4 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnection.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnection.kt @@ -189,7 +189,7 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) { private fun watchRestartAnnotation(namespace: String, workspaceName: String, kubeClient: ApiClient, thinClient: ThinClientHandle) { val restartWatchScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) RestartDevWorkspaceAnnotationWatch( - onRestartAnnotated(namespace, workspaceName, thinClient), + onRestartAnnotated(thinClient), kubeClient, namespace, workspaceName @@ -201,11 +201,13 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) { } @Suppress("UnstableApiUsage") - private fun onRestartAnnotated(namespace: String, workspaceName: String, thinClient: ThinClientHandle): () -> Job { + private fun onRestartAnnotated( + thinClient: ThinClientHandle + ): () -> Job { return { CoroutineScope(Dispatchers.IO).launch { - val restartHandler = DevWorkspaceRestart(namespace, workspaceName, devSpacesContext.client) - restartHandler.execute(thinClient) + val restartHandler = DevWorkspaceRestart(devSpacesContext) + restartHandler.restartWithProgress(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..4701ac8b 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,16 @@ */ package com.redhat.devtools.gateway.devworkspace +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager 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.Dispatchers import kotlinx.coroutines.delay import kotlin.time.Duration.Companion.seconds @@ -29,28 +35,110 @@ 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) { + fun restartWithProgress(thinClient: ThinClientHandle) { + + try { + kotlinx.coroutines.runBlocking(Dispatchers.IO) { + close(thinClient) + } + } catch (e: Exception) { + thisLogger().error("Failed to close thin client", e) + } + + ProgressManager.getInstance().runProcessWithProgressSynchronously( + { + val indicator = ProgressManager.getInstance().progressIndicator + indicator?.isIndeterminate = false + + try { + kotlinx.coroutines.runBlocking(Dispatchers.IO) { + executeWithProgress(indicator) + } + } catch (e: Exception) { + thisLogger().error("Workspace restart failed for $namespace/$workspaceName", e) + removeAnnotation() + throw e + } + }, + "Restarting DevWorkspace", + true, + null + ) + } + + private suspend fun executeWithProgress( + indicator: ProgressIndicator? + ) { + + indicator?.fraction = 0.0 + indicator?.text = "Closing IDE connection..." + + indicator?.fraction = 0.2 + indicator?.text = "Stopping workspace..." + stopWorkspaceAndWait() + + indicator?.fraction = 0.4 + indicator?.text = "Waiting for pods to terminate..." + waitForPodsDeleted(indicator) + + indicator?.fraction = 0.6 + indicator?.text = "Starting workspace..." + startWorkspaceAndWait() + + indicator?.fraction = 0.8 + indicator?.text = "Waiting for IDE to be ready..." + waitForIDEReady() + + indicator?.fraction = 1.0 + indicator?.text = "Connecting to IDE..." + + triggerIDEConnect() + removeAnnotation() + } + + private suspend fun waitForIDEReady() { + thisLogger().debug("Waiting for IDE server to be ready...") + try { - close(thinClient) - stopWorkspace() - waitForPodsDeleted() - startWorkspace() - removeAnnotation() + RemoteIDEServer(devSpacesContext).waitServerReady() + thisLogger().debug("IDE server is ready") } catch (e: Exception) { - thisLogger().error("Workspace restart failed for $namespace/$workspaceName", e) - removeAnnotation() + 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 +146,59 @@ 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(indicator: ProgressIndicator?) { + 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) { + indicator?.checkCanceled() + 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 } + if(activePods.isEmpty()) return + 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