22
33package com.coder.gateway
44
5+ import com.coder.gateway.cli.CoderCLIManager
56import com.coder.gateway.models.WorkspaceProjectIDE
7+ import com.coder.gateway.models.toIdeWithStatus
68import com.coder.gateway.models.toRawString
9+ import com.coder.gateway.models.withWorkspaceProject
710import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
811import com.coder.gateway.services.CoderSettingsService
12+ import com.coder.gateway.util.SemVer
13+ import com.coder.gateway.util.confirm
914import com.coder.gateway.util.humanizeDuration
1015import com.coder.gateway.util.isCancellation
1116import com.coder.gateway.util.isWorkerTimeout
1217import com.coder.gateway.util.suspendingRetryWithExponentialBackOff
13- import com.coder.gateway.cli.CoderCLIManager
1418import com.intellij.openapi.application.ApplicationManager
1519import com.intellij.openapi.components.service
1620import com.intellij.openapi.diagnostic.Logger
@@ -20,8 +24,12 @@ import com.intellij.openapi.ui.Messages
2024import com.intellij.remote.AuthType
2125import com.intellij.remote.RemoteCredentialsHolder
2226import com.intellij.remoteDev.hostStatus.UnattendedHostStatus
27+ import com.jetbrains.gateway.ssh.CachingProductsJsonWrapper
2328import com.jetbrains.gateway.ssh.ClientOverSshTunnelConnector
2429import 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
2533import com.jetbrains.gateway.ssh.SshHostTunnelConnector
2634import com.jetbrains.gateway.ssh.deploy.DeployException
2735import 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