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 @@ -43,9 +43,9 @@
/**
* Thin-client connection lifecycle.
*
* Thin-client close always ends the connect wait; [tearDownConnection] runs only if the
* connection is already live ([connectionLive]). Failures during connect are cleaned up by
* [connect]'s catch path.
* Thin-client close or failed-to-open sets connectFailed (ending the connect wait);
* [tearDownConnection] runs only if the connection is already live ([connectionLive]).

Check warning on line 47 in src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnection.kt

View workflow job for this annotation

GitHub Actions / Inspect code

Unresolved link in KDoc

Cannot resolve link 'tearDownConnection'
* Failures during connect are cleaned up by [connect]'s catch path.

Check warning on line 48 in src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnection.kt

View workflow job for this annotation

GitHub Actions / Inspect code

Unresolved link in KDoc

Cannot resolve link 'connect'
*
* Connect enablement in the wizard is based on workspace Running state (not
* [DevSpacesContext.activeWorkspaces]), because IDEA often keeps the connector view
Expand All @@ -54,7 +54,14 @@
* Still clear [DevSpacesContext.activeWorkspaces] when the connection ends (before optional
* remote stop) for tooltips / bookkeeping.
*/
class ThinClientNotReadyException(message: String) : IllegalStateException(message)

class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {

companion object {
private const val CONNECT_POLL: Long = 200 // millis
}

/** Ensures [tearDownConnection] runs at most once for this connect attempt. */
private val tearDownStarted = AtomicBoolean(false)

Expand Down Expand Up @@ -87,21 +94,21 @@

checkCancelled?.invoke()
onProgress?.invoke(ProgressCountdown.ProgressEvent(
message = "Waiting for the workspace IDE client to start..."))
message = "Waiting for the workspace IDE client to start (first-time download may take several minutes)..."))

val (fwd, localPort) = setupPortForwarding(remoteIdeServer.pod)
forwarder = fwd

val effectiveJoinLink = joinLink.replace(":5990", ":$localPort")
val connectWaitDone = AtomicBoolean(false)
val connectFailed = AtomicBoolean(false)

checkCancelled?.invoke()
client = startThinClient(
URI(effectiveJoinLink), workspace, onConnected, onConnectionEnded, onDevWorkspaceStopped,
remoteIdeServer, forwarder, connectWaitDone, connectionLive
remoteIdeServer, forwarder, connectFailed, connectionLive
)

waitForThinClientConnect(client, connectWaitDone, checkCancelled)
waitForThinClientConnect(client, connectFailed, checkCancelled)

if (registerRestartWatcher == true) {
watchRestartAnnotation(
Expand All @@ -116,7 +123,9 @@
onConnected()
client
} catch (e: Exception) {
runCatching { client?.close() }
if (e !is ThinClientNotReadyException || connectionLive.get()) {
runCatching { client?.close() }
}
tearDownConnection(
client, workspace, onConnectionEnded, onDevWorkspaceStopped, remoteIdeServer, forwarder
)
Expand All @@ -130,8 +139,8 @@
* already live; failures during connect are cleaned up by [connect]'s catch.
*/
@Suppress("UnstableApiUsage")
private fun onThinClientClosed(
connectWaitDone: AtomicBoolean,
internal fun onThinClientClosed(
connectFailed: AtomicBoolean,
connectionLive: AtomicBoolean,
thinClient: ThinClientHandle,
workspace: DevWorkspace,
Expand All @@ -140,7 +149,7 @@
remoteIdeServer: RemoteIDEServer?,
forwarder: Closeable?,
) {
connectWaitDone.set(true)
connectFailed.set(true)
if (connectionLive.get()) {
tearDownConnection(
thinClient,
Expand Down Expand Up @@ -215,11 +224,10 @@
val workspacePatch = DevWorkspacePatch(
workspace.namespace,
workspace.name,
devSpacesContext.client,
{
DevWorkspaces(devSpacesContext.client).get(workspace.namespace, workspace.name)
}
)
devSpacesContext.client
) {
DevWorkspaces(devSpacesContext.client).get(workspace.namespace, workspace.name)
}
try {
if (workspacePatch.hasRestartAnnotation()) {
closeAllProjects()
Expand Down Expand Up @@ -339,7 +347,7 @@
onDevWorkspaceStopped: () -> Unit,
remoteIdeServer: RemoteIDEServer?,
forwarder: Closeable?,
connectWaitDone: AtomicBoolean,
connectFailed: AtomicBoolean,
connectionLive: AtomicBoolean,
): ThinClientHandle {
val thinClient = LinkedClientManager
Expand All @@ -352,11 +360,9 @@
false
)

thinClient.onClientPresenceChanged.advise(thinClient.lifetime) { connectWaitDone.set(true) }

fun notifyThinClientClosed() {
onThinClientClosed(
connectWaitDone,
connectFailed,
connectionLive,
thinClient,
workspace,
Expand All @@ -372,22 +378,29 @@
return thinClient
}

private suspend fun waitForThinClientConnect(
/**
* Waits for the JetBrains Thin Client to become available.
*
* The first-time JetBrains Client download may take several minutes. This phase waits
* until [thinClient.clientPresent] is true, or for a permanent failure signalled via

Check warning on line 385 in src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnection.kt

View workflow job for this annotation

GitHub Actions / Inspect code

Unresolved link in KDoc

Cannot resolve link 'clientPresent'
* [connectFailed] (clientClosed / clientFailedToOpenProject). There is no wall-clock
* timeout on this wait.
*/
@Suppress("UnstableApiUsage")
internal suspend fun waitForThinClientConnect(
thinClient: ThinClientHandle,
connectWaitDone: AtomicBoolean,
connectFailed: AtomicBoolean,
checkCancelled: (() -> Unit)?
) {
@Suppress("ConvertLongToDuration")
val success = withTimeoutOrNull(60_000L) {
while (!connectWaitDone.get()) {
checkCancelled?.invoke()
delay(200L)
}
true
} ?: false
while (!thinClient.clientPresent && !connectFailed.get()) {
checkCancelled?.invoke()
delay(CONNECT_POLL)

Check warning on line 397 in src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnection.kt

View workflow job for this annotation

GitHub Actions / Inspect code

Long overload to Duration conversion

Legacy Long overload can be converted to Duration
}

check(success && thinClient.clientPresent) {
"Could not connect, workspace IDE is not ready."
if (!thinClient.clientPresent || connectFailed.get()) {
throw ThinClientNotReadyException(
"Could not connect, workspace IDE is not ready."
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
*/
package com.redhat.devtools.gateway

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.intellij.ui.dsl.builder.Align.Companion.CENTER
import com.intellij.ui.dsl.builder.panel
Expand Down Expand Up @@ -41,7 +39,7 @@
*/
class DevSpacesConnectionProvider : GatewayConnectionProvider {

@OptIn(ExperimentalCoroutinesApi::class)

Check notice on line 42 in src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnectionProvider.kt

View workflow job for this annotation

GitHub Actions / Inspect code

Unnecessary '@OptIn' annotation

The opt-in annotation is redundant: no matching experimental API is used
@Suppress("UnstableApiUsage")
override suspend fun connect(
parameters: Map<String, String>,
Expand All @@ -66,48 +64,13 @@
indicator.text = "Connecting to DevSpace..."

val handle = doConnect(parameters, ctx, indicator)
val thinClient = handle.clientHandle
?: throw RuntimeException("Failed to obtain ThinClientHandle")

if (thinClient.clientPresent) {
indicator.text = "Workspace IDE has started successfully"
indicator.text2 = "Opening project window…"
runDelayed(1000) { if (indicator.isRunning) indicator.stop() }
cont.resume(handle)
return@runProcessWithProgressSynchronously
}

indicator.text = "Waiting for workspace IDE to start..."

val ready = CompletableDeferred<GatewayConnectionHandle?>()
indicator.text = "Workspace IDE has started successfully"
indicator.text2 = "Opening project window…"
runDelayed(1000) { if (indicator.isRunning) indicator.stop() }

thinClient.onClientPresenceChanged.advise(thinClient.lifetime,
onClientPresenceChanged(ready, indicator, handle)
)
thinClient.clientFailedToOpenProject.advise(thinClient.lifetime,
onClientFailedToOpenProject(ready, indicator)
)
thinClient.clientClosed.advise(thinClient.lifetime,
onClientClosed(ready, indicator)
)
ready.invokeOnCompletion { error ->
if (error == null) {
cont.resume(ready.getCompleted())
} else {
cont.resumeWith(Result.failure(error))
}
}

runBlocking {
withTimeoutOrNull(60_000L) { ready.await() } ?: run {
if (ready.isActive) {
indicator.text = "Workspace IDE did not report readiness in time."
ready.completeExceptionally(
RuntimeException("Workspace IDE did not report readiness in time.")
)
}
}
}
cont.resume(handle)
return@runProcessWithProgressSynchronously
} catch (e: Exception) {
DevSpacesConnectionProviderErrors.showDialog(e, ctx, indicator)
runDelayed(2000) { if (indicator.isRunning) indicator.stop() }
Expand All @@ -123,53 +86,6 @@
}
}

private fun onClientPresenceChanged(
ready: CompletableDeferred<GatewayConnectionHandle?>,
indicator: ProgressIndicator,
handle: GatewayConnectionHandle
): (Unit) -> Unit = {
ApplicationManager.getApplication().invokeLater {
if (!ready.isCompleted) {
indicator.text = "Workspace IDE has started successfully"
indicator.text2 = "Opening project window…"
runDelayed(3000) {
if (indicator.isRunning) indicator.stop()
if (ready.isActive) ready.complete(handle)
}
}
}
}

private fun onClientFailedToOpenProject(
ready: CompletableDeferred<GatewayConnectionHandle?>,
indicator: ProgressIndicator
): (Int) -> Unit = { errorCode ->
ApplicationManager.getApplication().invokeLater {
if (!ready.isCompleted) {
indicator.text = "Failed to open remote project (code: $errorCode)"
runDelayed(2000) {
if (indicator.isRunning) indicator.stop()
if (ready.isActive) ready.complete(null)
}
}
}
}

private fun onClientClosed(
ready: CompletableDeferred<GatewayConnectionHandle?>,
indicator: ProgressIndicator
): (Unit) -> Unit = {
ApplicationManager.getApplication().invokeLater {
if (!ready.isCompleted) {
indicator.text = "Workspace IDE closed unexpectedly."
runDelayed(2000) {
if (indicator.isRunning) indicator.stop()
if (ready.isActive) ready.complete(null)
}
}
}
}

@Suppress("UnstableApiUsage")
@Throws(IllegalArgumentException::class)
private fun doConnect(
Expand Down Expand Up @@ -277,4 +193,4 @@
runnable.invoke()
}.start()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ class DevSpacesWorkspacesStepView(
// Progress text stays visible for the whole wait; update so a long poll
// does not look frozen while RemoteIDEServer probes status.
progressIndicator.text =
"Waiting for workspace IDE to become ready (up to ${RemoteIDEServer.readyTimeout}s)..."
"Waiting for workspace IDE to become ready..."
remoteIdeServer.waitServerReady(checkCancelled)
progressIndicator.text = "Reading workspace IDE status..."
remoteIdeServer.getStatus(checkCancelled)
Expand Down
Loading
Loading