5
5
6
6
import * as os from 'os' ;
7
7
import * as path from 'path' ;
8
+ import * as fs from 'fs' ;
9
+ import { NopeLogger , DebugLogger } from './logger' ;
10
+ import { TelemetryService } from './telemetryService' ;
8
11
9
12
interface ClientOptions {
10
13
host : string ;
11
14
gitpodHost : string ;
12
15
extIpcPort : number ;
13
16
machineID : string ;
14
17
debug : boolean ;
18
+ appRoot : string ;
19
+ extensionsDir : string ;
15
20
}
16
21
17
22
function getClientOptions ( ) : ClientOptions {
18
23
const args = process . argv . slice ( 2 ) ;
19
24
// %h is in the form of <ws_id>.vss.<gitpod_host>'
20
25
// add `https://` prefix since our gitpodHost is actually a url not host
21
26
const host = args [ 0 ] ;
27
+ const extIpcPort = Number . parseInt ( args [ 1 ] , 10 ) ;
28
+ const machineID = args [ 2 ] ?? '' ;
29
+ const debug = args [ 3 ] === 'debug' ;
30
+ const appRoot = args [ 4 ] ;
31
+ const extensionsDir = args [ 5 ] ;
22
32
const gitpodHost = 'https://' + args [ 0 ] . split ( '.' ) . splice ( 2 ) . join ( '.' ) ;
23
33
return {
24
34
host,
25
35
gitpodHost,
26
- extIpcPort : Number . parseInt ( args [ 1 ] , 10 ) ,
27
- machineID : args [ 2 ] ?? '' ,
28
- debug : args [ 3 ] === 'debug' ,
36
+ extIpcPort,
37
+ machineID,
38
+ debug,
39
+ appRoot,
40
+ extensionsDir
29
41
} ;
30
42
}
31
43
@@ -34,46 +46,6 @@ if (!options) {
34
46
process . exit ( 1 ) ;
35
47
}
36
48
37
- import { NopeLogger , DebugLogger } from './logger' ;
38
- const logService = options . debug ? new DebugLogger ( path . join ( os . tmpdir ( ) , `lssh-${ options . host } .log` ) ) : new NopeLogger ( ) ;
39
-
40
- import { TelemetryService } from './telemetryService' ;
41
- const telemetryService = new TelemetryService (
42
- process . env . SEGMENT_KEY ! ,
43
- options . machineID ,
44
- process . env . EXT_NAME ! ,
45
- process . env . EXT_VERSION ! ,
46
- options . gitpodHost ,
47
- logService
48
- ) ;
49
-
50
- const flow : SSHUserFlowTelemetry = {
51
- flow : 'local_ssh' ,
52
- gitpodHost : options . gitpodHost ,
53
- workspaceId : '' ,
54
- processId : process . pid ,
55
- } ;
56
-
57
- telemetryService . sendUserFlowStatus ( 'started' , flow ) ;
58
- const sendExited = ( exitCode : number , forceExit : boolean , exitSignal ?: NodeJS . Signals ) => {
59
- return telemetryService . sendUserFlowStatus ( 'exited' , {
60
- ...flow ,
61
- exitCode,
62
- forceExit : String ( forceExit ) ,
63
- signal : exitSignal
64
- } ) ;
65
- } ;
66
- // best effort to intercept process exit
67
- const beforeExitListener = ( exitCode : number ) => {
68
- process . removeListener ( 'beforeExit' , beforeExitListener ) ;
69
- return sendExited ( exitCode , false ) ;
70
- } ;
71
- process . addListener ( 'beforeExit' , beforeExitListener ) ;
72
- const exitProcess = async ( forceExit : boolean , signal ?: NodeJS . Signals ) => {
73
- await sendExited ( 0 , forceExit , signal ) ;
74
- process . exit ( 0 ) ;
75
- } ;
76
-
77
49
import { SshClient } from '@microsoft/dev-tunnels-ssh-tcp' ;
78
50
import { NodeStream , ObjectDisposedError , SshChannelError , SshChannelOpenFailureReason , SshClientCredentials , SshClientSession , SshConnectionError , SshDisconnectReason , SshReconnectError , SshReconnectFailureReason , SshServerSession , SshSessionConfiguration , Stream , TraceLevel , WebSocketStream } from '@microsoft/dev-tunnels-ssh' ;
79
51
import { importKey , importKeyBytes } from '@microsoft/dev-tunnels-ssh-keys' ;
@@ -127,27 +99,41 @@ interface SSHUserFlowTelemetry extends UserFlowTelemetryProperties {
127
99
class WebSocketSSHProxy {
128
100
private extensionIpc : Client < ExtensionServiceDefinition > ;
129
101
102
+ private flow : SSHUserFlowTelemetry ;
103
+
130
104
constructor (
131
105
private readonly options : ClientOptions ,
132
106
private readonly telemetryService : ITelemetryService ,
133
107
private readonly metricsReporter : LocalSSHMetricsReporter ,
134
- private readonly logService : ILogService ,
135
- private readonly flow : SSHUserFlowTelemetry
108
+ private readonly logService : ILogService
136
109
) {
137
- this . onExit ( ) ;
138
- this . onException ( ) ;
110
+ this . flow = {
111
+ flow : 'local_ssh' ,
112
+ gitpodHost : options . gitpodHost ,
113
+ workspaceId : '' ,
114
+ processId : process . pid ,
115
+ } ;
116
+
117
+ telemetryService . sendUserFlowStatus ( 'started' , this . flow ) ;
118
+
119
+ this . setupNativeHandlers ( ) ;
139
120
this . extensionIpc = createClient ( ExtensionServiceDefinition , createChannel ( '127.0.0.1:' + this . options . extIpcPort ) ) ;
140
121
}
141
122
142
- private onExit ( ) {
123
+ private setupNativeHandlers ( ) {
124
+ // best effort to intercept process exit
125
+ const beforeExitListener = ( exitCode : number ) => {
126
+ process . removeListener ( 'beforeExit' , beforeExitListener ) ;
127
+ return this . sendExited ( exitCode , false ) ;
128
+ } ;
129
+ process . addListener ( 'beforeExit' , beforeExitListener ) ;
130
+
143
131
const exitHandler = ( signal ?: NodeJS . Signals ) => {
144
- exitProcess ( false , signal ) ;
132
+ this . exitProcess ( false , signal ) ;
145
133
} ;
146
134
process . on ( 'SIGINT' , exitHandler ) ;
147
135
process . on ( 'SIGTERM' , exitHandler ) ;
148
- }
149
136
150
- private onException ( ) {
151
137
process . on ( 'uncaughtException' , ( err ) => {
152
138
this . logService . error ( err , 'uncaught exception' ) ;
153
139
} ) ;
@@ -156,6 +142,20 @@ class WebSocketSSHProxy {
156
142
} ) ;
157
143
}
158
144
145
+ private sendExited ( exitCode : number , forceExit : boolean , exitSignal ?: NodeJS . Signals ) {
146
+ return this . telemetryService . sendUserFlowStatus ( 'exited' , {
147
+ ...this . flow ,
148
+ exitCode,
149
+ forceExit : String ( forceExit ) ,
150
+ signal : exitSignal
151
+ } ) ;
152
+ }
153
+
154
+ private async exitProcess ( forceExit : boolean , signal ?: NodeJS . Signals ) {
155
+ await this . sendExited ( 0 , forceExit , signal ) ;
156
+ process . exit ( 0 ) ;
157
+ }
158
+
159
159
async start ( ) {
160
160
// Create as Duplex from stdin and stdout as passing them separately to NodeStream
161
161
// will result in an unhandled exception as NodeStream does not properly add
@@ -171,15 +171,9 @@ class WebSocketSSHProxy {
171
171
// So let's just force kill here
172
172
pipeSession ?. close ( SshDisconnectReason . byApplication ) ;
173
173
setTimeout ( ( ) => {
174
- exitProcess ( true ) ;
174
+ this . exitProcess ( true ) ;
175
175
} , 50 ) ;
176
176
} ) ;
177
- // sshStream.on('end', () => {
178
- // setTimeout(() => doProcessExit(0), 50);
179
- // });
180
- // sshStream.on('close', () => {
181
- // setTimeout(() => doProcessExit(0), 50);
182
- // });
183
177
184
178
// This is expected to never throw as key is hardcoded
185
179
const keys = await importKeyBytes ( getHostKey ( ) ) ;
@@ -202,7 +196,7 @@ class WebSocketSSHProxy {
202
196
localSession . close ( SshDisconnectReason . connectionLost ) ;
203
197
// but if not force exit
204
198
setTimeout ( ( ) => {
205
- exitProcess ( true ) ;
199
+ this . exitProcess ( true ) ;
206
200
} , 50 ) ;
207
201
} )
208
202
. then ( async session => {
@@ -394,13 +388,56 @@ class WebSocketSSHProxy {
394
388
}
395
389
}
396
390
397
- const metricsReporter = new LocalSSHMetricsReporter ( logService ) ;
391
+ let vscodeProductJson : any ;
392
+ async function getVSCodeProductJson ( appRoot : string ) {
393
+ if ( ! vscodeProductJson ) {
394
+ try {
395
+ const productJsonStr = await fs . promises . readFile ( path . join ( appRoot , 'product.json' ) , 'utf8' ) ;
396
+ vscodeProductJson = JSON . parse ( productJsonStr ) ;
397
+ } catch {
398
+ return { } ;
399
+ }
400
+ }
401
+
402
+ return vscodeProductJson ;
403
+ }
404
+
405
+ async function getExtensionsJson ( extensionsDir : string ) {
406
+ try {
407
+ const extensionJsonStr = await fs . promises . readFile ( path . join ( extensionsDir , 'extensions.json' ) , 'utf8' ) ;
408
+ return JSON . parse ( extensionJsonStr ) ;
409
+ } catch {
410
+ return [ ] ;
411
+ }
412
+ }
413
+
414
+ async function main ( ) {
415
+ const logService = options . debug ? new DebugLogger ( path . join ( os . tmpdir ( ) , `lssh-${ options . host } .log` ) ) : new NopeLogger ( ) ;
416
+ const telemetryService = new TelemetryService (
417
+ process . env . SEGMENT_KEY ! ,
418
+ options . machineID ,
419
+ process . env . EXT_NAME ! ,
420
+ process . env . EXT_VERSION ! ,
421
+ options . gitpodHost ,
422
+ logService
423
+ ) ;
424
+
425
+ const metricsReporter = new LocalSSHMetricsReporter ( logService ) ;
426
+ const proxy = new WebSocketSSHProxy ( options , telemetryService , metricsReporter , logService ) ;
427
+ const promise = proxy . start ( ) . catch ( e => {
428
+ const err = new WrapError ( 'Uncaught exception on start method' , e ) ;
429
+ telemetryService . sendTelemetryException ( err , { gitpodHost : options . gitpodHost } ) ;
430
+ } ) ;
431
+
432
+ Promise . all ( [ getVSCodeProductJson ( options . appRoot ) , getExtensionsJson ( options . extensionsDir ) ] )
433
+ . then ( ( [ productJson , extensionsJson ] ) => {
434
+ telemetryService . updateCommonProperties ( productJson , extensionsJson ) ;
435
+ } ) ;
436
+
437
+ await promise ;
438
+ }
398
439
399
- const proxy = new WebSocketSSHProxy ( options , telemetryService , metricsReporter , logService , flow ) ;
400
- proxy . start ( ) . catch ( e => {
401
- const err = new WrapError ( 'Uncaught exception on start method' , e ) ;
402
- telemetryService . sendTelemetryException ( err , { gitpodHost : options . gitpodHost } ) ;
403
- } ) ;
440
+ main ( ) ;
404
441
405
442
function fixSSHErrorName ( err : any ) {
406
443
if ( err instanceof SshConnectionError ) {
0 commit comments