From 4bacfc4f57cf0dcbcba832804f58ab2e5845a08d Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 27 Apr 2023 14:26:11 -0800 Subject: [PATCH 1/8] Add log when deploying before listing editors --- .../gateway/views/steps/CoderLocateRemoteProjectStepView.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt index 3154bbd4..8290a87e 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -180,6 +180,7 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea ideResolvingJob = cs.launch { val ides = suspendingRetryWithExponentialBackOff( action={ attempt -> + logger.info("Deploying to ${selectedWorkspace.name} on $deploymentURL (attempt $attempt)") // Reset text in the select dropdown. withContext(Dispatchers.Main) { cbIDE.renderer = IDECellRenderer( From 73f9b193a3db19a998649cba1c5aa4598cab36d1 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 27 Apr 2023 17:32:50 -0800 Subject: [PATCH 2/8] Fix timer not canceling It seems to operate in its own little world and I have no idea how to make it stop when the job running it has stopped. --- .../kotlin/com/coder/gateway/sdk/Retry.kt | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/sdk/Retry.kt b/src/main/kotlin/com/coder/gateway/sdk/Retry.kt index 23e6e650..0ae21339 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/Retry.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/Retry.kt @@ -1,11 +1,8 @@ package com.coder.gateway.sdk import kotlinx.coroutines.delay -import kotlinx.datetime.Clock import java.util.Random import java.util.concurrent.TimeUnit -import kotlin.concurrent.timer -import kotlin.math.max import kotlin.math.min /** @@ -27,18 +24,13 @@ suspend fun suspendingRetryWithExponentialBackOff( return action(attempt) } catch (e: Exception) { - val end = Clock.System.now().toEpochMilliseconds() + delayMs - val timer = timer(period = TimeUnit.SECONDS.toMillis(1)) { - val now = Clock.System.now().toEpochMilliseconds() - val next = max(end - now, 0) - if (next > 0) { - update(attempt, next, e) - } else { - this.cancel() - } + var remainingMs = delayMs + while (remainingMs > 0) { + update(attempt, remainingMs, e) + val next = min(remainingMs, TimeUnit.SECONDS.toMillis(1)) + remainingMs -= next + delay(next) } - delay(delayMs) - timer.cancel() delayMs = min(delayMs * backOffFactor, backOffLimitMs) + (random.nextGaussian() * delayMs * backOffJitter).toLong() } } From 405fb438936dde83cc55016af4c413741e8ff775 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 27 Apr 2023 17:33:34 -0800 Subject: [PATCH 3/8] Retry direct connection This will cover recent connections which connect directly without going through the whole setup flow. Pretty much the same logic as for listing editors but we display the errors in different ways since this all happens in a progress dialog. I tried to combine what I could in the retry. Also the SshException is misleading; it seems to wrap the real error so unwrap it otherwise it is impossible to tell what is really wrong. In particular this is causing us to retry on cancelations. --- .../gateway/CoderGatewayConnectionProvider.kt | 49 +++++++++++++++-- .../kotlin/com/coder/gateway/sdk/Retry.kt | 55 ++++++++++++++++--- .../steps/CoderLocateRemoteProjectStepView.kt | 47 +++++----------- .../messages/CoderGatewayBundle.properties | 5 ++ 4 files changed, 111 insertions(+), 45 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt index 07b7b961..cce4c24f 100644 --- a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt +++ b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt @@ -2,19 +2,26 @@ package com.coder.gateway +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() @@ -24,12 +31,42 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider { // 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) { - val context = SshMultistagePanelContext(parameters.toHostDeployInputs()) - logger.info("Deploying and starting IDE with $context") - launch { - @Suppress("UnstableApiUsage") SshDeployFlowUtil.fullDeployCycle( - clientLifetime, context, Duration.ofMinutes(10) - ) + val context = suspendingRetryWithExponentialBackOff( + label = "connect", + logger = logger, + action = { attempt -> + logger.info("Deploying (attempt $attempt)...") + indicator.text = + if (attempt > 1) CoderGatewayBundle.message("gateway.connector.coder.connection.retry.text", attempt) + else CoderGatewayBundle.message("gateway.connector.coder.connection.loading.text") + SshMultistagePanelContext(parameters.toHostDeployInputs()) + }, + predicate = { e -> + e is ConnectionException || e is TimeoutException + || e is SSHException || e is DeployException + }, + update = { _, e, remaining, -> + if (remaining != null) { + indicator.text2 = e?.message ?: CoderGatewayBundle.message("gateway.connector.no-details") + indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connection.retry-error.text", remaining) + } else { + ApplicationManager.getApplication().invokeAndWait { + Messages.showMessageDialog( + e?.message ?: CoderGatewayBundle.message("gateway.connector.no-details"), + CoderGatewayBundle.message("gateway.connector.coder.connection.error.text"), + Messages.getErrorIcon()) + } + } + }, + ) + if (context != null) { + 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) + ) + } } } diff --git a/src/main/kotlin/com/coder/gateway/sdk/Retry.kt b/src/main/kotlin/com/coder/gateway/sdk/Retry.kt index 0ae21339..c0f53b14 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/Retry.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/Retry.kt @@ -1,32 +1,73 @@ package com.coder.gateway.sdk +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.progress.ProcessCanceledException +import com.intellij.ssh.SshException import kotlinx.coroutines.delay import java.util.Random import java.util.concurrent.TimeUnit +import kotlin.coroutines.cancellation.CancellationException import kotlin.math.min +fun unwrap(ex: Exception): Throwable? { + var cause = ex.cause + while(cause?.cause != null) { + cause = cause.cause + } + return cause ?: ex +} + /** - * Similar to Intellij's except it gives you the next delay, does not do its own - * logging, updates periodically (for counting down), and runs forever. + * Similar to Intellij's except it gives you the next delay, logs differently, + * updates periodically (for counting down), runs forever, and takes a + * predicate for determining whether we should retry. + * + * The update will have a boolean to indicate whether it is the first update (so + * things like duplicate logs can be avoided). If remaining is null then no + * more retries will be attempted. + * + * If an exception related to canceling is received then return null. */ suspend fun suspendingRetryWithExponentialBackOff( initialDelayMs: Long = TimeUnit.SECONDS.toMillis(5), backOffLimitMs: Long = TimeUnit.MINUTES.toMillis(3), backOffFactor: Int = 2, backOffJitter: Double = 0.1, - update: (attempt: Int, remainingMs: Long, e: Exception) -> Unit, - action: suspend (attempt: Int) -> T -): T { + label: String, + logger: Logger, + predicate: (e: Throwable?) -> Boolean, + update: (attempt: Int, e: Throwable?, remaining: String?) -> Unit, + action: suspend (attempt: Int) -> T? +): T? { val random = Random() var delayMs = initialDelayMs for (attempt in 1..Int.MAX_VALUE) { try { return action(attempt) } - catch (e: Exception) { + catch (originalEx: Exception) { + // SshException can happen due to anything from a timeout to being + // canceled so unwrap to find out. + val unwrappedEx = if (originalEx is SshException) unwrap(originalEx) else originalEx + when (unwrappedEx) { + is InterruptedException, + is CancellationException, + is ProcessCanceledException -> { + logger.info("Retrying $label canceled due to ${unwrappedEx.javaClass}") + return null + } + } + if (!predicate(unwrappedEx)) { + logger.error("Failed to $label (attempt $attempt; will not retry)", originalEx) + update(attempt, unwrappedEx, null) + return null + } + logger.error("Failed to $label (attempt $attempt; will retry in $delayMs ms)", originalEx) var remainingMs = delayMs while (remainingMs > 0) { - update(attempt, remainingMs, e) + val remainingS = TimeUnit.MILLISECONDS.toSeconds(remainingMs) + val remaining = if (remainingS < 1) "now" else "in $remainingS second${if (remainingS > 1) "s" else ""}" + update(attempt, unwrappedEx, remaining) val next = min(remainingMs, TimeUnit.SECONDS.toMillis(1)) remainingMs -= next delay(next) diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt index 8290a87e..394c722d 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -68,7 +68,6 @@ import net.schmizz.sshj.connection.ConnectionException import java.awt.Component import java.awt.FlowLayout import java.util.Locale -import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import javax.swing.ComboBoxModel import javax.swing.DefaultComboBoxModel @@ -79,7 +78,6 @@ import javax.swing.JPanel import javax.swing.ListCellRenderer import javax.swing.SwingConstants import javax.swing.event.DocumentEvent -import kotlin.coroutines.cancellation.CancellationException class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolean) -> Unit) : CoderWorkspacesWizardStep, Disposable { private val cs = CoroutineScope(Dispatchers.Main) @@ -179,6 +177,8 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea ideResolvingJob = cs.launch { val ides = suspendingRetryWithExponentialBackOff( + label = "retrieve IDEs", + logger = logger, action={ attempt -> logger.info("Deploying to ${selectedWorkspace.name} on $deploymentURL (attempt $attempt)") // Reset text in the select dropdown. @@ -187,39 +187,22 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea if (attempt > 1) CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.retry.text", attempt) else CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.loading.text")) } - try { - val executor = createRemoteExecutor(CoderCLIManager.getHostName(deploymentURL, selectedWorkspace)) - if (ComponentValidator.getInstance(tfProject).isEmpty) { - installRemotePathValidator(executor) - } - retrieveIDEs(executor, selectedWorkspace) - } catch (e: Exception) { - when(e) { - is InterruptedException -> Unit - is CancellationException -> Unit - // Throw to retry these. The main one is - // DeployException which fires when dd times out. - is ConnectionException, is TimeoutException, - is SSHException, is DeployException -> throw e - else -> { - withContext(Dispatchers.Main) { - logger.error("Failed to retrieve IDEs (attempt $attempt)", e) - cbIDEComment.foreground = UIUtil.getErrorForeground() - cbIDEComment.text = e.message ?: "The error did not provide any further details" - cbIDE.renderer = IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.error.text"), UIUtil.getBalloonErrorIcon()) - } - } - } - null + val executor = createRemoteExecutor(CoderCLIManager.getHostName(deploymentURL, selectedWorkspace)) + if (ComponentValidator.getInstance(tfProject).isEmpty) { + installRemotePathValidator(executor) } + retrieveIDEs(executor, selectedWorkspace) + }, + predicate = { e -> + e is ConnectionException || e is TimeoutException + || e is SSHException || e is DeployException }, - update = { attempt, retryMs, e -> - logger.error("Failed to retrieve IDEs (attempt $attempt; will retry in $retryMs ms)", e) + update = { _, e, remaining -> cbIDEComment.foreground = UIUtil.getErrorForeground() - cbIDEComment.text = e.message ?: "The error did not provide any further details" - val delayS = TimeUnit.MILLISECONDS.toSeconds(retryMs) - val delay = if (delayS < 1) "now" else "in $delayS second${if (delayS > 1) "s" else ""}" - cbIDE.renderer = IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.retry-error.text", delay)) + cbIDEComment.text = e?.message ?: CoderGatewayBundle.message("gateway.connector.no-details") + cbIDE.renderer = + if (remaining != null) IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.retry-error.text", remaining)) + else IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.error.text"), UIUtil.getBalloonErrorIcon()) }, ) if (ides != null) { diff --git a/src/main/resources/messages/CoderGatewayBundle.properties b/src/main/resources/messages/CoderGatewayBundle.properties index d8295e5c..32c2090b 100644 --- a/src/main/resources/messages/CoderGatewayBundle.properties +++ b/src/main/resources/messages/CoderGatewayBundle.properties @@ -42,6 +42,10 @@ gateway.connector.recentconnections.new.wizard.button.tooltip=Open a new Coder W gateway.connector.recentconnections.remove.button.tooltip=Remove from Recent Connections gateway.connector.recentconnections.terminal.button.tooltip=Open SSH Web Terminal gateway.connector.coder.connection.provider.title=Connecting to Coder workspace... +gateway.connector.coder.connection.loading.text=Connecting... +gateway.connector.coder.connection.retry.text=Connecting (attempt {0})... +gateway.connector.coder.connection.retry-error.text=Failed to connect...retrying {0} +gateway.connector.coder.connection.error.text=Failed to connect gateway.connector.settings.binary-source.title=CLI source: gateway.connector.settings.binary-source.comment=Used to download the Coder \ CLI which is necessary to make SSH connections. The If-None-Matched header \ @@ -54,3 +58,4 @@ gateway.connector.settings.binary-destination.comment=Directories are created \ here that store the CLI and credentials for each domain to which the plugin \ connects. \ Defaults to {0}. +gateway.connector.no-details="The error did not provide any further details" From 4e5206ee6453ffa4f01d654e5e617a67fddadf24 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 27 Apr 2023 17:40:38 -0800 Subject: [PATCH 4/8] Provide better error when dd times out --- src/main/kotlin/com/coder/gateway/sdk/Retry.kt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/sdk/Retry.kt b/src/main/kotlin/com/coder/gateway/sdk/Retry.kt index c0f53b14..359f0ec7 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/Retry.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/Retry.kt @@ -3,6 +3,7 @@ package com.coder.gateway.sdk import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.ssh.SshException +import com.jetbrains.gateway.ssh.deploy.DeployException import kotlinx.coroutines.delay import java.util.Random import java.util.concurrent.TimeUnit @@ -19,8 +20,9 @@ fun unwrap(ex: Exception): Throwable? { /** * Similar to Intellij's except it gives you the next delay, logs differently, - * updates periodically (for counting down), runs forever, and takes a - * predicate for determining whether we should retry. + * updates periodically (for counting down), runs forever, takes a predicate for + * determining whether we should retry, and has some special handling for + * exceptions to provide the true cause or better messages. * * The update will have a boolean to indicate whether it is the first update (so * things like duplicate logs can be avoided). If remaining is null then no @@ -67,7 +69,15 @@ suspend fun suspendingRetryWithExponentialBackOff( while (remainingMs > 0) { val remainingS = TimeUnit.MILLISECONDS.toSeconds(remainingMs) val remaining = if (remainingS < 1) "now" else "in $remainingS second${if (remainingS > 1) "s" else ""}" - update(attempt, unwrappedEx, remaining) + // When the worker upload times out Gateway just says it failed. + // Even the root cause (IllegalStateException) is useless. The + // error also includes a very long useless tmp path. With all + // that in mind, provide a better error. + val mungedEx = + if (unwrappedEx is DeployException && unwrappedEx.message.contains("Worker binary deploy failed")) + DeployException("Failed to upload worker binary...it may have timed out", unwrappedEx) + else unwrappedEx + update(attempt, mungedEx, remaining) val next = min(remainingMs, TimeUnit.SECONDS.toMillis(1)) remainingMs -= next delay(next) From 88c329719de8b0e2df541da89df5928081d53587 Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 1 May 2023 13:45:39 -0800 Subject: [PATCH 5/8] Throwable does not need to be nullable If there is no cause we use the existing exception so this will never be null. --- .../com/coder/gateway/CoderGatewayConnectionProvider.kt | 4 ++-- src/main/kotlin/com/coder/gateway/sdk/Retry.kt | 6 +++--- .../gateway/views/steps/CoderLocateRemoteProjectStepView.kt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt index cce4c24f..d6c05483 100644 --- a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt +++ b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt @@ -47,12 +47,12 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider { }, update = { _, e, remaining, -> if (remaining != null) { - indicator.text2 = e?.message ?: CoderGatewayBundle.message("gateway.connector.no-details") + indicator.text2 = e.message ?: CoderGatewayBundle.message("gateway.connector.no-details") indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connection.retry-error.text", remaining) } else { ApplicationManager.getApplication().invokeAndWait { Messages.showMessageDialog( - e?.message ?: CoderGatewayBundle.message("gateway.connector.no-details"), + e.message ?: CoderGatewayBundle.message("gateway.connector.no-details"), CoderGatewayBundle.message("gateway.connector.coder.connection.error.text"), Messages.getErrorIcon()) } diff --git a/src/main/kotlin/com/coder/gateway/sdk/Retry.kt b/src/main/kotlin/com/coder/gateway/sdk/Retry.kt index 359f0ec7..e77a4166 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/Retry.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/Retry.kt @@ -10,7 +10,7 @@ import java.util.concurrent.TimeUnit import kotlin.coroutines.cancellation.CancellationException import kotlin.math.min -fun unwrap(ex: Exception): Throwable? { +fun unwrap(ex: Exception): Throwable { var cause = ex.cause while(cause?.cause != null) { cause = cause.cause @@ -37,8 +37,8 @@ suspend fun suspendingRetryWithExponentialBackOff( backOffJitter: Double = 0.1, label: String, logger: Logger, - predicate: (e: Throwable?) -> Boolean, - update: (attempt: Int, e: Throwable?, remaining: String?) -> Unit, + predicate: (e: Throwable) -> Boolean, + update: (attempt: Int, e: Throwable, remaining: String?) -> Unit, action: suspend (attempt: Int) -> T? ): T? { val random = Random() diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt index 394c722d..5ca9ffa2 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -199,7 +199,7 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea }, update = { _, e, remaining -> cbIDEComment.foreground = UIUtil.getErrorForeground() - cbIDEComment.text = e?.message ?: CoderGatewayBundle.message("gateway.connector.no-details") + cbIDEComment.text = e.message ?: CoderGatewayBundle.message("gateway.connector.no-details") cbIDE.renderer = if (remaining != null) IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.retry-error.text", remaining)) else IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.error.text"), UIUtil.getBalloonErrorIcon()) From f417542edc0477bd86e415cc4131d395e8165420 Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 1 May 2023 14:46:52 -0800 Subject: [PATCH 6/8] Break out humanizeDuration --- .../gateway/CoderGatewayConnectionProvider.kt | 7 ++++--- src/main/kotlin/com/coder/gateway/sdk/Retry.kt | 18 ++++++++++++++---- .../steps/CoderLocateRemoteProjectStepView.kt | 5 +++-- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt index d6c05483..b9a63371 100644 --- a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt +++ b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt @@ -2,6 +2,7 @@ package com.coder.gateway +import com.coder.gateway.sdk.humanizeDuration import com.coder.gateway.sdk.suspendingRetryWithExponentialBackOff import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService import com.intellij.openapi.application.ApplicationManager @@ -45,10 +46,10 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider { e is ConnectionException || e is TimeoutException || e is SSHException || e is DeployException }, - update = { _, e, remaining, -> - if (remaining != null) { + update = { _, e, remainingMs -> + if (remainingMs != null) { indicator.text2 = e.message ?: CoderGatewayBundle.message("gateway.connector.no-details") - indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connection.retry-error.text", remaining) + indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connection.retry-error.text", humanizeDuration(remainingMs)) } else { ApplicationManager.getApplication().invokeAndWait { Messages.showMessageDialog( diff --git a/src/main/kotlin/com/coder/gateway/sdk/Retry.kt b/src/main/kotlin/com/coder/gateway/sdk/Retry.kt index e77a4166..16cfd37a 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/Retry.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/Retry.kt @@ -38,7 +38,7 @@ suspend fun suspendingRetryWithExponentialBackOff( label: String, logger: Logger, predicate: (e: Throwable) -> Boolean, - update: (attempt: Int, e: Throwable, remaining: String?) -> Unit, + update: (attempt: Int, e: Throwable, remaining: Long?) -> Unit, action: suspend (attempt: Int) -> T? ): T? { val random = Random() @@ -67,8 +67,6 @@ suspend fun suspendingRetryWithExponentialBackOff( logger.error("Failed to $label (attempt $attempt; will retry in $delayMs ms)", originalEx) var remainingMs = delayMs while (remainingMs > 0) { - val remainingS = TimeUnit.MILLISECONDS.toSeconds(remainingMs) - val remaining = if (remainingS < 1) "now" else "in $remainingS second${if (remainingS > 1) "s" else ""}" // When the worker upload times out Gateway just says it failed. // Even the root cause (IllegalStateException) is useless. The // error also includes a very long useless tmp path. With all @@ -77,7 +75,7 @@ suspend fun suspendingRetryWithExponentialBackOff( if (unwrappedEx is DeployException && unwrappedEx.message.contains("Worker binary deploy failed")) DeployException("Failed to upload worker binary...it may have timed out", unwrappedEx) else unwrappedEx - update(attempt, mungedEx, remaining) + update(attempt, mungedEx, remainingMs) val next = min(remainingMs, TimeUnit.SECONDS.toMillis(1)) remainingMs -= next delay(next) @@ -87,3 +85,15 @@ suspend fun suspendingRetryWithExponentialBackOff( } error("Should never be reached") } + +/** + * Convert a millisecond duration into a human-readable string. + * + * < 1 second: "now" + * 1 second: "in one second" + * > 1 second: "in seconds" + */ +fun humanizeDuration(durationMs: Long): String { + val seconds = TimeUnit.MILLISECONDS.toSeconds(durationMs) + return if (seconds < 1) "now" else "in $seconds second${if (seconds > 1) "s" else ""}" +} diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt index 5ca9ffa2..b5f1f5b4 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -8,6 +8,7 @@ import com.coder.gateway.sdk.Arch import com.coder.gateway.sdk.CoderCLIManager import com.coder.gateway.sdk.CoderRestClientService import com.coder.gateway.sdk.OS +import com.coder.gateway.sdk.humanizeDuration import com.coder.gateway.sdk.suspendingRetryWithExponentialBackOff import com.coder.gateway.sdk.toURL import com.coder.gateway.sdk.withPath @@ -197,11 +198,11 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea e is ConnectionException || e is TimeoutException || e is SSHException || e is DeployException }, - update = { _, e, remaining -> + update = { _, e, remainingMs -> cbIDEComment.foreground = UIUtil.getErrorForeground() cbIDEComment.text = e.message ?: CoderGatewayBundle.message("gateway.connector.no-details") cbIDE.renderer = - if (remaining != null) IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.retry-error.text", remaining)) + if (remainingMs != null) IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.retry-error.text", humanizeDuration(remainingMs))) else IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.error.text"), UIUtil.getBalloonErrorIcon()) }, ) From 1a671579c095f00427b4444f72704b66ba53f93c Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 1 May 2023 14:51:32 -0800 Subject: [PATCH 7/8] Munge error at point of display --- .../gateway/CoderGatewayConnectionProvider.kt | 5 ++++- .../kotlin/com/coder/gateway/sdk/Retry.kt | 19 ++++++++++--------- .../steps/CoderLocateRemoteProjectStepView.kt | 5 ++++- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt index b9a63371..9d254ed6 100644 --- a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt +++ b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt @@ -3,6 +3,7 @@ package com.coder.gateway import com.coder.gateway.sdk.humanizeDuration +import com.coder.gateway.sdk.isWorkerTimeout import com.coder.gateway.sdk.suspendingRetryWithExponentialBackOff import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService import com.intellij.openapi.application.ApplicationManager @@ -48,7 +49,9 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider { }, update = { _, e, remainingMs -> if (remainingMs != null) { - indicator.text2 = e.message ?: CoderGatewayBundle.message("gateway.connector.no-details") + indicator.text2 = + if (isWorkerTimeout(e)) "Failed to upload worker binary...it may have timed out" + else e.message ?: CoderGatewayBundle.message("gateway.connector.no-details") indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connection.retry-error.text", humanizeDuration(remainingMs)) } else { ApplicationManager.getApplication().invokeAndWait { diff --git a/src/main/kotlin/com/coder/gateway/sdk/Retry.kt b/src/main/kotlin/com/coder/gateway/sdk/Retry.kt index 16cfd37a..213f23c8 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/Retry.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/Retry.kt @@ -67,15 +67,7 @@ suspend fun suspendingRetryWithExponentialBackOff( logger.error("Failed to $label (attempt $attempt; will retry in $delayMs ms)", originalEx) var remainingMs = delayMs while (remainingMs > 0) { - // When the worker upload times out Gateway just says it failed. - // Even the root cause (IllegalStateException) is useless. The - // error also includes a very long useless tmp path. With all - // that in mind, provide a better error. - val mungedEx = - if (unwrappedEx is DeployException && unwrappedEx.message.contains("Worker binary deploy failed")) - DeployException("Failed to upload worker binary...it may have timed out", unwrappedEx) - else unwrappedEx - update(attempt, mungedEx, remainingMs) + update(attempt, unwrappedEx, remainingMs) val next = min(remainingMs, TimeUnit.SECONDS.toMillis(1)) remainingMs -= next delay(next) @@ -97,3 +89,12 @@ fun humanizeDuration(durationMs: Long): String { val seconds = TimeUnit.MILLISECONDS.toSeconds(durationMs) return if (seconds < 1) "now" else "in $seconds second${if (seconds > 1) "s" else ""}" } + +/** + * When the worker upload times out Gateway just says it failed. Even the root + * cause (IllegalStateException) is useless. The error also includes a very + * long useless tmp path. Return true if the error looks like this timeout. + */ +fun isWorkerTimeout(e: Throwable): Boolean { + return e is DeployException && e.message.contains("Worker binary deploy failed") +} diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt index b5f1f5b4..cb65dc69 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -9,6 +9,7 @@ import com.coder.gateway.sdk.CoderCLIManager import com.coder.gateway.sdk.CoderRestClientService import com.coder.gateway.sdk.OS import com.coder.gateway.sdk.humanizeDuration +import com.coder.gateway.sdk.isWorkerTimeout import com.coder.gateway.sdk.suspendingRetryWithExponentialBackOff import com.coder.gateway.sdk.toURL import com.coder.gateway.sdk.withPath @@ -200,7 +201,9 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea }, update = { _, e, remainingMs -> cbIDEComment.foreground = UIUtil.getErrorForeground() - cbIDEComment.text = e.message ?: CoderGatewayBundle.message("gateway.connector.no-details") + cbIDEComment.text = + if (isWorkerTimeout(e)) "Failed to upload worker binary...it may have timed out. Check the command log for more details." + else e.message ?: CoderGatewayBundle.message("gateway.connector.no-details") cbIDE.renderer = if (remainingMs != null) IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.retry-error.text", humanizeDuration(remainingMs))) else IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.error.text"), UIUtil.getBalloonErrorIcon()) From d1d3e91a8c5174d2548a75213a5aaad7aaa85b8b Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 1 May 2023 15:45:23 -0800 Subject: [PATCH 8/8] Refactor retry --- .../gateway/CoderGatewayConnectionProvider.kt | 66 +++++++++-------- .../kotlin/com/coder/gateway/sdk/Retry.kt | 63 ++++++++-------- .../steps/CoderLocateRemoteProjectStepView.kt | 71 ++++++++++--------- .../messages/CoderGatewayBundle.properties | 16 ++--- 4 files changed, 119 insertions(+), 97 deletions(-) diff --git a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt index 9d254ed6..cd625208 100644 --- a/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt +++ b/src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt @@ -3,6 +3,7 @@ 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 @@ -33,37 +34,32 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider { // 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) { - val context = suspendingRetryWithExponentialBackOff( - label = "connect", - logger = logger, - action = { attempt -> - logger.info("Deploying (attempt $attempt)...") - indicator.text = - if (attempt > 1) CoderGatewayBundle.message("gateway.connector.coder.connection.retry.text", attempt) - else CoderGatewayBundle.message("gateway.connector.coder.connection.loading.text") - SshMultistagePanelContext(parameters.toHostDeployInputs()) - }, - predicate = { e -> - e is ConnectionException || e is TimeoutException - || e is SSHException || e is DeployException - }, - update = { _, e, remainingMs -> - if (remainingMs != 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") - indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connection.retry-error.text", humanizeDuration(remainingMs)) - } else { - ApplicationManager.getApplication().invokeAndWait { - Messages.showMessageDialog( - e.message ?: CoderGatewayBundle.message("gateway.connector.no-details"), - CoderGatewayBundle.message("gateway.connector.coder.connection.error.text"), - Messages.getErrorIcon()) - } - } - }, - ) - if (context != null) { + }, + 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. @@ -71,6 +67,20 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider { 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()) + } + } } } diff --git a/src/main/kotlin/com/coder/gateway/sdk/Retry.kt b/src/main/kotlin/com/coder/gateway/sdk/Retry.kt index 213f23c8..51d4c04c 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/Retry.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/Retry.kt @@ -1,6 +1,5 @@ package com.coder.gateway.sdk -import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.ssh.SshException import com.jetbrains.gateway.ssh.deploy.DeployException @@ -19,28 +18,35 @@ fun unwrap(ex: Exception): Throwable { } /** - * Similar to Intellij's except it gives you the next delay, logs differently, - * updates periodically (for counting down), runs forever, takes a predicate for - * determining whether we should retry, and has some special handling for - * exceptions to provide the true cause or better messages. + * Similar to Intellij's except it adds two new arguments: onCountdown (for + * displaying the time until the next try) and retryIf (to limit which + * exceptions can be retried). * - * The update will have a boolean to indicate whether it is the first update (so - * things like duplicate logs can be avoided). If remaining is null then no - * more retries will be attempted. + * Exceptions that cannot be retried will be thrown. * - * If an exception related to canceling is received then return null. + * onException and onCountdown will be called immediately on retryable failures. + * onCountdown will also be called every second until the next try with the time + * left until that next try (the last interval might be less than one second if + * the total delay is not divisible by one second). + * + * Some other differences: + * - onException gives you the time until the next try (intended to be logged + * with the error). + * - Infinite tries. + * - SshException is unwrapped. + * + * It is otherwise identical. */ suspend fun suspendingRetryWithExponentialBackOff( initialDelayMs: Long = TimeUnit.SECONDS.toMillis(5), backOffLimitMs: Long = TimeUnit.MINUTES.toMillis(3), backOffFactor: Int = 2, backOffJitter: Double = 0.1, - label: String, - logger: Logger, - predicate: (e: Throwable) -> Boolean, - update: (attempt: Int, e: Throwable, remaining: Long?) -> Unit, - action: suspend (attempt: Int) -> T? -): T? { + retryIf: (e: Throwable) -> Boolean, + onException: (attempt: Int, nextMs: Long, e: Throwable) -> Unit, + onCountdown: (remaining: Long) -> Unit, + action: suspend (attempt: Int) -> T +): T { val random = Random() var delayMs = initialDelayMs for (attempt in 1..Int.MAX_VALUE) { @@ -51,23 +57,13 @@ suspend fun suspendingRetryWithExponentialBackOff( // SshException can happen due to anything from a timeout to being // canceled so unwrap to find out. val unwrappedEx = if (originalEx is SshException) unwrap(originalEx) else originalEx - when (unwrappedEx) { - is InterruptedException, - is CancellationException, - is ProcessCanceledException -> { - logger.info("Retrying $label canceled due to ${unwrappedEx.javaClass}") - return null - } + if (!retryIf(unwrappedEx)) { + throw unwrappedEx } - if (!predicate(unwrappedEx)) { - logger.error("Failed to $label (attempt $attempt; will not retry)", originalEx) - update(attempt, unwrappedEx, null) - return null - } - logger.error("Failed to $label (attempt $attempt; will retry in $delayMs ms)", originalEx) + onException(attempt, delayMs, unwrappedEx) var remainingMs = delayMs while (remainingMs > 0) { - update(attempt, unwrappedEx, remainingMs) + onCountdown(remainingMs) val next = min(remainingMs, TimeUnit.SECONDS.toMillis(1)) remainingMs -= next delay(next) @@ -98,3 +94,12 @@ fun humanizeDuration(durationMs: Long): String { fun isWorkerTimeout(e: Throwable): Boolean { return e is DeployException && e.message.contains("Worker binary deploy failed") } + +/** + * Return true if the exception is some kind of cancellation. + */ +fun isCancellation(e: Throwable): Boolean { + return e is InterruptedException + || e is CancellationException + || e is ProcessCanceledException +} diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt index cb65dc69..3b209edd 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -9,6 +9,7 @@ import com.coder.gateway.sdk.CoderCLIManager import com.coder.gateway.sdk.CoderRestClientService import com.coder.gateway.sdk.OS 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.sdk.toURL @@ -162,6 +163,7 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea // Clear contents from the last attempt if any. cbIDEComment.foreground = UIUtil.getContextHelpForeground() cbIDEComment.text = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.ide.none.comment") + cbIDE.renderer = IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.retrieve-ides")) ideComboBoxModel.removeAllElements() setNextButtonEnabled(false) @@ -178,42 +180,47 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea terminalLink.url = coderClient.coderURL.withPath("/@${coderClient.me.username}/${selectedWorkspace.name}/terminal").toString() ideResolvingJob = cs.launch { - val ides = suspendingRetryWithExponentialBackOff( - label = "retrieve IDEs", - logger = logger, - action={ attempt -> - logger.info("Deploying to ${selectedWorkspace.name} on $deploymentURL (attempt $attempt)") - // Reset text in the select dropdown. - withContext(Dispatchers.Main) { - cbIDE.renderer = IDECellRenderer( - if (attempt > 1) CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.retry.text", attempt) - else CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.loading.text")) - } - val executor = createRemoteExecutor(CoderCLIManager.getHostName(deploymentURL, selectedWorkspace)) - if (ComponentValidator.getInstance(tfProject).isEmpty) { - installRemotePathValidator(executor) - } - retrieveIDEs(executor, selectedWorkspace) - }, - predicate = { e -> - e is ConnectionException || e is TimeoutException - || e is SSHException || e is DeployException - }, - update = { _, e, remainingMs -> - cbIDEComment.foreground = UIUtil.getErrorForeground() - cbIDEComment.text = - if (isWorkerTimeout(e)) "Failed to upload worker binary...it may have timed out. Check the command log for more details." - else e.message ?: CoderGatewayBundle.message("gateway.connector.no-details") - cbIDE.renderer = - if (remainingMs != null) IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.retry-error.text", humanizeDuration(remainingMs))) - else IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.error.text"), UIUtil.getBalloonErrorIcon()) - }, - ) - if (ides != null) { + try { + val ides = suspendingRetryWithExponentialBackOff( + action = { attempt -> + logger.info("Retrieving IDEs...(attempt $attempt)") + if (attempt > 1) { + cbIDE.renderer = IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.retrieve.ides.retry", attempt)) + } + val executor = createRemoteExecutor(CoderCLIManager.getHostName(deploymentURL, selectedWorkspace)) + if (ComponentValidator.getInstance(tfProject).isEmpty) { + installRemotePathValidator(executor) + } + retrieveIDEs(executor, selectedWorkspace) + }, + retryIf = { + it is ConnectionException || it is TimeoutException + || it is SSHException || it is DeployException + }, + onException = { attempt, nextMs, e -> + logger.error("Failed to retrieve IDEs (attempt $attempt; will retry in $nextMs ms)") + cbIDEComment.foreground = UIUtil.getErrorForeground() + cbIDEComment.text = + if (isWorkerTimeout(e)) "Failed to upload worker binary...it may have timed out. Check the command log for more details." + else e.message ?: CoderGatewayBundle.message("gateway.connector.no-details") + }, + onCountdown = { remainingMs -> + cbIDE.renderer = IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.retrieve-ides.failed.retry", humanizeDuration(remainingMs))) + }, + ) withContext(Dispatchers.Main) { ideComboBoxModel.addAll(ides) cbIDE.selectedIndex = 0 } + } catch (e: Exception) { + if (isCancellation(e)) { + logger.info("Connection canceled due to ${e.javaClass}") + } else { + logger.error("Failed to retrieve IDEs (will not retry)", e) + cbIDEComment.foreground = UIUtil.getErrorForeground() + cbIDEComment.text = e.message ?: CoderGatewayBundle.message("gateway.connector.no-details") + cbIDE.renderer = IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.retrieve-ides.failed"), UIUtil.getBalloonErrorIcon()) + } } } } diff --git a/src/main/resources/messages/CoderGatewayBundle.properties b/src/main/resources/messages/CoderGatewayBundle.properties index 32c2090b..c5e7e8b0 100644 --- a/src/main/resources/messages/CoderGatewayBundle.properties +++ b/src/main/resources/messages/CoderGatewayBundle.properties @@ -28,10 +28,10 @@ gateway.connector.view.workspaces.token.comment=The last used token is shown abo gateway.connector.view.workspaces.token.rejected=This token was rejected. gateway.connector.view.workspaces.token.injected=This token was pulled from your CLI config. gateway.connector.view.workspaces.token.none=No existing token found. -gateway.connector.view.coder.remoteproject.loading.text=Retrieving products... -gateway.connector.view.coder.remoteproject.retry.text=Retrieving products (attempt {0})... -gateway.connector.view.coder.remoteproject.error.text=Failed to retrieve IDEs -gateway.connector.view.coder.remoteproject.retry-error.text=Failed to retrieve IDEs...retrying {0} +gateway.connector.view.coder.retrieve-ides=Retrieving IDEs... +gateway.connector.view.coder.retrieve.ides.retry=Retrieving IDEs (attempt {0})... +gateway.connector.view.coder.retrieve-ides.failed=Failed to retrieve IDEs +gateway.connector.view.coder.retrieve-ides.failed.retry=Failed to retrieve IDEs...retrying {0} gateway.connector.view.coder.remoteproject.next.text=Start IDE and connect gateway.connector.view.coder.remoteproject.choose.text=Choose IDE and project for workspace {0} gateway.connector.view.coder.remoteproject.ide.download.comment=This IDE will be downloaded from jetbrains.com and installed to the default path on the remote host. @@ -42,10 +42,10 @@ gateway.connector.recentconnections.new.wizard.button.tooltip=Open a new Coder W gateway.connector.recentconnections.remove.button.tooltip=Remove from Recent Connections gateway.connector.recentconnections.terminal.button.tooltip=Open SSH Web Terminal gateway.connector.coder.connection.provider.title=Connecting to Coder workspace... -gateway.connector.coder.connection.loading.text=Connecting... -gateway.connector.coder.connection.retry.text=Connecting (attempt {0})... -gateway.connector.coder.connection.retry-error.text=Failed to connect...retrying {0} -gateway.connector.coder.connection.error.text=Failed to connect +gateway.connector.coder.connecting=Connecting... +gateway.connector.coder.connecting.retry=Connecting (attempt {0})... +gateway.connector.coder.connection.failed=Failed to connect +gateway.connector.coder.connecting.failed.retry=Failed to connect...retrying {0} gateway.connector.settings.binary-source.title=CLI source: gateway.connector.settings.binary-source.comment=Used to download the Coder \ CLI which is necessary to make SSH connections. The If-None-Matched header \