Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
onDevWorkspaceStopped: () -> Unit,
onProgress: ((value: ProgressCountdown.ProgressEvent) -> Unit)? = null,
checkCancelled: (() -> Unit)? = null,
modalityState: ModalityState? = null,
registerRestartWatcher: Boolean? = true
): ThinClientHandle {
val workspace = devSpacesContext.devWorkspace
Expand All @@ -60,7 +61,7 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
while (!remoteIdeServerStatus.isReady) {
checkCancelled?.invoke()
onProgress?.invoke(ProgressCountdown.ProgressEvent(
message = "Waiting for the workspace to get ready...",
message = "Waiting for the workspace to get started...",
countdownSeconds = DevWorkspaces.RUNNING_TIMEOUT))

DevWorkspaces(devSpacesContext.client)
Expand All @@ -76,15 +77,15 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {

remoteIdeServer = RemoteIDEServer(devSpacesContext)
remoteIdeServerStatus = runCatching {
remoteIdeServer.apply { waitServerReady(checkCancelled) }.getStatus()
remoteIdeServer.apply { waitServerReady(checkCancelled) }.getStatus(checkCancelled)
}.getOrElse { e ->
if (e.isCancellationException()) throw e
RemoteIDEServerStatus.empty()
}

checkCancelled?.invoke()
if (!remoteIdeServerStatus.isReady) {
val restartWorkspace = Dialogs.ideNotResponding()
val restartWorkspace = Dialogs.ideNotResponding(modalityState)

if (restartWorkspace) {
// User chose "Restart Pod": stop the Pod and try starting from scratch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,8 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
},
checkCancelled = {
if (indicator.isCanceled) throw CancellationException("User cancelled the operation")
}
},
modalityState = indicator.modalityState
)
} catch (e: ApiException) {
throw e.toWorkspaceException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.intellij.openapi.diagnostic.logger
import com.redhat.devtools.gateway.util.isCancellationException
import io.kubernetes.client.PortForward
import io.kubernetes.client.custom.IOTrio
import io.kubernetes.client.openapi.ApiClient
import io.kubernetes.client.openapi.ApiException
import io.kubernetes.client.openapi.apis.CoreV1Api
Expand All @@ -38,6 +39,18 @@

private val logger = logger<DevWorkspacePods>()

// Bounded by OkHttp's default 10s connect/read/write timeouts on the shared client
fun isPodRunning(pod: V1Pod): Boolean {
val name = pod.metadata?.name ?: return false
val namespace = pod.metadata?.namespace ?: return false
return try {
val current = CoreV1Api(client).readNamespacedPod(name, namespace).execute()
current.status?.phase == "Running"
} catch (_: Exception) {
false
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Example:
// https://github.com/kubernetes-client/java/blob/master/examples/examples-release-latest/src/main/java/io/kubernetes/client/examples/ExecExample.java
suspend fun exec(
Expand Down Expand Up @@ -73,30 +86,18 @@
container = container,
command = command,
onOpen = { io ->
stdoutStream = io.stdout
stderrStream = io.stderr
launchCheckCancelled(checkCancelled, scope, io)

stdoutJob = scope.launch { readStream(io.stdout, stdout, checkCancelled) }
stderrJob = scope.launch { readStream(io.stderr, stderr, checkCancelled) }

scope.launch {
try {
listOfNotNull(stdoutJob, stderrJob).joinAll()
closed.await()

checkCancelled?.invoke()
cont.resume(stdout.toString())
} catch (e: Throwable) {
if (e.isCancellationException()) cont.cancel(e)
else if (cont.isActive) cont.resumeWithException(e)
}
}
launchJoinStdOutStdErr(scope, stdoutJob, stderrJob, checkCancelled, closed, cont, stdout, execClientApi)
},
onClosed = { _, _ ->
closed.complete(Unit)
},
onError = { err, _ ->
closed.complete(Unit)
shutdownExecClient(execClientApi)
cont.resumeWithException(err)
},
timeoutMs = timeout * 1000,
Expand All @@ -111,22 +112,82 @@
execHandle.future.cancel(true)
} catch (_: Throwable) {}
scope.cancel()
shutdownExecClient(execClientApi)
}
} catch (e: Exception) {
shutdownExecClient(execClientApi)
if (cont.isActive) cont.resumeWithException(e)
}
}

private fun launchJoinStdOutStdErr(
scope: CoroutineScope,
stdoutJob: Job,
stderrJob: Job,
checkCancelled: (() -> Unit)?,
closed: CompletableDeferred<Unit>,
cont: CancellableContinuation<String>,
stdout: StringBuilder,
execClientApi: ApiClient
) {
scope.launch {
try {
listOfNotNull(stdoutJob, stderrJob).joinAll()
checkCancelled?.invoke()
closed.await()

checkCancelled?.invoke()
if (cont.isActive) cont.resume(stdout.toString())
} catch (e: Throwable) {
if (e.isCancellationException()) cont.cancel(e)
else if (cont.isActive) cont.resumeWithException(e)
} finally {
scope.cancel()
shutdownExecClient(execClientApi)
}
}
}

private fun launchCheckCancelled(
checkCancelled: (() -> Unit)?,
scope: CoroutineScope,
io: IOTrio
) {
if (checkCancelled == null) {
return
}
scope.launch {
try {
while (isActive) {
checkCancelled.invoke()
delay(200)

Check warning on line 163 in src/main/kotlin/com/redhat/devtools/gateway/openshift/DevWorkspacePods.kt

View workflow job for this annotation

GitHub Actions / Inspect code

Long overload to Duration conversion

Legacy Long overload can be converted to Duration
}
} catch (_: Throwable) {
runCatching { io.stdout.close() }
runCatching { io.stderr.close() }
}
}
}

private fun shutdownExecClient(client: ApiClient) {
runCatching { client.httpClient.dispatcher.executorService.shutdownNow() }
runCatching { client.httpClient.connectionPool.evictAll() }
}

private fun readStream(
input: InputStream,
output: StringBuilder,
checkCancelled: (() -> Unit)?
) {
while (true) {
checkCancelled?.invoke()
val b = input.read()
if (b == -1) break
output.append(b.toChar())
try {
while (true) {
checkCancelled?.invoke()
val b = input.read()
if (b == -1) break
output.append(b.toChar())
}
} catch (_: IOException) {
// Stream was closed (possibly due to cancellation)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class RemoteIDEServer(private val devSpacesContext: DevSpacesContext) {
@Throws(CancellationException::class)
suspend fun getStatus(checkCancelled: (() -> Unit)? = null): RemoteIDEServerStatus =
withContext(Dispatchers.IO) {
checkCancelled?.invoke()
if (!DevWorkspacePods(devSpacesContext.client).isPodRunning(pod)) {
return@withContext RemoteIDEServerStatus.empty()
}
checkCancelled?.invoke()
val output = DevWorkspacePods(devSpacesContext.client).exec(
pod = pod,
Expand Down
19 changes: 12 additions & 7 deletions src/main/kotlin/com/redhat/devtools/gateway/view/ui/Dialogs.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.redhat.devtools.gateway.view.ui

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.ui.Messages
import java.util.concurrent.atomic.AtomicInteger
import javax.swing.Icon
Expand Down Expand Up @@ -32,10 +33,11 @@ object Dialogs {
title: String,
options: Array<String>,
defaultOptionIndex: Int = 0,
icon: Icon? = Messages.getWarningIcon()
icon: Icon? = Messages.getWarningIcon(),
modalityState: ModalityState? = null
): Int {
val result = AtomicInteger(-1)
ApplicationManager.getApplication().invokeAndWait {
ApplicationManager.getApplication().invokeAndWait({
result.set(
Messages.showDialog(
message,
Expand All @@ -45,7 +47,7 @@ object Dialogs {
icon
)
)
}
}, modalityState ?: ModalityState.defaultModalityState())
return result.get()
}

Expand All @@ -64,14 +66,16 @@ object Dialogs {
title: String,
buttons: Array<String>,
confirmOptionIndex: Int,
defaultOptionIndex: Int = 0
defaultOptionIndex: Int = 0,
modalityState: ModalityState? = null
): Boolean {
return showInEdt(
message,
title,
buttons,
defaultOptionIndex,
Messages.getWarningIcon()
Messages.getWarningIcon(),
modalityState
) == confirmOptionIndex
}

Expand All @@ -96,14 +100,15 @@ object Dialogs {
*
* @return true if the user wants to restart the Pod, false if they want to cancel the connection
*/
fun ideNotResponding(): Boolean {
fun ideNotResponding(modalityState: ModalityState? = null): Boolean {
return confirm(
message = "The workspace IDE is not responding properly.\n" +
"Would you like to try restarting the Pod or cancel the connection?",
title = "Cannot Connect to workspace IDE",
buttons = arrayOf("Cancel Connection", "Restart Pod and try again"),
confirmOptionIndex = 1,
defaultOptionIndex = 0
defaultOptionIndex = 0,
modalityState = modalityState
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class DevWorkspaceRestartTest {
every { pods.list(any(), any()) } returns V1PodList().items(emptyList())
// Default: IDE ready and connection succeed (`just Awaits` never completes — hangs runTest)
coJustRun { remoteIDEServer.waitServerReady() }
coEvery { devSpacesConnection.connect(any(), any(), any(), any(), any(), any()) } returns mockk(relaxed = true)
coEvery { devSpacesConnection.connect(any(), any(), any(), any(), any(), any(), any()) } returns mockk(relaxed = true)
}

@Test
Expand Down Expand Up @@ -186,7 +186,7 @@ class DevWorkspaceRestartTest {
// when
restart.restart(thinClient, indicator)
// then
coVerify { devSpacesConnection.connect(any(), any(), any(), any(), any(), any()) }
coVerify { devSpacesConnection.connect(any(), any(), any(), any(), any(), any(), any()) }
}

@Test
Expand All @@ -196,7 +196,7 @@ class DevWorkspaceRestartTest {
// then
coVerifyOrder {
remoteIDEServer.waitServerReady()
devSpacesConnection.connect(any(), any(), any(), any(), any(), any())
devSpacesConnection.connect(any(), any(), any(), any(), any(), any(), any())
}
}

Expand Down Expand Up @@ -241,7 +241,7 @@ class DevWorkspaceRestartTest {
}
coVerifyOrder {
remoteIDEServer.waitServerReady()
devSpacesConnection.connect(any(), any(), any(), any(), any(), any())
devSpacesConnection.connect(any(), any(), any(), any(), any(), any(), any())
}
}

Expand Down Expand Up @@ -297,7 +297,7 @@ class DevWorkspaceRestartTest {
fun `#doRestart fails when IDE connection fails`() = runTest {
// given
val exception = RuntimeException("Connection failed")
coEvery { devSpacesConnection.connect(any(), any(), any(), any(), any(), any()) } throws exception
coEvery { devSpacesConnection.connect(any(), any(), any(), any(), any(), any(), any()) } throws exception

// when/then
val result = runCatching { restart.restart(thinClient, indicator) }
Expand Down
Loading
Loading