Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Break connection code out into separate class
The connect entrypoint is used both for Gateway
links (jetbrains-gateway://) and internally in our own code at the end
of the wizard and for recent connections, but this will be two separate
sets of parameters (internally we have the CLI set up and pass around
the hostname while the links will only have the workspace ID or name).

So break out the code into a separate class that we can call internally
which will let us dedicate the connect entrypoint to handle the Gateway
links.  There we will set up the CLI and gather the required parameters
before calling the now-broken-out code.
  • Loading branch information
code-asher committed Aug 3, 2023
commit bc0ce3fd69979892096776abac793ea6b3d54f94
Original file line number Diff line number Diff line change
Expand Up @@ -2,90 +2,15 @@

package com.coder.gateway

import com.coder.gateway.sdk.humanizeDuration
import com.coder.gateway.sdk.isCancellation
import com.coder.gateway.sdk.isWorkerTimeout
import com.coder.gateway.sdk.suspendingRetryWithExponentialBackOff
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.rd.util.launchUnderBackgroundProgress
import com.intellij.openapi.ui.Messages
import com.jetbrains.gateway.api.ConnectionRequestor
import com.jetbrains.gateway.api.GatewayConnectionHandle
import com.jetbrains.gateway.api.GatewayConnectionProvider
import com.jetbrains.gateway.api.GatewayUI
import com.jetbrains.gateway.ssh.SshDeployFlowUtil
import com.jetbrains.gateway.ssh.SshMultistagePanelContext
import com.jetbrains.gateway.ssh.deploy.DeployException
import com.jetbrains.rd.util.lifetime.LifetimeDefinition
import kotlinx.coroutines.launch
import net.schmizz.sshj.common.SSHException
import net.schmizz.sshj.connection.ConnectionException
import java.time.Duration
import java.util.concurrent.TimeoutException

class CoderGatewayConnectionProvider : GatewayConnectionProvider {
private val recentConnectionsService = service<CoderRecentWorkspaceConnectionsService>()

override suspend fun connect(parameters: Map<String, String>, requestor: ConnectionRequestor): GatewayConnectionHandle? {
val clientLifetime = LifetimeDefinition()
// TODO: If this fails determine if it is an auth error and if so prompt
// for a new token, configure the CLI, then try again.
clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.coder.connection.provider.title"), canBeCancelled = true, isIndeterminate = true, project = null) {
try {
indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connecting")
val context = suspendingRetryWithExponentialBackOff(
action = { attempt ->
logger.info("Connecting... (attempt $attempt")
if (attempt > 1) {
// indicator.text is the text above the progress bar.
indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connecting.retry", attempt)
}
SshMultistagePanelContext(parameters.toHostDeployInputs())
},
retryIf = {
it is ConnectionException || it is TimeoutException
|| it is SSHException || it is DeployException
},
onException = { attempt, nextMs, e ->
logger.error("Failed to connect (attempt $attempt; will retry in $nextMs ms)")
// indicator.text2 is the text below the progress bar.
indicator.text2 =
if (isWorkerTimeout(e)) "Failed to upload worker binary...it may have timed out"
else e.message ?: CoderGatewayBundle.message("gateway.connector.no-details")
},
onCountdown = { remainingMs ->
indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connecting.failed.retry", humanizeDuration(remainingMs))
},
)
launch {
logger.info("Deploying and starting IDE with $context")
// At this point JetBrains takes over with their own UI.
@Suppress("UnstableApiUsage") SshDeployFlowUtil.fullDeployCycle(
clientLifetime, context, Duration.ofMinutes(10)
)
}
} catch (e: Exception) {
if (isCancellation(e)) {
logger.info("Connection canceled due to ${e.javaClass}")
} else {
logger.info("Failed to connect (will not retry)", e)
// The dialog will close once we return so write the error
// out into a new dialog.
ApplicationManager.getApplication().invokeAndWait {
Messages.showMessageDialog(
e.message ?: CoderGatewayBundle.message("gateway.connector.no-details"),
CoderGatewayBundle.message("gateway.connector.coder.connection.failed"),
Messages.getErrorIcon())
}
}
}
}

recentConnectionsService.addRecentConnection(parameters.toRecentWorkspaceConnection())
GatewayUI.getInstance().reset()
logger.debug("Launched Coder connection provider", parameters)
CoderRemoteConnectionHandle().connect(parameters)
return null
}

Expand Down
92 changes: 92 additions & 0 deletions src/main/kotlin/com/coder/gateway/CoderRemoteConnectionHandle.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
@file:Suppress("DialogTitleCapitalization")

package com.coder.gateway

import com.coder.gateway.sdk.humanizeDuration
import com.coder.gateway.sdk.isCancellation
import com.coder.gateway.sdk.isWorkerTimeout
import com.coder.gateway.sdk.suspendingRetryWithExponentialBackOff
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.rd.util.launchUnderBackgroundProgress
import com.intellij.openapi.ui.Messages
import com.jetbrains.gateway.ssh.SshDeployFlowUtil
import com.jetbrains.gateway.ssh.SshMultistagePanelContext
import com.jetbrains.gateway.ssh.deploy.DeployException
import com.jetbrains.rd.util.lifetime.LifetimeDefinition
import kotlinx.coroutines.launch
import net.schmizz.sshj.common.SSHException
import net.schmizz.sshj.connection.ConnectionException
import java.time.Duration
import java.util.concurrent.TimeoutException

// CoderRemoteConnection uses the provided workspace SSH parameters to launch an
// IDE against the workspace. If successful the connection is added to recent
// connections.
class CoderRemoteConnectionHandle {
private val recentConnectionsService = service<CoderRecentWorkspaceConnectionsService>()

suspend fun connect(parameters: Map<String, String>) {
logger.debug("Creating connection handle", parameters)
val clientLifetime = LifetimeDefinition()
// TODO: If this fails determine if it is an auth error and if so prompt
// for a new token, configure the CLI, then try again.
clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.coder.connection.provider.title"), canBeCancelled = true, isIndeterminate = true, project = null) {
try {
indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connecting")
val context = suspendingRetryWithExponentialBackOff(
action = { attempt ->
logger.info("Connecting... (attempt $attempt")
if (attempt > 1) {
// indicator.text is the text above the progress bar.
indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connecting.retry", attempt)
}
SshMultistagePanelContext(parameters.toHostDeployInputs())
},
retryIf = {
it is ConnectionException || it is TimeoutException
|| it is SSHException || it is DeployException
},
onException = { attempt, nextMs, e ->
logger.error("Failed to connect (attempt $attempt; will retry in $nextMs ms)")
// indicator.text2 is the text below the progress bar.
indicator.text2 =
if (isWorkerTimeout(e)) "Failed to upload worker binary...it may have timed out"
else e.message ?: CoderGatewayBundle.message("gateway.connector.no-details")
},
onCountdown = { remainingMs ->
indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connecting.failed.retry", humanizeDuration(remainingMs))
},
)
launch {
logger.info("Deploying and starting IDE with $context")
// At this point JetBrains takes over with their own UI.
@Suppress("UnstableApiUsage") SshDeployFlowUtil.fullDeployCycle(
clientLifetime, context, Duration.ofMinutes(10)
)
}
recentConnectionsService.addRecentConnection(parameters.toRecentWorkspaceConnection())
} catch (e: Exception) {
if (isCancellation(e)) {
logger.info("Connection canceled due to ${e.javaClass}")
} else {
logger.info("Failed to connect (will not retry)", e)
// The dialog will close once we return so write the error
// out into a new dialog.
ApplicationManager.getApplication().invokeAndWait {
Messages.showMessageDialog(
e.message ?: CoderGatewayBundle.message("gateway.connector.no-details"),
CoderGatewayBundle.message("gateway.connector.coder.connection.failed"),
Messages.getErrorIcon())
}
}
}
}
}

companion object {
val logger = Logger.getInstance(CoderRemoteConnectionHandle::class.java.simpleName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package com.coder.gateway.views

import com.coder.gateway.CoderGatewayBundle
import com.coder.gateway.CoderGatewayConstants
import com.coder.gateway.CoderRemoteConnectionHandle
import com.coder.gateway.icons.CoderIcons
import com.coder.gateway.models.RecentWorkspaceConnection
import com.coder.gateway.models.WorkspaceAgentModel
Expand Down Expand Up @@ -215,7 +216,8 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
icon(product.icon)
cell(ActionLink(connectionDetails.projectPath!!) {
cs.launch {
GatewayUI.getInstance().connect(connectionDetails.toWorkspaceParams())
CoderRemoteConnectionHandle().connect(connectionDetails.toWorkspaceParams())
GatewayUI.getInstance().reset()
}
})
label("").resizableColumn().align(AlignX.FILL)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.coder.gateway.views.steps

import com.coder.gateway.CoderGatewayBundle
import com.coder.gateway.CoderRemoteConnectionHandle
import com.coder.gateway.icons.CoderIcons
import com.coder.gateway.models.CoderWorkspacesWizardModel
import com.coder.gateway.models.WorkspaceAgentModel
Expand Down Expand Up @@ -338,7 +339,7 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
return false
}
cs.launch {
GatewayUI.getInstance().connect(
CoderRemoteConnectionHandle().connect(
selectedIDE
.toWorkspaceParams()
.withWorkspaceHostname(CoderCLIManager.getHostName(deploymentURL, selectedWorkspace))
Expand All @@ -347,6 +348,7 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
.withConfigDirectory(wizardModel.configDirectory)
.withName(selectedWorkspace.name)
)
GatewayUI.getInstance().reset()
}
return true
}
Expand Down