Skip to content

Commit 43eb600

Browse files
committed
Add update check
1 parent 14507c7 commit 43eb600

File tree

2 files changed

+97
-29
lines changed

2 files changed

+97
-29
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
## Unreleased
66

7+
### Added
8+
9+
- When using a recent workspace connection, check if there is an update to the
10+
IDE and prompt to upgrade if an upgrade exists.
11+
712
## 2.12.2 - 2024-07-12
813

914
### Fixed

src/main/kotlin/com/coder/gateway/CoderRemoteConnectionHandle.kt

+92-29
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@
22

33
package com.coder.gateway
44

5+
import com.coder.gateway.cli.CoderCLIManager
56
import com.coder.gateway.models.WorkspaceProjectIDE
7+
import com.coder.gateway.models.toIdeWithStatus
68
import com.coder.gateway.models.toRawString
9+
import com.coder.gateway.models.withWorkspaceProject
710
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
811
import com.coder.gateway.services.CoderSettingsService
12+
import com.coder.gateway.util.SemVer
13+
import com.coder.gateway.util.confirm
914
import com.coder.gateway.util.humanizeDuration
1015
import com.coder.gateway.util.isCancellation
1116
import com.coder.gateway.util.isWorkerTimeout
1217
import com.coder.gateway.util.suspendingRetryWithExponentialBackOff
13-
import com.coder.gateway.cli.CoderCLIManager
1418
import com.intellij.openapi.application.ApplicationManager
1519
import com.intellij.openapi.components.service
1620
import com.intellij.openapi.diagnostic.Logger
@@ -20,8 +24,12 @@ import com.intellij.openapi.ui.Messages
2024
import com.intellij.remote.AuthType
2125
import com.intellij.remote.RemoteCredentialsHolder
2226
import com.intellij.remoteDev.hostStatus.UnattendedHostStatus
27+
import com.jetbrains.gateway.ssh.CachingProductsJsonWrapper
2328
import com.jetbrains.gateway.ssh.ClientOverSshTunnelConnector
2429
import com.jetbrains.gateway.ssh.HighLevelHostAccessor
30+
import com.jetbrains.gateway.ssh.IdeWithStatus
31+
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
32+
import com.jetbrains.gateway.ssh.ReleaseType
2533
import com.jetbrains.gateway.ssh.SshHostTunnelConnector
2634
import com.jetbrains.gateway.ssh.deploy.DeployException
2735
import com.jetbrains.gateway.ssh.deploy.ShellArgument
@@ -58,17 +66,50 @@ class CoderRemoteConnectionHandle {
5866
val clientLifetime = LifetimeDefinition()
5967
clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.coder.connection.provider.title")) {
6068
try {
61-
val parameters = getParameters(indicator)
69+
var parameters = getParameters(indicator)
6270
logger.debug("Creating connection handle", parameters)
6371
indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connecting")
6472
suspendingRetryWithExponentialBackOff(
6573
action = { attempt ->
66-
logger.info("Connecting... (attempt $attempt)")
74+
logger.info("Connecting to remote worker on ${parameters.hostname}... (attempt $attempt)")
6775
if (attempt > 1) {
6876
// indicator.text is the text above the progress bar.
6977
indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connecting.retry", attempt)
78+
} else {
79+
indicator.text = "Connecting to remote worker..."
80+
}
81+
// This establishes an SSH connection to a remote worker binary.
82+
// TODO: Can/should accessors to the same host be shared?
83+
val accessor = HighLevelHostAccessor.create(
84+
RemoteCredentialsHolder().apply {
85+
setHost(CoderCLIManager.getBackgroundHostName(parameters.hostname))
86+
userName = "coder"
87+
port = 22
88+
authType = AuthType.OPEN_SSH
89+
},
90+
true,
91+
)
92+
if (attempt == 1) {
93+
// See if there is a newer (non-EAP) version of the IDE available.
94+
checkUpdate(accessor, parameters, indicator)?.let { update ->
95+
// Delete the old IDE.
96+
indicator.text = "Deleting ${parameters.ideName} backend..."
97+
parameters.idePathOnHost?.let { path ->
98+
accessor.removePathOnRemote(accessor.makeRemotePath(ShellArgument.PlainText(path)))
99+
}
100+
// Delete the old connection.
101+
recentConnectionsService.removeConnection(parameters.toRecentWorkspaceConnection())
102+
// Continue with the new IDE.
103+
parameters = update.withWorkspaceProject(
104+
name = parameters.name,
105+
hostname = parameters.hostname,
106+
projectPath = parameters.projectPath,
107+
deploymentURL = parameters.deploymentURL,
108+
)
109+
}
70110
}
71111
doConnect(
112+
accessor,
72113
parameters,
73114
indicator,
74115
clientLifetime,
@@ -122,9 +163,39 @@ class CoderRemoteConnectionHandle {
122163
}
123164

124165
/**
125-
* Deploy (if needed), connect to the IDE, and update the last opened date.
166+
* Return a new (non-EAP) IDE if we should update.
167+
*/
168+
private suspend fun checkUpdate(
169+
accessor: HighLevelHostAccessor,
170+
workspace: WorkspaceProjectIDE,
171+
indicator: ProgressIndicator,
172+
): IdeWithStatus? {
173+
indicator.text = "Checking for updates..."
174+
val workspaceOS = accessor.guessOs()
175+
logger.info("Got $workspaceOS for ${workspace.hostname}")
176+
val availableIdes = CachingProductsJsonWrapper.getInstance().getAvailableIdes(
177+
IntelliJPlatformProduct.fromProductCode(workspace.ideProduct.productCode)
178+
?: throw Exception("invalid product code"),
179+
workspaceOS,
180+
)
181+
.filter { it.releaseType == ReleaseType.RELEASE }
182+
.map { it.toIdeWithStatus() }
183+
val latest = availableIdes.minOrNull()
184+
logger.info("latest: $latest")
185+
if (latest != null && SemVer.parse(latest.buildNumber) > SemVer.parse(workspace.ideBuildNumber)) {
186+
if (confirm("Update IDE", "There is a new version of this IDE: ${latest.buildNumber}", "Would you like to update?")) {
187+
return latest
188+
}
189+
}
190+
return null
191+
}
192+
193+
/**
194+
* Check for updates, deploy (if needed), connect to the IDE, and update the
195+
* last opened date.
126196
*/
127197
private suspend fun doConnect(
198+
accessor: HighLevelHostAccessor,
128199
workspace: WorkspaceProjectIDE,
129200
indicator: ProgressIndicator,
130201
lifetime: LifetimeDefinition,
@@ -134,38 +205,20 @@ class CoderRemoteConnectionHandle {
134205
) {
135206
workspace.lastOpened = localTimeFormatter.format(LocalDateTime.now())
136207

137-
// This establishes an SSH connection to a remote worker binary.
138-
// TODO: Can/should accessors to the same host be shared?
139-
indicator.text = "Connecting to remote worker..."
140-
logger.info("Connecting to remote worker on ${workspace.hostname}")
141-
val credentials = RemoteCredentialsHolder().apply {
142-
setHost(workspace.hostname)
143-
userName = "coder"
144-
port = 22
145-
authType = AuthType.OPEN_SSH
146-
}
147-
val backgroundCredentials = RemoteCredentialsHolder().apply {
148-
setHost(CoderCLIManager.getBackgroundHostName(workspace.hostname))
149-
userName = "coder"
150-
port = 22
151-
authType = AuthType.OPEN_SSH
152-
}
153-
val accessor = HighLevelHostAccessor.create(backgroundCredentials, true)
154-
155208
// Deploy if we need to.
156-
val ideDir = this.deploy(workspace, accessor, indicator, timeout)
209+
val ideDir = deploy(accessor, workspace, indicator, timeout)
157210
workspace.idePathOnHost = ideDir.toRawString()
158211

159212
// Run the setup command.
160-
this.setup(workspace, indicator, setupCommand, ignoreSetupFailure)
213+
setup(workspace, indicator, setupCommand, ignoreSetupFailure)
161214

162215
// Wait for the IDE to come up.
163216
indicator.text = "Waiting for ${workspace.ideName} backend..."
164217
var status: UnattendedHostStatus? = null
165218
val remoteProjectPath = accessor.makeRemotePath(ShellArgument.PlainText(workspace.projectPath))
166219
val logsDir = accessor.getLogsDir(workspace.ideProduct.productCode, remoteProjectPath)
167220
while (lifetime.status == LifetimeStatus.Alive) {
168-
status = ensureIDEBackend(workspace, accessor, ideDir, remoteProjectPath, logsDir, lifetime, null)
221+
status = ensureIDEBackend(accessor, workspace, ideDir, remoteProjectPath, logsDir, lifetime, null)
169222
if (!status?.joinLink.isNullOrBlank()) {
170223
break
171224
}
@@ -182,15 +235,25 @@ class CoderRemoteConnectionHandle {
182235
// Make the initial connection.
183236
indicator.text = "Connecting ${workspace.ideName} client..."
184237
logger.info("Connecting ${workspace.ideName} client to coder@${workspace.hostname}:22")
185-
val client = ClientOverSshTunnelConnector(lifetime, SshHostTunnelConnector(credentials))
238+
val client = ClientOverSshTunnelConnector(
239+
lifetime,
240+
SshHostTunnelConnector(
241+
RemoteCredentialsHolder().apply {
242+
setHost(workspace.hostname)
243+
userName = "coder"
244+
port = 22
245+
authType = AuthType.OPEN_SSH
246+
},
247+
),
248+
)
186249
val handle = client.connect(URI(joinLink)) // Downloads the client too, if needed.
187250

188251
// Reconnect if the join link changes.
189252
logger.info("Launched ${workspace.ideName} client; beginning backend monitoring")
190253
lifetime.coroutineScope.launch {
191254
while (isActive) {
192255
delay(5000)
193-
val newStatus = ensureIDEBackend(workspace, accessor, ideDir, remoteProjectPath, logsDir, lifetime, status)
256+
val newStatus = ensureIDEBackend(accessor, workspace, ideDir, remoteProjectPath, logsDir, lifetime, status)
194257
val newLink = newStatus?.joinLink
195258
if (newLink != null && newLink != status?.joinLink) {
196259
logger.info("${workspace.ideName} backend join link changed; updating")
@@ -243,8 +306,8 @@ class CoderRemoteConnectionHandle {
243306
* Deploy the IDE if necessary and return the path to its location on disk.
244307
*/
245308
private suspend fun deploy(
246-
workspace: WorkspaceProjectIDE,
247309
accessor: HighLevelHostAccessor,
310+
workspace: WorkspaceProjectIDE,
248311
indicator: ProgressIndicator,
249312
timeout: Duration,
250313
): ShellArgument.RemotePath {
@@ -371,8 +434,8 @@ class CoderRemoteConnectionHandle {
371434
* backend has not started.
372435
*/
373436
private suspend fun ensureIDEBackend(
374-
workspace: WorkspaceProjectIDE,
375437
accessor: HighLevelHostAccessor,
438+
workspace: WorkspaceProjectIDE,
376439
ideDir: ShellArgument.RemotePath,
377440
remoteProjectPath: ShellArgument.RemotePath,
378441
logsDir: ShellArgument.RemotePath,

0 commit comments

Comments
 (0)