From fc3ae59630a7405053b511043ef40b3eb9d0af57 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Tue, 20 May 2025 23:11:50 +0300 Subject: [PATCH] fix: open URLs on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The URLs on Windows failed to be opened because the cmd executed via ProcessExecutor was not correctly constructed. We were calling `exec("cmd", "start \"$url\"")` but in Windows `/c` is also needed to the `cmd`. We originally used native commands to open URLs because Toolbox didn’t support it. Now that LocalDesktopManager provides an API for launching the browser, we no longer need to fix the command-line logic — we can just use the Toolbox API instead. --- CHANGELOG.md | 1 + .../coder/toolbox/CoderRemoteEnvironment.kt | 10 ++- .../com/coder/toolbox/CoderRemoteProvider.kt | 4 +- .../com/coder/toolbox/CoderToolboxContext.kt | 3 +- .../coder/toolbox/CoderToolboxExtension.kt | 2 + .../com/coder/toolbox/browser/BrowserUtil.kt | 73 ++++--------------- .../kotlin/com/coder/toolbox/util/Dialogs.kt | 18 +---- .../coder/toolbox/cli/CoderCLIManagerTest.kt | 2 + .../coder/toolbox/sdk/CoderRestClientTest.kt | 2 + 9 files changed, 34 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1189777..307ee1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixed - `Open web terminal` action is no longer displayed when the workspace is stopped. +- URL links can now be opened in Windows ## 0.2.1 - 2025-05-05 diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index ae9721a..9effe19 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -1,6 +1,6 @@ package com.coder.toolbox -import com.coder.toolbox.browser.BrowserUtil +import com.coder.toolbox.browser.browse import com.coder.toolbox.cli.CoderCLIManager import com.coder.toolbox.cli.SshCommandProcessHandle import com.coder.toolbox.models.WorkspaceAndAgentStatus @@ -74,7 +74,7 @@ class CoderRemoteEnvironment( if (wsRawStatus.canStop()) { actions.add(Action(context.i18n.ptrl("Open web terminal")) { context.cs.launch { - BrowserUtil.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) { + context.desktop.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) { context.ui.showErrorInfoPopup(it) } } @@ -83,7 +83,9 @@ class CoderRemoteEnvironment( actions.add( Action(context.i18n.ptrl("Open in dashboard")) { context.cs.launch { - BrowserUtil.browse(client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString()) { + context.desktop.browse( + client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString() + ) { context.ui.showErrorInfoPopup(it) } } @@ -91,7 +93,7 @@ class CoderRemoteEnvironment( actions.add(Action(context.i18n.ptrl("View template")) { context.cs.launch { - BrowserUtil.browse(client.url.withPath("/templates/${workspace.templateName}").toString()) { + context.desktop.browse(client.url.withPath("/templates/${workspace.templateName}").toString()) { context.ui.showErrorInfoPopup(it) } } diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index c4b3784..952b0dd 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -1,6 +1,6 @@ package com.coder.toolbox -import com.coder.toolbox.browser.BrowserUtil +import com.coder.toolbox.browser.browse import com.coder.toolbox.cli.CoderCLIManager import com.coder.toolbox.sdk.CoderRestClient import com.coder.toolbox.sdk.v2.models.WorkspaceStatus @@ -190,7 +190,7 @@ class CoderRemoteProvider( listOf( Action(context.i18n.ptrl("Create workspace")) { context.cs.launch { - BrowserUtil.browse(client?.url?.withPath("/templates").toString()) { + context.desktop.browse(client?.url?.withPath("/templates").toString()) { context.ui.showErrorInfoPopup(it) } } diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt index 9e5eace..856b88f 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt @@ -5,6 +5,7 @@ import com.coder.toolbox.store.CoderSecretsStore import com.coder.toolbox.store.CoderSettingsStore import com.coder.toolbox.util.toURL 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.ToolboxProxySettings @@ -18,6 +19,7 @@ data class CoderToolboxContext( val envPageManager: EnvironmentUiPageManager, val envStateColorPalette: EnvironmentStateColorPalette, val ideOrchestrator: ClientHelper, + val desktop: LocalDesktopManager, val cs: CoroutineScope, val logger: Logger, val i18n: LocalizableStringFactory, @@ -62,5 +64,4 @@ data class CoderToolboxContext( } else null } } - } diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt index 5ab89a2..05424ae 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt @@ -8,6 +8,7 @@ import com.jetbrains.toolbox.api.core.PluginSettingsStore import com.jetbrains.toolbox.api.core.ServiceLocator import com.jetbrains.toolbox.api.core.diagnostics.Logger import com.jetbrains.toolbox.api.core.getService +import com.jetbrains.toolbox.api.core.os.LocalDesktopManager import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.remoteDev.RemoteDevExtension import com.jetbrains.toolbox.api.remoteDev.RemoteProvider @@ -31,6 +32,7 @@ class CoderToolboxExtension : RemoteDevExtension { serviceLocator.getService(), serviceLocator.getService(), serviceLocator.getService(), + serviceLocator.getService(), serviceLocator.getService(), serviceLocator.getService(), serviceLocator.getService(), diff --git a/src/main/kotlin/com/coder/toolbox/browser/BrowserUtil.kt b/src/main/kotlin/com/coder/toolbox/browser/BrowserUtil.kt index 000263c..f81bba3 100644 --- a/src/main/kotlin/com/coder/toolbox/browser/BrowserUtil.kt +++ b/src/main/kotlin/com/coder/toolbox/browser/BrowserUtil.kt @@ -1,66 +1,19 @@ package com.coder.toolbox.browser -import com.coder.toolbox.util.OS -import com.coder.toolbox.util.getOS -import org.zeroturnaround.exec.ProcessExecutor +import com.jetbrains.toolbox.api.core.os.LocalDesktopManager +import java.net.URI -class BrowserUtil { - companion object { - suspend fun browse(url: String, errorHandler: suspend (BrowserException) -> Unit) { - val os = getOS() - if (os == null) { - errorHandler(BrowserException("Failed to open the URL because we can't detect the OS")) - return - } - when (os) { - OS.LINUX -> linuxBrowse(url, errorHandler) - OS.MAC -> macBrowse(url, errorHandler) - OS.WINDOWS -> windowsBrowse(url, errorHandler) - } - } - private suspend fun linuxBrowse(url: String, errorHandler: suspend (BrowserException) -> Unit) { - try { - if (OS.LINUX.getDesktopEnvironment()?.uppercase()?.contains("GNOME") == true) { - exec("gnome-open", url) - } else { - exec("xdg-open", url) - } - } catch (e: Exception) { - errorHandler( - BrowserException( - "Failed to open URL because an error was encountered. Please make sure xdg-open from package xdg-utils is available!", - e - ) - ) - } - } - - private suspend fun macBrowse(url: String, errorHandler: suspend (BrowserException) -> Unit) { - try { - exec("open", url) - } catch (e: Exception) { - errorHandler(BrowserException("Failed to open URL because an error was encountered.", e)) - } - } - - private suspend fun windowsBrowse(url: String, errorHandler: suspend (BrowserException) -> Unit) { - try { - exec("cmd", "start \"$url\"") - } catch (e: Exception) { - errorHandler(BrowserException("Failed to open URL because an error was encountered.", e)) - } - } - - private fun exec(vararg args: String): String { - val stdout = - ProcessExecutor() - .command(*args) - .exitValues(0) - .readOutput(true) - .execute() - .outputUTF8() - return stdout - } +suspend fun LocalDesktopManager.browse(rawUrl: String, errorHandler: suspend (BrowserException) -> Unit) { + try { + val url = URI.create(rawUrl).toURL() + this.openUrl(url) + } catch (e: Exception) { + errorHandler( + BrowserException( + "Failed to open $rawUrl because an error was encountered", + e + ) + ) } } \ No newline at end of file diff --git a/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt b/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt index 44a3dfb..d3adabc 100644 --- a/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt +++ b/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt @@ -1,7 +1,7 @@ package com.coder.toolbox.util import com.coder.toolbox.CoderToolboxContext -import com.coder.toolbox.browser.BrowserUtil +import com.coder.toolbox.browser.browse import com.jetbrains.toolbox.api.localization.LocalizableString import com.jetbrains.toolbox.api.ui.components.TextType import java.net.URL @@ -23,12 +23,7 @@ class DialogUi(private val context: CoderToolboxContext) { placeholder: LocalizableString? = null, ): String? { return context.ui.showTextInputPopup( - title, - description, - placeholder, - TextType.General, - context.i18n.ptrl("OK"), - context.i18n.ptrl("Cancel") + title, description, placeholder, TextType.General, context.i18n.ptrl("OK"), context.i18n.ptrl("Cancel") ) } @@ -38,17 +33,12 @@ class DialogUi(private val context: CoderToolboxContext) { placeholder: LocalizableString? = null, ): String? { return context.ui.showTextInputPopup( - title, - description, - placeholder, - TextType.Password, - context.i18n.ptrl("OK"), - context.i18n.ptrl("Cancel") + title, description, placeholder, TextType.Password, context.i18n.ptrl("OK"), context.i18n.ptrl("Cancel") ) } private suspend fun openUrl(url: URL) { - BrowserUtil.browse(url.toString()) { + context.desktop.browse(url.toString()) { context.ui.showErrorInfoPopup(it) } } diff --git a/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt b/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt index b8dc145..a7c6f72 100644 --- a/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt +++ b/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt @@ -31,6 +31,7 @@ import com.coder.toolbox.util.pluginTestSettingsStore import com.coder.toolbox.util.sha1 import com.coder.toolbox.util.toURL 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.ToolboxProxySettings @@ -66,6 +67,7 @@ internal class CoderCLIManagerTest { mockk(), mockk(), mockk(), + mockk(), mockk(), mockk(relaxed = true), mockk(), diff --git a/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt b/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt index 0cee720..c32e7b1 100644 --- a/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt +++ b/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt @@ -21,6 +21,7 @@ import com.coder.toolbox.store.TLS_CA_PATH import com.coder.toolbox.util.pluginTestSettingsStore import com.coder.toolbox.util.sslContextFromPEMs 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.ToolboxProxySettings @@ -102,6 +103,7 @@ class CoderRestClientTest { mockk(), mockk(), mockk(), + mockk(), mockk(), mockk(relaxed = true), mockk(),