Skip to content

Commit ce3f67b

Browse files
committed
impl: support for latest_eap, latest_release and latest_installed placeholders
This PR adds support for the IDE placeholders in the URI build number parameter. This is a request coming from Netflix, the placeholders provide an easier way to fill in the build number from the web dashboard without having to know the available versions of IDE from Toolbox. IDE launch flow is refactored to support dynamic build selectors, and it also improves the install/launch logic. When latest_eap or latest_release is used: - prefer the newest matching version that is available for install - install it only if it isn’t already installed - fall back to the latest available version if no match exists - show an error if none of the above happens When latest_installed is used: - launch the newest installed version - if none are installed, install and launch the latest available version - show an error if none of the above happens
1 parent d5bf957 commit ce3f67b

File tree

1 file changed

+111
-44
lines changed

1 file changed

+111
-44
lines changed

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

Lines changed: 111 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.coder.toolbox.util
33
import com.coder.toolbox.CoderToolboxContext
44
import com.coder.toolbox.cli.CoderCLIManager
55
import com.coder.toolbox.feed.IdeFeedManager
6+
import com.coder.toolbox.feed.IdeType
67
import com.coder.toolbox.models.WorkspaceAndAgentStatus
78
import com.coder.toolbox.sdk.CoderRestClient
89
import com.coder.toolbox.sdk.v2.models.Workspace
@@ -27,7 +28,7 @@ private const val CAN_T_HANDLE_URI_TITLE = "Can't handle URI"
2728
@Suppress("UnstableApiUsage")
2829
open class CoderProtocolHandler(
2930
private val context: CoderToolboxContext,
30-
ideFeedManager: IdeFeedManager,
31+
private val ideFeedManager: IdeFeedManager,
3132
) {
3233
private val settings = context.settingsStore.readOnly()
3334

@@ -236,72 +237,136 @@ open class CoderProtocolHandler(
236237
private fun launchIde(
237238
environmentId: String,
238239
productCode: String,
239-
buildNumber: String,
240+
buildNumberHint: String,
240241
projectFolder: String?
241242
) {
242243
context.cs.launch(CoroutineName("Launch Remote IDE")) {
243-
val selectedIde = selectAndInstallRemoteIde(productCode, buildNumber, environmentId) ?: return@launch
244-
context.logger.info("$productCode-$buildNumber is already on $environmentId. Going to launch JBClient")
244+
val selectedIde = selectAndInstallRemoteIde(productCode, buildNumberHint, environmentId) ?: return@launch
245+
context.logger.info("Selected IDE $selectedIde for $productCode with hint $buildNumberHint")
246+
247+
// Ensure JBClient is prepared (installed/downloaded locally)
245248
installJBClient(selectedIde, environmentId).join()
249+
250+
// Launch
246251
launchJBClient(selectedIde, environmentId, projectFolder)
247252
}
248253
}
249254

250255
private suspend fun selectAndInstallRemoteIde(
251256
productCode: String,
252-
buildNumber: String,
257+
buildNumberHint: String,
253258
environmentId: String
254259
): String? {
255-
val installedIdes = context.remoteIdeOrchestrator.getInstalledRemoteTools(environmentId, productCode)
260+
val selectedIdeVersion = resolveIdeIdentifier(environmentId, productCode, buildNumberHint) ?: return null
261+
val installedIdeVersions = context.remoteIdeOrchestrator.getInstalledRemoteTools(environmentId, productCode)
262+
263+
if (installedIdeVersions.contains(selectedIdeVersion)) {
264+
context.logger.info("$selectedIdeVersion is already installed on $environmentId")
265+
return selectedIdeVersion
266+
}
267+
268+
val selectedIde = "$productCode-$selectedIdeVersion"
269+
context.logger.info("Installing $selectedIde on $environmentId...")
270+
context.remoteIdeOrchestrator.installRemoteTool(environmentId, selectedIde)
256271

257-
var selectedIde = "$productCode-$buildNumber"
258-
if (installedIdes.firstOrNull { it.contains(buildNumber) } != null) {
259-
context.logger.info("$selectedIde is already installed on $environmentId")
272+
if (context.remoteIdeOrchestrator.waitForIdeToBeInstalled(environmentId, selectedIde)) {
273+
context.logger.info("Successfully installed $selectedIdeVersion on $environmentId.")
260274
return selectedIde
275+
} else {
276+
context.ui.showSnackbar(
277+
UUID.randomUUID().toString(),
278+
context.i18n.pnotr("$selectedIde could not be installed"),
279+
context.i18n.pnotr("$selectedIde could not be installed on time. Check the logs for more details"),
280+
context.i18n.ptrl("OK")
281+
)
282+
return null
261283
}
284+
}
262285

263-
selectedIde = resolveAvailableIde(environmentId, productCode, buildNumber) ?: return null
286+
/**
287+
* Resolves the full IDE identifier (e.g., "RR-241.14494.240") based on the build hint.
288+
* Supports: latest_eap, latest_release, latest_installed, or specific build number.
289+
*/
290+
internal suspend fun resolveIdeIdentifier(
291+
environmentId: String,
292+
productCode: String,
293+
buildNumberHint: String
294+
): String? {
295+
val availableBuilds = context.remoteIdeOrchestrator.getAvailableRemoteTools(environmentId, productCode)
296+
297+
when (buildNumberHint) {
298+
"latest_eap" -> {
299+
// Use IdeFeedManager to find best EAP match from available builds
300+
val bestEap = ideFeedManager.findBestMatch(
301+
productCode,
302+
IdeType.EAP,
303+
availableBuilds
304+
)
264305

265-
// needed otherwise TBX will install it again
266-
if (!installedIdes.contains(selectedIde)) {
267-
context.logger.info("Installing $selectedIde on $environmentId...")
268-
context.remoteIdeOrchestrator.installRemoteTool(environmentId, selectedIde)
306+
return if (bestEap != null) {
307+
bestEap.build
308+
} else {
309+
// Fallback to latest available if valid
310+
if (availableBuilds.isEmpty()) {
311+
context.logAndShowError(
312+
CAN_T_HANDLE_URI_TITLE,
313+
"$productCode is not available on $environmentId"
314+
)
315+
return null
316+
}
317+
val fallback = availableBuilds.maxBy { it }
318+
context.logger.info("No EAP found for $productCode, falling back to latest available: $fallback")
319+
fallback
320+
}
321+
}
269322

270-
if (context.remoteIdeOrchestrator.waitForIdeToBeInstalled(environmentId, selectedIde)) {
271-
context.logger.info("Successfully installed $selectedIde on $environmentId...")
272-
return selectedIde
273-
} else {
274-
context.ui.showSnackbar(
275-
UUID.randomUUID().toString(),
276-
context.i18n.pnotr("$selectedIde could not be installed"),
277-
context.i18n.pnotr("$selectedIde could not be installed on time. Check the logs for more details"),
278-
context.i18n.ptrl("OK")
323+
"latest_release" -> {
324+
val bestRelease = ideFeedManager.findBestMatch(
325+
productCode,
326+
IdeType.RELEASE,
327+
availableBuilds
279328
)
280-
return null
329+
330+
return if (bestRelease != null) {
331+
bestRelease.build
332+
} else {
333+
// Fallback to latest available if valid
334+
if (availableBuilds.isEmpty()) {
335+
context.logAndShowError(
336+
CAN_T_HANDLE_URI_TITLE,
337+
"$productCode is not available on $environmentId"
338+
)
339+
return null
340+
}
341+
val fallback = availableBuilds.maxBy { it }
342+
context.logger.info("No Release found for $productCode, falling back to latest available: $fallback")
343+
fallback
344+
}
281345
}
282-
} else {
283-
context.logger.info("$selectedIde is already present on $environmentId...")
284-
return selectedIde
285-
}
286-
}
287346

288-
private suspend fun resolveAvailableIde(environmentId: String, productCode: String, buildNumber: String): String? {
289-
val availableVersions = context
290-
.remoteIdeOrchestrator
291-
.getAvailableRemoteTools(environmentId, productCode)
347+
"latest_installed" -> {
348+
val installed = context.remoteIdeOrchestrator.getInstalledRemoteTools(environmentId, productCode)
349+
if (installed.isNotEmpty()) {
350+
return installed.maxBy { it }
351+
}
352+
// Fallback to latest available if valid
353+
if (availableBuilds.isEmpty()) {
354+
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "$productCode is not available on $environmentId")
355+
return null
356+
}
357+
val fallback = availableBuilds.maxBy { it }
358+
context.logger.info("No installed IDE found, falling back to latest available: $fallback")
359+
return fallback
360+
}
292361

293-
if (availableVersions.isEmpty()) {
294-
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "$productCode is not available on $environmentId")
295-
return null
296-
}
362+
else -> {
363+
// Specific build number
364+
// Check if exact match exists in available or installed (implicitly handled by install check later)
365+
// Often the input buildNumber might be just "241" or "241.1234", but full build version is in the form of 241.1234.234"
297366

298-
val buildNumberIsNotAvailable = availableVersions.firstOrNull { it.contains(buildNumber) } == null
299-
if (buildNumberIsNotAvailable) {
300-
val selectedIde = availableVersions.maxOf { it }
301-
context.logger.info("$productCode-$buildNumber is not available, we've selected the latest $selectedIde")
302-
return selectedIde
367+
return availableBuilds.filter { it.contains(buildNumberHint) }.maxByOrNull { it }
368+
}
303369
}
304-
return "$productCode-$buildNumber"
305370
}
306371

307372
private fun installJBClient(selectedIde: String, environmentId: String): Job =
@@ -340,7 +405,9 @@ open class CoderProtocolHandler(
340405
withTimeout(waitTime.toJavaDuration()) {
341406
while (!isInstalled) {
342407
delay(5.seconds)
343-
isInstalled = getInstalledRemoteTools(environmentId, ideHint).isNotEmpty()
408+
val installed = getInstalledRemoteTools(environmentId, ideHint) // Hint matching
409+
// Check if *specific* IDE is installed now
410+
isInstalled = installed.contains(ideHint) || installed.any { it.contains(ideHint) }
344411
}
345412
}
346413
return true

0 commit comments

Comments
 (0)