diff --git a/CHANGELOG.md b/CHANGELOG.md index d161afe..3b6e569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Changed + +- improved workflow when network connection is flaky + ## 0.5.2 - 2025-07-22 ### Fixed diff --git a/gradle.properties b/gradle.properties index e6dc4d8..0becc24 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=0.5.2 +version=0.6.0 group=com.coder.toolbox -name=coder-toolbox +name=coder-toolbox \ No newline at end of file diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index 08f5a07..f8b3a17 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -68,6 +68,13 @@ class CoderRemoteEnvironment( private val proxyCommandHandle = SshCommandProcessHandle(context) private var pollJob: Job? = null + init { + if (context.settingsStore.shouldAutoConnect(id)) { + context.logger.info("resuming SSH connection to $id — last session was still active.") + startSshConnection() + } + } + fun asPairOfWorkspaceAndAgent(): Pair = Pair(workspace, agent) private fun getAvailableActions(): List { @@ -158,6 +165,7 @@ class CoderRemoteEnvironment( override fun beforeConnection() { context.logger.info("Connecting to $id...") isConnected.update { true } + context.settingsStore.updateAutoConnect(this.id, true) pollJob = pollNetworkMetrics() } @@ -180,12 +188,9 @@ class CoderRemoteEnvironment( } context.logger.debug("Loading metrics from ${metricsFile.absolutePath} for $id") try { - val metrics = networkMetricsMarshaller.fromJson(metricsFile.readText()) - if (metrics == null) { - return@launch - } + val metrics = networkMetricsMarshaller.fromJson(metricsFile.readText()) ?: return@launch context.logger.debug("$id metrics: $metrics") - additionalEnvironmentInformation.put(context.i18n.ptrl("Network Status"), metrics.toPretty()) + additionalEnvironmentInformation[context.i18n.ptrl("Network Status")] = metrics.toPretty() } catch (e: Exception) { context.logger.error( e, @@ -203,6 +208,10 @@ class CoderRemoteEnvironment( pollJob?.cancel() this.connectionRequest.update { false } isConnected.update { false } + if (isManual) { + // if the user manually disconnects the ssh connection we should not connect automatically + context.settingsStore.updateAutoConnect(this.id, false) + } context.logger.info("Disconnected from $id") } diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index d68b077..2e5d557 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -80,6 +80,8 @@ class CoderRemoteProvider( ) ) + private val errorBuffer = mutableListOf() + /** * With the provided client, start polling for workspaces. Every time a new * workspace is added, reconfigure SSH using the provided cli (including the @@ -160,23 +162,20 @@ class CoderRemoteProvider( } catch (ex: Exception) { val elapsed = lastPollTime.elapsedNow() if (elapsed > POLL_INTERVAL * 2) { - context.logger.info("wake-up from an OS sleep was detected, going to re-initialize the http client...") - client.setupSession() + context.logger.info("wake-up from an OS sleep was detected") } else { - context.logger.error(ex, "workspace polling error encountered, trying to auto-login") + context.logger.error(ex, "workspace polling error encountered") if (ex is APIResponseException && ex.isTokenExpired) { WorkspaceConnectionManager.shouldEstablishWorkspaceConnections = true + close() + context.envPageManager.showPluginEnvironmentsPage() + errorBuffer.add(ex) + break } - close() - // force auto-login - firstRun = true - context.envPageManager.showPluginEnvironmentsPage() - break } } - // TODO: Listening on a web socket might be better? - select { + select { onTimeout(POLL_INTERVAL) { context.logger.trace("workspace poller waked up by the $POLL_INTERVAL timeout") } @@ -196,9 +195,6 @@ class CoderRemoteProvider( * first page. */ private fun logout() { - // Keep the URL and token to make it easy to log back in, but set - // rememberMe to false so we do not try to automatically log in. - context.secrets.rememberMe = false WorkspaceConnectionManager.reset() close() } @@ -360,22 +356,17 @@ class CoderRemoteProvider( override fun getOverrideUiPage(): UiPage? { // Show the setup page if we have not configured the client yet. if (client == null) { - val errorBuffer = mutableListOf() // When coming back to the application, initializeSession immediately. - val autoSetup = shouldDoAutoSetup() - context.secrets.lastToken.let { lastToken -> - context.secrets.lastDeploymentURL.let { lastDeploymentURL -> - if (autoSetup && lastDeploymentURL.isNotBlank() && (lastToken.isNotBlank() || !settings.requireTokenAuth)) { - try { - CoderCliSetupWizardState.goToStep(WizardStep.CONNECT) - return CoderCliSetupWizardPage(context, settingsPage, visibilityState, true, ::onConnect) - } catch (ex: Exception) { - errorBuffer.add(ex) - } - } + if (shouldDoAutoSetup()) { + try { + CoderCliSetupWizardState.goToStep(WizardStep.CONNECT) + return CoderCliSetupWizardPage(context, settingsPage, visibilityState, true, ::onConnect) + } catch (ex: Exception) { + errorBuffer.add(ex) + } finally { + firstRun = false } } - firstRun = false // Login flow. val setupWizardPage = @@ -384,21 +375,24 @@ class CoderRemoteProvider( errorBuffer.forEach { setupWizardPage.notify("Error encountered", it) } + errorBuffer.clear() // and now reset the errors, otherwise we show it every time on the screen return setupWizardPage } return null } - private fun shouldDoAutoSetup(): Boolean = firstRun && context.secrets.rememberMe == true + /** + * Auto-login only on first the firs run if there is a url & token configured or the auth + * should be done via certificates. + */ + private fun shouldDoAutoSetup(): Boolean = firstRun && (context.secrets.canAutoLogin || !settings.requireTokenAuth) private fun onConnect(client: CoderRestClient, cli: CoderCLIManager) { // Store the URL and token for use next time. context.secrets.lastDeploymentURL = client.url.toString() context.secrets.lastToken = client.token ?: "" context.secrets.storeTokenFor(client.url, context.secrets.lastToken) - // Currently we always remember, but this could be made an option. - context.secrets.rememberMe = true this.client = client pollJob?.cancel() environments.showLoadingMessage() diff --git a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt index e3a882f..1a0f18e 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt @@ -60,7 +60,7 @@ open class CoderRestClient( setupSession() } - fun setupSession() { + private fun setupSession() { moshi = Moshi.Builder() .add(ArchConverter()) diff --git a/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt b/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt index a6d6143..693c1fd 100644 --- a/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt +++ b/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt @@ -146,6 +146,11 @@ interface ReadOnlyCoderSettings { * Return the URL and token from the config, if they exist. */ fun readConfig(dir: Path): Pair + + /** + * Returns whether the SSH connection should be automatically established. + */ + fun shouldAutoConnect(workspaceId: String): Boolean } /** diff --git a/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt b/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt index 3170a06..a807b69 100644 --- a/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt +++ b/src/main/kotlin/com/coder/toolbox/store/CoderSecretsStore.kt @@ -24,9 +24,8 @@ class CoderSecretsStore(private val store: PluginSecretStore) { var lastToken: String get() = get("last-token") set(value) = set("last-token", value) - var rememberMe: Boolean - get() = get("remember-me").toBoolean() - set(value) = set("remember-me", value.toString()) + val canAutoLogin: Boolean + get() = lastDeploymentURL.isNotBlank() && lastToken.isNotBlank() fun tokenFor(url: URL): String? = store[url.host] diff --git a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt index ff56380..0fa4914 100644 --- a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt +++ b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt @@ -142,6 +142,10 @@ class CoderSettingsStore( } } + override fun shouldAutoConnect(workspaceId: String): Boolean { + return store["$SSH_AUTO_CONNECT_PREFIX$workspaceId"]?.toBooleanStrictOrNull() ?: false + } + // a readonly cast fun readOnly(): ReadOnlyCoderSettings = this @@ -213,6 +217,10 @@ class CoderSettingsStore( store[SSH_CONFIG_OPTIONS] = options } + fun updateAutoConnect(workspaceId: String, autoConnect: Boolean) { + store["$SSH_AUTO_CONNECT_PREFIX$workspaceId"] = autoConnect.toString() + } + private fun getDefaultGlobalDataDir(): Path { return when (getOS()) { OS.WINDOWS -> Paths.get(env.get("LOCALAPPDATA"), "coder-toolbox") diff --git a/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt b/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt index 91e3b74..cd1a05d 100644 --- a/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt +++ b/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt @@ -42,3 +42,5 @@ internal const val SSH_CONFIG_OPTIONS = "sshConfigOptions" internal const val NETWORK_INFO_DIR = "networkInfoDir" +internal const val SSH_AUTO_CONNECT_PREFIX = "ssh_auto_connect_" + diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt index e01971c..7ea93e4 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt @@ -128,7 +128,6 @@ class ConnectStep( if (shouldAutoLogin.value) { CoderCliSetupContext.reset() CoderCliSetupWizardState.goToFirstStep() - context.secrets.rememberMe = false } else { if (context.settingsStore.requireTokenAuth) { CoderCliSetupWizardState.goToPreviousStep() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy