Skip to content

Commit 5e19a7d

Browse files
committed
impl: finish support for URI handling
The available API up to TBX 2.6.3 was buggy in terms of URI handling. It didn't allow plugins to programmatically install remote ides and launch them. The launch operation only worked when the IDE was already installed and a project was already opened with the IDE. TBX 2.6.3 adds a new API, _RemoteToolboxHelp_ which provides routines for listing the available IDEs on the remote, what is already installed and a command to install specific versions of the IDE. Additionally, there were fixes provided to the existing _ClientHelper_ which now launches the JBClient if a project was not specified. An additional quirk I've discovered is that if we provide a project, and that project was not already opened (present in the Projects tab) the IDE still won't open. And there is no API available to query the available projects. This commit uses the new API to: - query the installed ides - check if the provided ide is in the list of already installed IDEs. - if that's not the case we query the available list of IDEs and the available versions - if the provided ide and build no., is in the available list we will schedule it for install - if not, we select the latest available build number for the provided product code. - wait for the remote IDE to be installed - and then download and launch the JBClient with a project path if it was provided.
1 parent 0df5319 commit 5e19a7d

File tree

7 files changed

+97
-6
lines changed

7 files changed

+97
-6
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Added
6+
7+
- support for Toolbox 2.6.3 with improved URI handling
8+
59
## 0.2.3 - 2025-05-26
610

711
### Changed

src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,21 @@ import com.jetbrains.toolbox.api.core.diagnostics.Logger
77
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
88
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
99
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
10+
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
1011
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
1112
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
1213
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
1314
import com.jetbrains.toolbox.api.ui.ToolboxUi
1415
import kotlinx.coroutines.CoroutineScope
1516
import java.net.URL
1617

