Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

### Changed

Retrieve workspace directly in link handler when using wildcardSSH feature
- Retrieve workspace directly in link handler when using wildcardSSH feature

### Fixed

- installed EAP, RC, NIGHTLY and PREVIEW IDEs are no longer displayed if there is a higher released version available for download.

## 2.19.0 - 2025-02-21

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pluginUntilBuild=251.*
# that exists, ideally the most recent one, for example
# 233.15325-EAP-CANDIDATE-SNAPSHOT).
platformType=GW
platformVersion=233.15619-EAP-CANDIDATE-SNAPSHOT
platformVersion=241.19416-EAP-CANDIDATE-SNAPSHOT
instrumentationCompiler=243.15521-EAP-CANDIDATE-SNAPSHOT
# Gateway does not have open sources.
platformDownloadSources=true
Expand Down
71 changes: 54 additions & 17 deletions src/main/kotlin/com/coder/gateway/models/WorkspaceProjectIDE.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import com.jetbrains.gateway.ssh.IdeStatus
import com.jetbrains.gateway.ssh.IdeWithStatus
import com.jetbrains.gateway.ssh.InstalledIdeUIEx
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
import com.jetbrains.gateway.ssh.ReleaseType
import com.jetbrains.gateway.ssh.deploy.ShellArgument
import java.net.URL
import java.nio.file.Path
import kotlin.io.path.name

private val NON_STABLE_RELEASE_TYPES = setOf("EAP", "RC", "NIGHTLY", "PREVIEW")

/**
* Validated parameters for downloading and opening a project using an IDE on a
* workspace.
Expand Down Expand Up @@ -101,7 +104,8 @@ class WorkspaceProjectIDE(
name = name,
hostname = hostname,
projectPath = projectPath,
ideProduct = IntelliJPlatformProduct.fromProductCode(ideProductCode) ?: throw Exception("invalid product code"),
ideProduct = IntelliJPlatformProduct.fromProductCode(ideProductCode)
?: throw Exception("invalid product code"),
ideBuildNumber = ideBuildNumber,
idePathOnHost = idePathOnHost,
downloadSource = downloadSource,
Expand All @@ -126,13 +130,13 @@ fun RecentWorkspaceConnection.toWorkspaceProjectIDE(): WorkspaceProjectIDE {
// connections page, so it could be missing. Try to get it from the
// host name.
name =
if (name.isNullOrBlank() && !hostname.isNullOrBlank()) {
hostname
.removePrefix("coder-jetbrains--")
.removeSuffix("--${hostname.split("--").last()}")
} else {
name
},
if (name.isNullOrBlank() && !hostname.isNullOrBlank()) {
hostname
.removePrefix("coder-jetbrains--")
.removeSuffix("--${hostname.split("--").last()}")
} else {
name
},
hostname = hostname,
projectPath = projectPath,
ideProductCode = ideProductCode,
Expand All @@ -146,17 +150,17 @@ fun RecentWorkspaceConnection.toWorkspaceProjectIDE(): WorkspaceProjectIDE {
// the config directory). For backwards compatibility with existing
// entries, extract the URL from the config directory or host name.
deploymentURL =
if (deploymentURL.isNullOrBlank()) {
if (!dir.isNullOrBlank()) {
"https://${Path.of(dir).parent.name}"
} else if (!hostname.isNullOrBlank()) {
"https://${hostname.split("--").last()}"
if (deploymentURL.isNullOrBlank()) {
if (!dir.isNullOrBlank()) {
"https://${Path.of(dir).parent.name}"
} else if (!hostname.isNullOrBlank()) {
"https://${hostname.split("--").last()}"
} else {
deploymentURL
}
} else {
deploymentURL
}
} else {
deploymentURL
},
},
lastOpened = lastOpened,
)
}
Expand Down Expand Up @@ -195,6 +199,39 @@ fun AvailableIde.toIdeWithStatus(): IdeWithStatus = IdeWithStatus(
remoteDevType = remoteDevType,
)

/**
* Returns a list of installed IDEs that don't have a RELEASED version available for download.
* Typically, installed EAP, RC, nightly or preview builds should be superseded by released versions.
*/
fun List<InstalledIdeUIEx>.filterOutAvailableReleasedIdes(availableIde: List<AvailableIde>): List<InstalledIdeUIEx> {
val availableReleasedByProductCode = availableIde
.filter { it.releaseType == ReleaseType.RELEASE }
.groupBy { it.product.productCode }
val result = mutableListOf<InstalledIdeUIEx>()

this.forEach { installedIde ->
// installed IDEs have the release type embedded in the presentable version
// which is a string in the form: 2024.2.4 NIGHTLY
if (NON_STABLE_RELEASE_TYPES.any { it in installedIde.presentableVersion }) {
// we can show the installed IDe if there isn't a higher released version available for download
if (installedIde.isSNotSupersededBy(availableReleasedByProductCode[installedIde.product.productCode])) {
result.add(installedIde)
}
} else {
result.add(installedIde)
}
}

return result
}

private fun InstalledIdeUIEx.isSNotSupersededBy(availableIdes: List<AvailableIde>?): Boolean {
if (availableIdes.isNullOrEmpty()) {
return true
}
return !availableIdes.any { it.buildNumber >= this.buildNumber }
}

/**
* Convert an installed IDE to an IDE with status.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.coder.gateway.cli.CoderCLIManager
import com.coder.gateway.icons.CoderIcons
import com.coder.gateway.models.WorkspaceProjectIDE
import com.coder.gateway.models.filterOutAvailableReleasedIdes
import com.coder.gateway.models.toIdeWithStatus
import com.coder.gateway.models.withWorkspaceProject
import com.coder.gateway.sdk.v2.models.Workspace
Expand Down Expand Up @@ -82,9 +83,12 @@
import javax.swing.event.DocumentEvent

// Just extracting the way we display the IDE info into a helper function.
private fun displayIdeWithStatus(ideWithStatus: IdeWithStatus): String = "${ideWithStatus.product.productCode} ${ideWithStatus.presentableVersion} ${ideWithStatus.buildNumber} | ${ideWithStatus.status.name.lowercase(
Locale.getDefault(),
)}"
private fun displayIdeWithStatus(ideWithStatus: IdeWithStatus): String =
"${ideWithStatus.product.productCode} ${ideWithStatus.presentableVersion} ${ideWithStatus.buildNumber} | ${
ideWithStatus.status.name.lowercase(
Locale.getDefault(),
)
}"

/**
* View for a single workspace. In particular, show available IDEs and a button
Expand Down Expand Up @@ -222,12 +226,21 @@
cbIDE.renderer =
if (attempt > 1) {
IDECellRenderer(
CoderGatewayBundle.message("gateway.connector.view.coder.connect-ssh.retry", attempt),
CoderGatewayBundle.message(
"gateway.connector.view.coder.connect-ssh.retry",
attempt
),
)
} else {
IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.connect-ssh"))
}
val executor = createRemoteExecutor(CoderCLIManager(data.client.url).getBackgroundHostName(data.workspace, data.client.me, data.agent))
val executor = createRemoteExecutor(
CoderCLIManager(data.client.url).getBackgroundHostName(
data.workspace,
data.client.me,
data.agent
)
)

if (ComponentValidator.getInstance(tfProject).isEmpty) {
logger.info("Installing remote path validator...")
Expand All @@ -238,7 +251,10 @@
cbIDE.renderer =
if (attempt > 1) {
IDECellRenderer(
CoderGatewayBundle.message("gateway.connector.view.coder.retrieve-ides.retry", attempt),
CoderGatewayBundle.message(
"gateway.connector.view.coder.retrieve-ides.retry",
attempt
),
)
} else {
IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.retrieve-ides"))
Expand All @@ -247,9 +263,9 @@
},
retryIf = {
it is ConnectionException ||
it is TimeoutException ||
it is SSHException ||
it is DeployException
it is TimeoutException ||
it is SSHException ||
it is DeployException
},
onException = { attempt, nextMs, e ->
logger.error("Failed to retrieve IDEs (attempt $attempt; will retry in $nextMs ms)")
Expand All @@ -273,7 +289,7 @@
)

// Check the provided setting to see if there's a default IDE to set.
val defaultIde = ides.find { it ->

Check notice on line 292 in src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Redundant lambda arrow

Redundant lambda arrow
// Using contains on the displayable version of the ide means they can be as specific or as vague as they want
// CL 2023.3.6 233.15619.8 -> a specific Clion build
// CL 2023.3.6 -> a specific Clion version
Expand Down Expand Up @@ -311,7 +327,10 @@
* Validate the remote path whenever it changes.
*/
private fun installRemotePathValidator(executor: HighLevelHostAccessor) {
val disposable = Disposer.newDisposable(ApplicationManager.getApplication(), CoderWorkspaceProjectIDEStepView::class.java.name)
val disposable = Disposer.newDisposable(
ApplicationManager.getApplication(),
CoderWorkspaceProjectIDEStepView::class.java.name
)
ComponentValidator(disposable).installOn(tfProject)

tfProject.document.addDocumentListener(
Expand All @@ -324,7 +343,12 @@
val isPathPresent = validateRemotePath(tfProject.text, executor)
if (isPathPresent.pathOrNull == null) {
ComponentValidator.getInstance(tfProject).ifPresent {
it.updateInfo(ValidationInfo("Can't find directory: ${tfProject.text}", tfProject))
it.updateInfo(
ValidationInfo(
"Can't find directory: ${tfProject.text}",
tfProject
)
)
}
} else {
ComponentValidator.getInstance(tfProject).ifPresent {
Expand All @@ -333,7 +357,12 @@
}
} catch (e: Exception) {
ComponentValidator.getInstance(tfProject).ifPresent {
it.updateInfo(ValidationInfo("Can't validate directory: ${tfProject.text}", tfProject))
it.updateInfo(
ValidationInfo(
"Can't validate directory: ${tfProject.text}",
tfProject
)
)
}
}
}
Expand Down Expand Up @@ -377,27 +406,34 @@
}

logger.info("Resolved OS and Arch for $name is: $workspaceOS")
val installedIdesJob =
cs.async(Dispatchers.IO) {
executor.getInstalledIDEs().map { it.toIdeWithStatus() }
}
val idesWithStatusJob =
cs.async(Dispatchers.IO) {
IntelliJPlatformProduct.entries
.filter { it.showInGateway }
.flatMap { CachingProductsJsonWrapper.getInstance().getAvailableIdes(it, workspaceOS) }
.map { it.toIdeWithStatus() }
}
val installedIdesJob = cs.async(Dispatchers.IO) {
executor.getInstalledIDEs()
}
val availableToDownloadIdesJob = cs.async(Dispatchers.IO) {
IntelliJPlatformProduct.entries
.filter { it.showInGateway }
.flatMap { CachingProductsJsonWrapper.getInstance().getAvailableIdes(it, workspaceOS) }
}

val installedIdes = installedIdesJob.await()
val availableIdes = availableToDownloadIdesJob.await()

val installedIdes = installedIdesJob.await().sorted()
val idesWithStatus = idesWithStatusJob.await().sorted()
if (installedIdes.isEmpty()) {
logger.info("No IDE is installed in $name")
}
if (idesWithStatus.isEmpty()) {
if (availableIdes.isEmpty()) {
logger.warn("Could not resolve any IDE for $name, probably $workspaceOS is not supported by Gateway")
}
return installedIdes + idesWithStatus

val remainingInstalledIdes = installedIdes.filterOutAvailableReleasedIdes(availableIdes)
if (remainingInstalledIdes.size < installedIdes.size) {
logger.info(
"Skipping the following list of installed IDEs because there is already a released version " +
"available for download: ${(installedIdes - remainingInstalledIdes).joinToString { "${it.product.productCode} ${it.presentableVersion}" }}"

Check notice on line 432 in src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Argument could be converted to 'Set' to improve performance

The argument can be converted to 'Set' to improve performance
)
}
return remainingInstalledIdes.map { it.toIdeWithStatus() }.sorted() + availableIdes.map { it.toIdeWithStatus() }
.sorted()
}

private fun toDeployedOS(
Expand Down Expand Up @@ -455,7 +491,8 @@
override fun getSelectedItem(): IdeWithStatus? = super.getSelectedItem() as IdeWithStatus?
}

private class IDECellRenderer(message: String, cellIcon: Icon = AnimatedIcon.Default.INSTANCE) : ListCellRenderer<IdeWithStatus> {
private class IDECellRenderer(message: String, cellIcon: Icon = AnimatedIcon.Default.INSTANCE) :
ListCellRenderer<IdeWithStatus> {
private val loadingComponentRenderer: ListCellRenderer<IdeWithStatus> =
object : ColoredListCellRenderer<IdeWithStatus>() {
override fun customizeCellRenderer(
Expand Down
Loading