2
2
3
3
package com.coder.gateway
4
4
5
+ import com.coder.gateway.cli.CoderCLIManager
5
6
import com.coder.gateway.models.WorkspaceProjectIDE
7
+ import com.coder.gateway.models.toIdeWithStatus
6
8
import com.coder.gateway.models.toRawString
9
+ import com.coder.gateway.models.withWorkspaceProject
7
10
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
8
11
import com.coder.gateway.services.CoderSettingsService
12
+ import com.coder.gateway.util.SemVer
13
+ import com.coder.gateway.util.confirm
9
14
import com.coder.gateway.util.humanizeDuration
10
15
import com.coder.gateway.util.isCancellation
11
16
import com.coder.gateway.util.isWorkerTimeout
12
17
import com.coder.gateway.util.suspendingRetryWithExponentialBackOff
13
- import com.coder.gateway.cli.CoderCLIManager
14
18
import com.intellij.openapi.application.ApplicationManager
15
19
import com.intellij.openapi.components.service
16
20
import com.intellij.openapi.diagnostic.Logger
@@ -20,8 +24,12 @@ import com.intellij.openapi.ui.Messages
20
24
import com.intellij.remote.AuthType
21
25
import com.intellij.remote.RemoteCredentialsHolder
22
26
import com.intellij.remoteDev.hostStatus.UnattendedHostStatus
27
+ import com.jetbrains.gateway.ssh.CachingProductsJsonWrapper
23
28
import com.jetbrains.gateway.ssh.ClientOverSshTunnelConnector
24
29
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
25
33
import com.jetbrains.gateway.ssh.SshHostTunnelConnector
26
34
import com.jetbrains.gateway.ssh.deploy.DeployException
27
35
import com.jetbrains.gateway.ssh.deploy.ShellArgument
@@ -58,17 +66,50 @@ class CoderRemoteConnectionHandle {
58
66
val clientLifetime = LifetimeDefinition ()
59
67
clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle .message(" gateway.connector.coder.connection.provider.title" )) {
60
68
try {
61
- val parameters = getParameters(indicator)
69
+ var parameters = getParameters(indicator)
62
70
logger.debug(" Creating connection handle" , parameters)
63
71
indicator.text = CoderGatewayBundle .message(" gateway.connector.coder.connecting" )
64
72
suspendingRetryWithExponentialBackOff(
65
73
action = { attempt ->
66
- logger.info(" Connecting... (attempt $attempt )" )
74
+ logger.info(" Connecting to remote worker on ${parameters.hostname} ... (attempt $attempt )" )
67
75
if (attempt > 1 ) {
68
76
// indicator.text is the text above the progress bar.
69
77
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
+ }
70
110
}
71
111
doConnect(
112
+ accessor,
72
113
parameters,
73
114
indicator,
74
115
clientLifetime,
@@ -122,9 +163,39 @@ class CoderRemoteConnectionHandle {
122
163
}
123
164
124
165
/* *
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.
126
196
*/
127
197
private suspend fun doConnect (
198
+ accessor : HighLevelHostAccessor ,
128
199
workspace : WorkspaceProjectIDE ,
129
200
indicator : ProgressIndicator ,
130
201
lifetime : LifetimeDefinition ,
@@ -134,38 +205,20 @@ class CoderRemoteConnectionHandle {
134
205
) {
135
206
workspace.lastOpened = localTimeFormatter.format(LocalDateTime .now())
136
207
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
-
155
208
// Deploy if we need to.
156
- val ideDir = this . deploy(workspace, accessor , indicator, timeout)
209
+ val ideDir = deploy(accessor, workspace , indicator, timeout)
157
210
workspace.idePathOnHost = ideDir.toRawString()
158
211
159
212
// Run the setup command.
160
- this . setup(workspace, indicator, setupCommand, ignoreSetupFailure)
213
+ setup(workspace, indicator, setupCommand, ignoreSetupFailure)
161
214
162
215
// Wait for the IDE to come up.
163
216
indicator.text = " Waiting for ${workspace.ideName} backend..."
164
217
var status: UnattendedHostStatus ? = null
165
218
val remoteProjectPath = accessor.makeRemotePath(ShellArgument .PlainText (workspace.projectPath))
166
219
val logsDir = accessor.getLogsDir(workspace.ideProduct.productCode, remoteProjectPath)
167
220
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 )
169
222
if (! status?.joinLink.isNullOrBlank()) {
170
223
break
171
224
}
@@ -182,15 +235,25 @@ class CoderRemoteConnectionHandle {
182
235
// Make the initial connection.
183
236
indicator.text = " Connecting ${workspace.ideName} client..."
184
237
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
+ )
186
249
val handle = client.connect(URI (joinLink)) // Downloads the client too, if needed.
187
250
188
251
// Reconnect if the join link changes.
189
252
logger.info(" Launched ${workspace.ideName} client; beginning backend monitoring" )
190
253
lifetime.coroutineScope.launch {
191
254
while (isActive) {
192
255
delay(5000 )
193
- val newStatus = ensureIDEBackend(workspace, accessor , ideDir, remoteProjectPath, logsDir, lifetime, status)
256
+ val newStatus = ensureIDEBackend(accessor, workspace , ideDir, remoteProjectPath, logsDir, lifetime, status)
194
257
val newLink = newStatus?.joinLink
195
258
if (newLink != null && newLink != status?.joinLink) {
196
259
logger.info(" ${workspace.ideName} backend join link changed; updating" )
@@ -243,8 +306,8 @@ class CoderRemoteConnectionHandle {
243
306
* Deploy the IDE if necessary and return the path to its location on disk.
244
307
*/
245
308
private suspend fun deploy (
246
- workspace : WorkspaceProjectIDE ,
247
309
accessor : HighLevelHostAccessor ,
310
+ workspace : WorkspaceProjectIDE ,
248
311
indicator : ProgressIndicator ,
249
312
timeout : Duration ,
250
313
): ShellArgument .RemotePath {
@@ -371,8 +434,8 @@ class CoderRemoteConnectionHandle {
371
434
* backend has not started.
372
435
*/
373
436
private suspend fun ensureIDEBackend (
374
- workspace : WorkspaceProjectIDE ,
375
437
accessor : HighLevelHostAccessor ,
438
+ workspace : WorkspaceProjectIDE ,
376
439
ideDir : ShellArgument .RemotePath ,
377
440
remoteProjectPath : ShellArgument .RemotePath ,
378
441
logsDir : ShellArgument .RemotePath ,
0 commit comments