Skip to content

Commit e68c4dc

Browse files
committed
Add update check
1 parent 14507c7 commit e68c4dc

File tree

2 files changed

+110
-35
lines changed

2 files changed

+110
-35
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
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

Lines changed: 105 additions & 35 deletions
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,23 +66,70 @@ 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)
70+
var oldParameters: WorkspaceProjectIDE? = null
6271
logger.debug("Creating connection handle", parameters)
6372
indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connecting")
6473
suspendingRetryWithExponentialBackOff(
6574
action = { attempt ->
66-
logger.info("Connecting... (attempt $attempt)")
75+
logger.info("Connecting to remote worker on ${parameters.hostname}... (attempt $attempt)")
6776
if (attempt > 1) {
6877
// indicator.text is the text above the progress bar.
6978
indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connecting.retry", attempt)
79+
} else {
80+
indicator.text = "Connecting to remote worker..."
81+
}
82+
// This establishes an SSH connection to a remote worker binary.
83+
// TODO: Can/should accessors to the same host be shared?
84+
val accessor = HighLevelHostAccessor.create(
85+
RemoteCredentialsHolder().apply {
86+
setHost(CoderCLIManager.getBackgroundHostName(parameters.hostname))
87+
userName = "coder"
88+
port = 22
89+
authType = AuthType.OPEN_SSH
90+
},
91+
true,
92+
)
93+
if (attempt == 1) {
94+
// See if there is a newer (non-EAP) version of the IDE available.
95+
checkUpdate(accessor, parameters, indicator)?.let { update ->
96+
// Store the old IDE to delete later.
97+
oldParameters = parameters
98+
// Continue with the new IDE.
99+
parameters = update.withWorkspaceProject(
100+
name = parameters.name,
101+
hostname = parameters.hostname,
102+
projectPath = parameters.projectPath,
103+
deploymentURL = parameters.deploymentURL,
104+
)
105+
}
70106
}
71107
doConnect(
108+
accessor,
72109
parameters,
73110
indicator,
74111
clientLifetime,
75112
settings.setupCommand,
76113
settings.ignoreSetupFailure,
77114
)
115+
// If successful, delete the old IDE and connection.
116+
oldParameters?.let {
117+
indicator.text = "Deleting ${it.ideName} backend..."
118+
try {
119+
it.idePathOnHost?.let { path ->
120+
accessor.removePathOnRemote(accessor.makeRemotePath(ShellArgument.PlainText(path)))
121+
}
122+
recentConnectionsService.removeConnection(it.toRecentWorkspaceConnection())
123+
} catch (ex: Exception) {
124+
logger.error("Failed to delete old IDE or connection", ex)
125+
}
126+
}
127+
indicator.text = "Connecting ${parameters.ideName} client..."
128+
// The presence handler runs a good deal earlier than the client
129+
// actually appears, which results in some dead space where it can look
130+
// like opening the client silently failed. This delay janks around
131+
// that, so we can keep the progress indicator open a bit longer.
132+
delay(5000)
78133
},
79134
retryIf = {
80135
it is ConnectionException ||
@@ -122,9 +177,38 @@ class CoderRemoteConnectionHandle {
122177
}
123178

124179
/**
125-
* Deploy (if needed), connect to the IDE, and update the last opened date.
180+
* Return a new (non-EAP) IDE if we should update.
181+
*/
182+
private suspend fun checkUpdate(
183+
accessor: HighLevelHostAccessor,
184+
workspace: WorkspaceProjectIDE,
185+
indicator: ProgressIndicator,
186+
): IdeWithStatus? {
187+
indicator.text = "Checking for updates..."
188+
val workspaceOS = accessor.guessOs()
189+
logger.info("Got $workspaceOS for ${workspace.hostname}")
190+
val latest = CachingProductsJsonWrapper.getInstance().getAvailableIdes(
191+
IntelliJPlatformProduct.fromProductCode(workspace.ideProduct.productCode)
192+
?: throw Exception("invalid product code ${workspace.ideProduct.productCode}"),
193+
workspaceOS,
194+
)
195+
.filter { it.releaseType == ReleaseType.RELEASE }
196+
.minOfOrNull { it.toIdeWithStatus() }
197+
if (latest != null && SemVer.parse(latest.buildNumber) > SemVer.parse(workspace.ideBuildNumber)) {
198+
logger.info("Got newer version: ${latest.buildNumber} versus current ${workspace.ideBuildNumber}")
199+
if (confirm("Update IDE", "There is a new version of this IDE: ${latest.buildNumber}", "Would you like to update?")) {
200+
return latest
201+
}
202+
}
203+
return null
204+
}
205+
206+
/**
207+
* Check for updates, deploy (if needed), connect to the IDE, and update the
208+
* last opened date.
126209
*/
127210
private suspend fun doConnect(
211+
accessor: HighLevelHostAccessor,
128212
workspace: WorkspaceProjectIDE,
129213
indicator: ProgressIndicator,
130214
lifetime: LifetimeDefinition,
@@ -134,38 +218,20 @@ class CoderRemoteConnectionHandle {
134218
) {
135219
workspace.lastOpened = localTimeFormatter.format(LocalDateTime.now())
136220

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-
155221
// Deploy if we need to.
156-
val ideDir = this.deploy(workspace, accessor, indicator, timeout)
222+
val ideDir = deploy(accessor, workspace, indicator, timeout)
157223
workspace.idePathOnHost = ideDir.toRawString()
158224

159225
// Run the setup command.
160-
this.setup(workspace, indicator, setupCommand, ignoreSetupFailure)
226+
setup(workspace, indicator, setupCommand, ignoreSetupFailure)
161227

162228
// Wait for the IDE to come up.
163229
indicator.text = "Waiting for ${workspace.ideName} backend..."
164230
var status: UnattendedHostStatus? = null
165231
val remoteProjectPath = accessor.makeRemotePath(ShellArgument.PlainText(workspace.projectPath))
166232
val logsDir = accessor.getLogsDir(workspace.ideProduct.productCode, remoteProjectPath)
167233
while (lifetime.status == LifetimeStatus.Alive) {
168-
status = ensureIDEBackend(workspace, accessor, ideDir, remoteProjectPath, logsDir, lifetime, null)
234+
status = ensureIDEBackend(accessor, workspace, ideDir, remoteProjectPath, logsDir, lifetime, null)
169235
if (!status?.joinLink.isNullOrBlank()) {
170236
break
171237
}
@@ -182,15 +248,25 @@ class CoderRemoteConnectionHandle {
182248
// Make the initial connection.
183249
indicator.text = "Connecting ${workspace.ideName} client..."
184250
logger.info("Connecting ${workspace.ideName} client to coder@${workspace.hostname}:22")
185-
val client = ClientOverSshTunnelConnector(lifetime, SshHostTunnelConnector(credentials))
251+
val client = ClientOverSshTunnelConnector(
252+
lifetime,
253+
SshHostTunnelConnector(
254+
RemoteCredentialsHolder().apply {
255+
setHost(workspace.hostname)
256+
userName = "coder"
257+
port = 22
258+
authType = AuthType.OPEN_SSH
259+
},
260+
),
261+
)
186262
val handle = client.connect(URI(joinLink)) // Downloads the client too, if needed.
187263

188264
// Reconnect if the join link changes.
189265
logger.info("Launched ${workspace.ideName} client; beginning backend monitoring")
190266
lifetime.coroutineScope.launch {
191267
while (isActive) {
192268
delay(5000)
193-
val newStatus = ensureIDEBackend(workspace, accessor, ideDir, remoteProjectPath, logsDir, lifetime, status)
269+
val newStatus = ensureIDEBackend(accessor, workspace, ideDir, remoteProjectPath, logsDir, lifetime, status)
194270
val newLink = newStatus?.joinLink
195271
if (newLink != null && newLink != status?.joinLink) {
196272
logger.info("${workspace.ideName} backend join link changed; updating")
@@ -231,20 +307,14 @@ class CoderRemoteConnectionHandle {
231307
}
232308
}
233309
}
234-
235-
// The presence handler runs a good deal earlier than the client
236-
// actually appears, which results in some dead space where it can look
237-
// like opening the client silently failed. This delay janks around
238-
// that, so we can keep the progress indicator open a bit longer.
239-
delay(5000)
240310
}
241311

242312
/**
243313
* Deploy the IDE if necessary and return the path to its location on disk.
244314
*/
245315
private suspend fun deploy(
246-
workspace: WorkspaceProjectIDE,
247316
accessor: HighLevelHostAccessor,
317+
workspace: WorkspaceProjectIDE,
248318
indicator: ProgressIndicator,
249319
timeout: Duration,
250320
): ShellArgument.RemotePath {
@@ -371,8 +441,8 @@ class CoderRemoteConnectionHandle {
371441
* backend has not started.
372442
*/
373443
private suspend fun ensureIDEBackend(
374-
workspace: WorkspaceProjectIDE,
375444
accessor: HighLevelHostAccessor,
445+
workspace: WorkspaceProjectIDE,
376446
ideDir: ShellArgument.RemotePath,
377447
remoteProjectPath: ShellArgument.RemotePath,
378448
logsDir: ShellArgument.RemotePath,

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy