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" 
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" 
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" 
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