diff --git a/CHANGELOG.md b/CHANGELOG.md index b699b66..f77a155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +### Added + +- support for basic authentication for HTTP/HTTPS proxy +- support for Toolbox 2.7 release + +### Changed + +- improved message while loading the workspace + ## 0.3.2 - 2025-06-25 ### Changed diff --git a/README.md b/README.md index 8bffe5b..a86a8a0 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,8 @@ If `ide_product_code` and `ide_build_number` is missing, Toolbox will only open page. Coder Toolbox will attempt to start the workspace if it’s not already running; however, for the most reliable experience, it’s recommended to ensure the workspace is running prior to initiating the connection. -> ⚠️ Note: `folder` should point to a remote IDEA project that has already been opened and appears in the `Projects` +> [!NOTE] +> `folder` should point to a remote IDEA project that has already been opened and appears in the `Projects` > tab. > If the path refers to a project that doesn't exist, the remote IDE won’t start or load it. @@ -110,7 +111,7 @@ experience, it’s recommended to ensure the workspace is running prior to initi ## Configuring and Testing workspace polling with HTTP & SOCKS5 Proxy -This section explains how to set up a local proxy (without authentication which is not yet supported) and verify that +This section explains how to set up a local proxy and verify that the plugin’s REST client works correctly when routed through it. We’ll use [mitmproxy](https://mitmproxy.org/) for this — it can act as both an HTTP and SOCKS5 proxy with SSL @@ -134,6 +135,12 @@ mitmproxy can do HTTP and SOCKS5 proxying. To configure one or the other: 2. Navigate to `Options -> Edit Options` 3. Update the `Mode` field to `regular` in order to activate HTTP/HTTPS or to `socks5` 4. Proxy authentication can be enabled by updating the `proxyauth` to `username:password` +5. Alternatively you can run the following commands: + +```bash +mitmweb --ssl-insecure --set stream_large_bodies="10m" --mode regular --proxyauth proxyUsername:proxyPassword +mitmweb --ssl-insecure --set stream_large_bodies="10m" --mode socks5 +``` ### Configure Proxy in Toolbox @@ -144,6 +151,11 @@ mitmproxy can do HTTP and SOCKS5 proxying. To configure one or the other: 5. Before authenticating to the Coder deployment we need to tell the plugin where can we find mitmproxy certificates. In Coder's Settings page, set the `TLS CA path` to `~/.mitmproxy/mitmproxy-ca-cert.pem` +> [!NOTE] +> Coder Toolbox plugin handles only HTTP/HTTPS proxy authentication. +> SOCKS5 proxy authentication is currently not supported due to limitations +> described in: https://youtrack.jetbrains.com/issue/TBX-14532/Missing-proxy-authentication-settings#focus=Comments-27-12265861.0-0 + ## Debugging and Reporting issues Enabling debug logging is essential for diagnosing issues with the Toolbox plugin, especially when SSH diff --git a/gradle.properties b/gradle.properties index b2b2959..efbc54f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=0.3.2 +version=0.4.0 group=com.coder.toolbox name=coder-toolbox diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f46170..3c410f3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,15 +1,15 @@ [versions] -toolbox-plugin-api = "1.1.41749" -kotlin = "2.1.10" -coroutines = "1.10.1" -serialization = "1.8.0" +toolbox-plugin-api = "1.3.47293" +kotlin = "2.1.20" +coroutines = "1.10.2" +serialization = "1.8.1" okhttp = "4.12.0" dependency-license-report = "2.9" marketplace-client = "2.0.46" gradle-wrapper = "0.14.0" exec = "1.12" moshi = "1.15.2" -ksp = "2.1.10-1.0.31" +ksp = "2.1.20-2.0.1" retrofit = "3.0.0" changelog = "2.2.1" gettext = "0.7.0" diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index 101cf71..be4c40a 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -7,6 +7,7 @@ import com.coder.toolbox.sdk.ex.APIResponseException import com.coder.toolbox.sdk.v2.models.WorkspaceStatus import com.coder.toolbox.util.CoderProtocolHandler import com.coder.toolbox.util.DialogUi +import com.coder.toolbox.util.waitForTrue import com.coder.toolbox.util.withPath import com.coder.toolbox.views.Action import com.coder.toolbox.views.CoderCliSetupWizardPage @@ -63,9 +64,10 @@ class CoderRemoteProvider( // On the first load, automatically log in if we can. private var firstRun = true private val isInitialized: MutableStateFlow = MutableStateFlow(false) - private var coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(context.deploymentUrl.toString())) + private val coderHeaderPage = NewEnvironmentPage(context.i18n.pnotr(context.deploymentUrl.toString())) private val linkHandler = CoderProtocolHandler(context, dialogUi, isInitialized) + override val loadingEnvironmentsDescription: LocalizableString = context.i18n.ptrl("Loading workspaces...") override val environments: MutableStateFlow>> = MutableStateFlow( LoadableState.Loading ) @@ -167,7 +169,7 @@ class CoderRemoteProvider( close() // force auto-login firstRun = true - goToEnvironmentsPage() + context.envPageManager.showPluginEnvironmentsPage() break } } @@ -315,27 +317,19 @@ class CoderRemoteProvider( ) { restClient, cli -> // stop polling and de-initialize resources close() + isInitialized.update { + false + } // start initialization with the new settings this@CoderRemoteProvider.client = restClient - coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(restClient.url.toString())) + coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString())) environments.showLoadingMessage() pollJob = poll(restClient, cli) + isInitialized.waitForTrue() } } - /** - * Make Toolbox ask for the page again. Use any time we need to change the - * root page (for example, sign-in or the environment list). - * - * When moving between related pages, instead use ui.showUiPage() and - * ui.hideUiPage() which stacks and has built-in back navigation, rather - * than using multiple root pages. - */ - private fun goToEnvironmentsPage() { - context.envPageManager.showPluginEnvironmentsPage() - } - /** * Return the sign-in page if we do not have a valid client. @@ -377,7 +371,7 @@ class CoderRemoteProvider( private fun shouldDoAutoSetup(): Boolean = firstRun && context.secrets.rememberMe == true - private suspend fun onConnect(client: CoderRestClient, cli: CoderCLIManager) { + 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 ?: "" @@ -387,9 +381,9 @@ class CoderRemoteProvider( this.client = client pollJob?.cancel() environments.showLoadingMessage() - coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(client.url.toString())) + coderHeaderPage.setTitle(context.i18n.pnotr(client.url.toString())) pollJob = poll(client, cli) - context.refreshMainPage() + context.envPageManager.showPluginEnvironmentsPage() } private fun MutableStateFlow>>.showLoadingMessage() { diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt index 0bb4135..4291321 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt @@ -3,7 +3,6 @@ package com.coder.toolbox import com.coder.toolbox.store.CoderSecretsStore import com.coder.toolbox.store.CoderSettingsStore import com.coder.toolbox.util.toURL -import com.coder.toolbox.views.CoderPage import com.jetbrains.toolbox.api.core.diagnostics.Logger import com.jetbrains.toolbox.api.core.os.LocalDesktopManager import com.jetbrains.toolbox.api.localization.LocalizableStringFactory @@ -14,10 +13,8 @@ import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager import com.jetbrains.toolbox.api.ui.ToolboxUi import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay import java.net.URL import java.util.UUID -import kotlin.time.Duration.Companion.milliseconds @Suppress("UnstableApiUsage") data class CoderToolboxContext( @@ -91,26 +88,4 @@ data class CoderToolboxContext( i18n.ptrl("OK") ) } - - /** - * Forces the title bar on the main page to be refreshed - */ - suspend fun refreshMainPage() { - // the url/title on the main page is only refreshed if - // we're navigating to the main env page from another page. - // If TBX is already on the main page the title is not refreshed - // hence we force a navigation from a blank page. - ui.showUiPage(CoderPage.emptyPage(this)) - - - // Toolbox uses an internal shared flow with a buffer of 4 items and a DROP_OLDEST strategy. - // Both showUiPage and showPluginEnvironmentsPage send events to this flow. - // If we emit two events back-to-back, the first one often gets dropped and only the second is shown. - // To reduce this risk, we add a small delay to let the UI coroutine process the first event. - // Simply yielding the coroutine isn't reliable, especially right after Toolbox starts via URI handling. - // Based on my testing, a 5–10 ms delay is enough to ensure the blank page is processed, - // while still short enough to be invisible to users. - delay(10.milliseconds) - envPageManager.showPluginEnvironmentsPage() - } } diff --git a/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt b/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt index cc04dfe..28cc14b 100644 --- a/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt +++ b/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt @@ -65,9 +65,10 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) { return CustomRemoteEnvironmentStateV2( context.i18n.pnotr(label), color = getStateColor(context), - reachable = ready() || unhealthy(), + isReachable = ready() || unhealthy(), // TODO@JB: How does this work? Would like a spinner for pending states. - icon = getStateIcon() + iconId = getStateIcon().id, + isPriorityShow = true ) } diff --git a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt index 365e1ed..9aa3dfb 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt @@ -24,7 +24,9 @@ import com.coder.toolbox.util.coderTrustManagers import com.coder.toolbox.util.getArch import com.coder.toolbox.util.getHeaders import com.coder.toolbox.util.getOS +import com.jetbrains.toolbox.api.remoteDev.connection.ProxyAuth import com.squareup.moshi.Moshi +import okhttp3.Credentials import okhttp3.OkHttpClient import retrofit2.Response import retrofit2.Retrofit @@ -78,18 +80,19 @@ open class CoderRestClient( builder.proxySelector(context.proxySettings.getProxySelector()!!) } - //TODO - add support for proxy auth. when Toolbox exposes them -// builder.proxyAuthenticator { _, response -> -// if (proxyValues.useAuth && proxyValues.username != null && proxyValues.password != null) { -// val credentials = Credentials.basic(proxyValues.username, proxyValues.password) -// response.request.newBuilder() -// .header("Proxy-Authorization", credentials) -// .build() -// } else { -// null -// } -// } -// } + // Note: This handles only HTTP/HTTPS proxy authentication. + // SOCKS5 proxy authentication is currently not supported due to limitations described in: + // https://youtrack.jetbrains.com/issue/TBX-14532/Missing-proxy-authentication-settings#focus=Comments-27-12265861.0-0 + builder.proxyAuthenticator { _, response -> + val proxyAuth = context.proxySettings.getProxyAuth() + if (proxyAuth == null || proxyAuth !is ProxyAuth.Basic) { + return@proxyAuthenticator null + } + val credentials = Credentials.basic(proxyAuth.username, proxyAuth.password) + response.request.newBuilder() + .header("Proxy-Authorization", credentials) + .build() + } if (token != null) { builder = builder.addInterceptor { diff --git a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt index 7d24029..c87c47e 100644 --- a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt +++ b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt @@ -67,12 +67,12 @@ open class CoderProtocolHandler( val workspace = restClient.workspaces().matchName(workspaceName, deploymentURL) ?: return val cli = configureCli(deploymentURL, restClient) - reInitialize(restClient, cli) var agent: WorkspaceAgent try { markAsBusy() - context.refreshMainPage() + reInitialize(restClient, cli) + context.envPageManager.showPluginEnvironmentsPage() if (!prepareWorkspace(workspace, restClient, workspaceName, deploymentURL)) return // we resolve the agent after the workspace is started otherwise we can get misleading // errors like: no agent available while workspace is starting or stopping @@ -86,6 +86,7 @@ open class CoderProtocolHandler( } finally { unmarkAsBusy() } + delay(2.seconds) val environmentId = "${workspace.name}.${agent.name}" context.showEnvironmentPage(environmentId) diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt index c6193da..5115204 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt @@ -25,7 +25,7 @@ class CoderCliSetupWizardPage( client: CoderRestClient, cli: CoderCLIManager, ) -> Unit, -) : CoderPage(context.i18n.ptrl("Setting up Coder"), false) { +) : CoderPage(MutableStateFlow(context.i18n.ptrl("Setting up Coder")), false) { private val shouldAutoSetup = MutableStateFlow(initialAutoSetup) private val settingsAction = Action(context.i18n.ptrl("Settings"), actionBlock = { context.ui.showUiPage(settingsPage) diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt index 9b83f45..363d618 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt @@ -7,6 +7,7 @@ import com.jetbrains.toolbox.api.localization.LocalizableString import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription import com.jetbrains.toolbox.api.ui.components.UiPage import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update /** * Base page that handles the icon, displaying error notifications, and @@ -19,9 +20,15 @@ import kotlinx.coroutines.flow.MutableStateFlow * to use the mouse. */ abstract class CoderPage( - title: LocalizableString, + private val titleObservable: MutableStateFlow, showIcon: Boolean = true, -) : UiPage(title) { +) : UiPage(titleObservable) { + + fun setTitle(title: LocalizableString) { + titleObservable.update { + title + } + } /** * Return the icon, if showing one. diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt index de2ce0b..61827be 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.launch * I have not been able to test this page. */ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel) : - CoderPage(context.i18n.ptrl("Coder Settings"), false) { + CoderPage(MutableStateFlow(context.i18n.ptrl("Coder Settings")), false) { private val settings = context.settingsStore.readOnly() // TODO: Copy over the descriptions, holding until I can test this page. diff --git a/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt b/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt index 83e07c7..cde23b2 100644 --- a/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt @@ -1,6 +1,5 @@ package com.coder.toolbox.views -import com.coder.toolbox.CoderToolboxContext import com.jetbrains.toolbox.api.localization.LocalizableString import com.jetbrains.toolbox.api.ui.components.UiField import kotlinx.coroutines.flow.MutableStateFlow @@ -14,7 +13,7 @@ import kotlinx.coroutines.flow.StateFlow * For now we just use this to display the deployment URL since we do not * support creating environments from the plugin. */ -class NewEnvironmentPage(context: CoderToolboxContext, deploymentURL: LocalizableString) : - CoderPage(deploymentURL) { +class NewEnvironmentPage(deploymentURL: LocalizableString) : + CoderPage(MutableStateFlow(deploymentURL)) { override val fields: StateFlow> = MutableStateFlow(emptyList()) } diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index fe1f90c..787424c 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -146,4 +146,7 @@ msgid "Error encountered while setting up Coder" msgstr "" msgid "Setting up Coder" +msgstr "" + +msgid "Loading workspaces..." msgstr "" \ No newline at end of file diff --git a/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt b/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt index 2727228..c42ead2 100644 --- a/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt +++ b/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt @@ -24,6 +24,7 @@ import com.jetbrains.toolbox.api.core.diagnostics.Logger import com.jetbrains.toolbox.api.core.os.LocalDesktopManager import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper +import com.jetbrains.toolbox.api.remoteDev.connection.ProxyAuth import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette @@ -114,6 +115,8 @@ class CoderRestClientTest { object : ToolboxProxySettings { override fun getProxy(): Proxy? = null override fun getProxySelector(): ProxySelector? = null + override fun getProxyAuth(): ProxyAuth? = null + override fun addProxyChangeListener(listener: Runnable) { } @@ -579,6 +582,7 @@ class CoderRestClientTest { } } + override fun getProxyAuth(): ProxyAuth? = null override fun addProxyChangeListener(listener: Runnable) { }