@@ -24,6 +24,7 @@ import TelemetryReporter from './telemetryReporter';
24
24
import { addHostToHostFile , checkNewHostInHostkeys } from './ssh/hostfile' ;
25
25
import { checkDefaultIdentityFiles } from './ssh/identityFiles' ;
26
26
import { HeartbeatManager } from './heartbeat' ;
27
+ import { getGitpodVersion , isFeatureSupported } from './featureSupport' ;
27
28
28
29
interface SSHConnectionParams {
29
30
workspaceId : string ;
@@ -430,11 +431,12 @@ export default class RemoteConnector extends Disposable {
430
431
431
432
private async getWorkspaceSSHDestination ( accessToken : string , { workspaceId, gitpodHost } : SSHConnectionParams ) : Promise < { destination : string ; password ?: string } > {
432
433
const serviceUrl = new URL ( gitpodHost ) ;
434
+ const gitpodVersion = await getGitpodVersion ( gitpodHost ) ;
433
435
434
436
const [ workspaceInfo , ownerToken , registeredSSHKeys ] = await withServerApi ( accessToken , serviceUrl . toString ( ) , service => Promise . all ( [
435
437
service . server . getWorkspace ( workspaceId ) ,
436
438
service . server . getOwnerToken ( workspaceId ) ,
437
- service . server . getSSHPublicKeys ( )
439
+ isFeatureSupported ( gitpodVersion , 'SSHPublicKeys' ) ? service . server . getSSHPublicKeys ( ) : undefined
438
440
] ) , this . logger ) ;
439
441
440
442
if ( workspaceInfo . latestInstance ?. status ?. phase !== 'running' ) {
@@ -505,47 +507,32 @@ export default class RemoteConnector extends Disposable {
505
507
let identityFilePaths = await checkDefaultIdentityFiles ( ) ;
506
508
this . logger . trace ( `Default identity files:` , identityFilePaths . length ? identityFilePaths . toString ( ) : 'None' ) ;
507
509
508
- const keyFingerprints = registeredSSHKeys . map ( i => i . fingerprint ) ;
509
- const publickKeyFiles = await Promise . allSettled ( identityFilePaths . map ( path => fs . promises . readFile ( path + '.pub' ) ) ) ;
510
- identityFilePaths = identityFilePaths . filter ( ( _ , index ) => {
511
- const result = publickKeyFiles [ index ] ;
512
- if ( result . status === 'rejected' ) {
513
- return false ;
514
- }
510
+ if ( registeredSSHKeys ) {
511
+ const keyFingerprints = registeredSSHKeys . map ( i => i . fingerprint ) ;
512
+ const publickKeyFiles = await Promise . allSettled ( identityFilePaths . map ( path => fs . promises . readFile ( path + '.pub' ) ) ) ;
513
+ identityFilePaths = identityFilePaths . filter ( ( _ , index ) => {
514
+ const result = publickKeyFiles [ index ] ;
515
+ if ( result . status === 'rejected' ) {
516
+ return false ;
517
+ }
515
518
516
- const parsedResult = sshUtils . parseKey ( result . value ) ;
517
- if ( parsedResult instanceof Error || ! parsedResult ) {
518
- this . logger . error ( `Error while parsing SSH public key${ identityFilePaths [ index ] + '.pub' } :` , parsedResult ) ;
519
- return false ;
520
- }
519
+ const parsedResult = sshUtils . parseKey ( result . value ) ;
520
+ if ( parsedResult instanceof Error || ! parsedResult ) {
521
+ this . logger . error ( `Error while parsing SSH public key${ identityFilePaths [ index ] + '.pub' } :` , parsedResult ) ;
522
+ return false ;
523
+ }
521
524
522
- const parsedKey = Array . isArray ( parsedResult ) ? parsedResult [ 0 ] : parsedResult ;
523
- const fingerprint = crypto . createHash ( 'sha256' ) . update ( parsedKey . getPublicSSH ( ) ) . digest ( 'base64' ) ;
524
- return keyFingerprints . includes ( fingerprint ) ;
525
- } ) ;
526
- this . logger . trace ( `Registered public keys in Gitpod account:` , identityFilePaths . length ? identityFilePaths . toString ( ) : 'None' ) ;
527
-
528
- // Commented this for now as `checkDefaultIdentityFiles` seems enough
529
- // Connect to the OpenSSH agent and check for registered keys
530
- // let sshKeys: ParsedKey[] | undefined;
531
- // try {
532
- // if (process.env['SSH_AUTH_SOCK']) {
533
- // sshKeys = await new Promise<ParsedKey[]>((resolve, reject) => {
534
- // const sshAgent = new OpenSSHAgent(process.env['SSH_AUTH_SOCK']!);
535
- // sshAgent.getIdentities((err, publicKeys) => {
536
- // if (err) {
537
- // reject(err);
538
- // } else {
539
- // resolve(publicKeys!);
540
- // }
541
- // });
542
- // });
543
- // } else {
544
- // this.logger.error(`'SSH_AUTH_SOCK' env variable not defined, cannot connect to OpenSSH agent`);
545
- // }
546
- // } catch (e) {
547
- // this.logger.error(`Couldn't get identities from OpenSSH agent`, e);
548
- // }
525
+ const parsedKey = Array . isArray ( parsedResult ) ? parsedResult [ 0 ] : parsedResult ;
526
+ const fingerprint = crypto . createHash ( 'sha256' ) . update ( parsedKey . getPublicSSH ( ) ) . digest ( 'base64' ) ;
527
+ return keyFingerprints . includes ( fingerprint ) ;
528
+ } ) ;
529
+ this . logger . trace ( `Registered public keys in Gitpod account:` , identityFilePaths . length ? identityFilePaths . toString ( ) : 'None' ) ;
530
+ } else {
531
+ if ( identityFilePaths . length ) {
532
+ sshDestInfo . user = `${ workspaceId } #${ ownerToken } ` ;
533
+ }
534
+ this . logger . warn ( `Registered SSH public keys not supported in ${ gitpodHost } , using version ${ gitpodVersion } ` ) ;
535
+ }
549
536
550
537
return {
551
538
destination : Buffer . from ( JSON . stringify ( sshDestInfo ) , 'utf8' ) . toString ( 'hex' ) ,
@@ -664,9 +651,17 @@ export default class RemoteConnector extends Disposable {
664
651
this . logger . info ( `Updated 'gitpod.host' setting to '${ gitpodHost } ' while trying to connect to a Gitpod workspace` ) ;
665
652
}
666
653
654
+ const gitpodVersion = await getGitpodVersion ( gitpodHost ) ;
655
+ const sessionScopes = [ 'function:getWorkspace' , 'function:getOwnerToken' , 'function:getLoggedInUser' , 'resource:default' ] ;
656
+ if ( isFeatureSupported ( gitpodVersion , 'SSHPublicKeys' ) /* && isFeatureSupported('', 'sendHeartBeat') */ ) {
657
+ sessionScopes . push ( 'function:getSSHPublicKeys' , 'function:sendHeartBeat' ) ;
658
+ } else {
659
+ this . logger . warn ( `function:getSSHPublicKeys and function:sendHeartBeat session scopes not supported in ${ gitpodHost } , using version ${ gitpodVersion } ` ) ;
660
+ }
661
+
667
662
return vscode . authentication . getSession (
668
663
'gitpod' ,
669
- [ 'function:getWorkspace' , 'function:getOwnerToken' , 'function:getLoggedInUser' , 'function:getSSHPublicKeys' , 'function:sendHeartBeat' , 'resource:default' ] ,
664
+ sessionScopes ,
670
665
{ createIfNone : true }
671
666
) ;
672
667
}
@@ -829,19 +824,31 @@ export default class RemoteConnector extends Disposable {
829
824
830
825
await this . context . globalState . update ( `${ RemoteConnector . SSH_DEST_KEY } ${ sshDestStr } ` , { ...connectionInfo , isFirstConnection : false } ) ;
831
826
832
- // gitpod remote extension installation is async so sometimes gitpod-desktop will activate before gitpod-remote
833
- // let's wait a few seconds for it to finish install
834
- setTimeout ( async ( ) => {
835
- // Check for gitpod remote extension version to avoid sending heartbeat in both extensions at the same time
836
- const isGitpodRemoteHeartbeatCancelled = await cancelGitpodRemoteHeartbeat ( ) ;
837
- if ( isGitpodRemoteHeartbeatCancelled ) {
838
- const session = await this . getGitpodSession ( connectionInfo . gitpodHost ) ;
839
- if ( session ) {
840
- this . startHeartBeat ( session . accessToken , connectionInfo ) ;
827
+ const gitpodVersion = await getGitpodVersion ( connectionInfo . gitpodHost ) ;
828
+ if ( isFeatureSupported ( gitpodVersion , 'localHeartbeat' ) ) {
829
+ // gitpod remote extension installation is async so sometimes gitpod-desktop will activate before gitpod-remote
830
+ // let's try a few times for it to finish install
831
+ let retryCount = 10 ;
832
+ const tryStartHeartbeat = async ( ) => {
833
+ // Check for gitpod remote extension version to avoid sending heartbeat in both extensions at the same time
834
+ const isGitpodRemoteHeartbeatCancelled = await cancelGitpodRemoteHeartbeat ( ) ;
835
+ if ( isGitpodRemoteHeartbeatCancelled ) {
836
+ const session = await this . getGitpodSession ( connectionInfo . gitpodHost ) ;
837
+ if ( session ) {
838
+ this . startHeartBeat ( session . accessToken , connectionInfo ) ;
839
+ }
840
+ this . telemetry . sendTelemetryEvent ( 'vscode_desktop_heartbeat_state' , { enabled : String ( ! ! this . heartbeatManager ) , gitpodHost : connectionInfo . gitpodHost , workspaceId : connectionInfo . workspaceId , instanceId : connectionInfo . instanceId } ) ;
841
+ } else if ( retryCount > 0 ) {
842
+ retryCount -- ;
843
+ setTimeout ( tryStartHeartbeat , 3000 ) ;
844
+ } else {
845
+ this . telemetry . sendTelemetryEvent ( 'vscode_desktop_heartbeat_state' , { enabled : String ( false ) , gitpodHost : connectionInfo . gitpodHost , workspaceId : connectionInfo . workspaceId , instanceId : connectionInfo . instanceId } ) ;
841
846
}
842
- }
843
- this . telemetry . sendTelemetryEvent ( 'vscode_desktop_heartbeat_state' , { enabled : String ( ! ! this . heartbeatManager ) , gitpodHost : connectionInfo . gitpodHost , workspaceId : connectionInfo . workspaceId , instanceId : connectionInfo . instanceId } ) ;
844
- } , 7000 ) ;
847
+ } ;
848
+ tryStartHeartbeat ( ) ;
849
+ } else {
850
+ this . logger . warn ( `Local heatbeat not supported in ${ connectionInfo . gitpodHost } , using version ${ gitpodVersion } ` ) ;
851
+ }
845
852
}
846
853
847
854
public override async dispose ( ) : Promise < void > {
0 commit comments