18+
@Suppress("UnstableApiUsage")
1719
data class CoderToolboxContext(
1820
val ui: ToolboxUi,
1921
val envPageManager: EnvironmentUiPageManager,
2022
val envStateColorPalette: EnvironmentStateColorPalette,
21-
val ideOrchestrator: ClientHelper,
23+
val remoteIdeOrchestrator: RemoteToolsHelper,
24+
val jbClientOrchestrator: ClientHelper,
2225
val desktop: LocalDesktopManager,
2326
val cs: CoroutineScope,
2427
val logger: Logger,

src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
1313
import com.jetbrains.toolbox.api.remoteDev.RemoteDevExtension
1414
import com.jetbrains.toolbox.api.remoteDev.RemoteProvider
1515
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
16+
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
1617
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
1718
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
1819
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
@@ -31,6 +32,7 @@ class CoderToolboxExtension : RemoteDevExtension {
3132
serviceLocator.getService<ToolboxUi>(),
3233
serviceLocator.getService<EnvironmentUiPageManager>(),
3334
serviceLocator.getService<EnvironmentStateColorPalette>(),
35+
serviceLocator.getService<RemoteToolsHelper>(),
3436
serviceLocator.getService<ClientHelper>(),
3537
serviceLocator.getService<LocalDesktopManager>(),
3638
serviceLocator.getService<CoroutineScope>(),

src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.coder.toolbox.sdk.v2.models.Workspace
1010
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
1111
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
1212
import com.jetbrains.toolbox.api.localization.LocalizableString
13+
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
1314
import kotlinx.coroutines.TimeoutCancellationException
1415
import kotlinx.coroutines.delay
1516
import kotlinx.coroutines.flow.StateFlow
@@ -18,10 +19,13 @@ import kotlinx.coroutines.time.withTimeout
1819
import java.net.HttpURLConnection
1920
import java.net.URI
2021
import java.net.URL
22+
import java.util.UUID
23+
import kotlin.time.Duration
2124
import kotlin.time.Duration.Companion.minutes
2225
import kotlin.time.Duration.Companion.seconds
2326
import kotlin.time.toJavaDuration
2427

28+
@Suppress("UnstableApiUsage")
2529
open class CoderProtocolHandler(
2630
private val context: CoderToolboxContext,
2731
private val dialogUi: DialogUi,
@@ -175,15 +179,67 @@ open class CoderProtocolHandler(
175179
val buildNumber = params.ideBuildNumber()
176180
val projectFolder = params.projectFolder()
177181
if (!productCode.isNullOrBlank() && !buildNumber.isNullOrBlank()) {
182+
var selectedIde = "$productCode-$buildNumber"
178183
context.cs.launch {
179-
val ideVersion = "$productCode-$buildNumber"
180-
context.logger.info("installing $ideVersion on $environmentId")
184+
val installedIdes = context.remoteIdeOrchestrator.getInstalledRemoteTools(environmentId, productCode)
185+
val alreadyInstalled = installedIdes.firstOrNull { it.contains(buildNumber) } != null
186+
if (alreadyInstalled) {
187+
context.logger.info("$productCode-$buildNumber is already on $environmentId. Going to launch JBClient")
188+
} else {
189+
val availableVersions =
190+
context.remoteIdeOrchestrator.getAvailableRemoteTools(environmentId, productCode)
191+
if (availableVersions.isEmpty()) {
192+
val error = IllegalArgumentException("$productCode is not available on $environmentId")
193+
context.logger.error(error, "Error encountered while handling Coder URI")
194+
context.ui.showSnackbar(
195+
UUID.randomUUID().toString(),
196+
context.i18n.ptrl("Error encountered while handling Coder URI"),
197+
context.i18n.pnotr("$productCode is not available on $environmentId"),
198+
context.i18n.ptrl("OK")
199+
)
200+
return@launch
201+
}
202+
203+
val matchingBuildNumber = availableVersions.firstOrNull { it.contains(buildNumber) } != null
204+
if (!matchingBuildNumber) {
205+
selectedIde = availableVersions.maxOf { it }
206+
val msg =
207+
"$productCode-$buildNumber is not available, we've selected the latest $selectedIde"
208+
context.logger.info(msg)
209+
context.ui.showSnackbar(
210+
UUID.randomUUID().toString(),
211+
context.i18n.pnotr("$productCode-$buildNumber not available"),
212+
context.i18n.pnotr(msg),
213+
context.i18n.ptrl("OK")
214+
)
215+
}
216+
217+
// needed otherwise TBX will install it again
218+
if (!installedIdes.contains(selectedIde)) {
219+
context.logger.info("Installing $selectedIde on $environmentId...")
220+
context.remoteIdeOrchestrator.installRemoteTool(environmentId, selectedIde)
221+
if (context.remoteIdeOrchestrator.waitForIdeToBeInstalled(environmentId, selectedIde)) {
222+
context.logger.info("Successfully installed $selectedIde on $environmentId...")
223+
} else {
224+
context.ui.showSnackbar(
225+
UUID.randomUUID().toString(),
226+
context.i18n.pnotr("$selectedIde could not be installed"),
227+
context.i18n.pnotr("$selectedIde could not be installed on time. Check the logs for more details"),
228+
context.i18n.ptrl("OK")
229+
)
230+
}
231+
} else {
232+
context.logger.info("$selectedIde is already present on $environmentId...")
233+
}
234+
}
235+
181236
val job = context.cs.launch {
182-
context.ideOrchestrator.prepareClient(environmentId, ideVersion)
237+
context.logger.info("Downloading and installing JBClient counterpart to $selectedIde locally")
238+
context.jbClientOrchestrator.prepareClient(environmentId, selectedIde)
183239
}
184240
job.join()
185-
context.logger.info("launching $ideVersion on $environmentId")
186-
context.ideOrchestrator.connectToIde(environmentId, ideVersion, projectFolder)
241+
context.logger.info("Launching $selectedIde on $environmentId")
242+
context.jbClientOrchestrator.connectToIde(environmentId, selectedIde, projectFolder)
187243
}
188244
}
189245
}
@@ -203,6 +259,25 @@ open class CoderProtocolHandler(
203259
}
204260
}
205261

262+
private suspend fun RemoteToolsHelper.waitForIdeToBeInstalled(
263+
environmentId: String,
264+
ideHint: String,
265+
waitTime: Duration = 2.minutes
266+
): Boolean {
267+
var isInstalled = false
268+
try {
269+
withTimeout(waitTime.toJavaDuration()) {
270+
while (!isInstalled) {
271+
delay(5.seconds)
272+
isInstalled = getInstalledRemoteTools(environmentId, ideHint).isNotEmpty()
273+
}
274+
}
275+
return true
276+
} catch (_: TimeoutCancellationException) {
277+
return false
278+
}
279+
}
280+
206281
private suspend fun askUrl(): String? {
207282
context.popupPluginMainPage()
208283
return dialogUi.ask(

src/main/resources/localization/defaultMessages.po

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,7 @@ msgid "Network Status"
137137
msgstr ""
138138

139139
msgid "Create workspace"
140+
msgstr ""
141+
142+
msgid "Error encountered while handling Coder URI"
140143
msgstr ""

src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import com.jetbrains.toolbox.api.core.diagnostics.Logger
3434
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
3535
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
3636
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
37+
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
3738
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
3839
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
3940
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
@@ -66,6 +67,7 @@ internal class CoderCLIManagerTest {
6667
mockk<ToolboxUi>(),
6768
mockk<EnvironmentUiPageManager>(),
6869
mockk<EnvironmentStateColorPalette>(),
70+
mockk<RemoteToolsHelper>(),
6971
mockk<ClientHelper>(),
7072
mockk<LocalDesktopManager>(),
7173
mockk<CoroutineScope>(),

src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.jetbrains.toolbox.api.core.diagnostics.Logger
2424
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
2525
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
2626
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
27+
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
2728
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
2829
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
2930
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
@@ -102,6 +103,7 @@ class CoderRestClientTest {
102103
mockk<ToolboxUi>(),
103104
mockk<EnvironmentUiPageManager>(),
104105
mockk<EnvironmentStateColorPalette>(),
106+
mockk<RemoteToolsHelper>(),
105107
mockk<ClientHelper>(),
106108
mockk<LocalDesktopManager>(),
107109
mockk<CoroutineScope>(),

0 commit comments

Comments
 (0)