Skip to content

Commit 854045d

Browse files
authored
Add vscode version and remote ssh version to telemetry (#99)
* Add vscode version and remote ssh version to telemetry * Read files async
1 parent deb627f commit 854045d

File tree

3 files changed

+119
-70
lines changed

3 files changed

+119
-70
lines changed

src/local-ssh/proxy.ts

Lines changed: 102 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,39 @@
55

66
import * as os from 'os';
77
import * as path from 'path';
8+
import * as fs from 'fs';
9+
import { NopeLogger, DebugLogger } from './logger';
10+
import { TelemetryService } from './telemetryService';
811

912
interface ClientOptions {
1013
host: string;
1114
gitpodHost: string;
1215
extIpcPort: number;
1316
machineID: string;
1417
debug: boolean;
18+
appRoot: string;
19+
extensionsDir: string;
1520
}
1621

1722
function getClientOptions(): ClientOptions {
1823
const args = process.argv.slice(2);
1924
// %h is in the form of <ws_id>.vss.<gitpod_host>'
2025
// add `https://` prefix since our gitpodHost is actually a url not host
2126
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];
2232
const gitpodHost = 'https://' + args[0].split('.').splice(2).join('.');
2333
return {
2434
host,
2535
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
2941
};
3042
}
3143

@@ -34,46 +46,6 @@ if (!options) {
3446
process.exit(1);
3547
}
3648

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-
7749
import { SshClient } from '@microsoft/dev-tunnels-ssh-tcp';
7850
import { NodeStream, ObjectDisposedError, SshChannelError, SshChannelOpenFailureReason, SshClientCredentials, SshClientSession, SshConnectionError, SshDisconnectReason, SshReconnectError, SshReconnectFailureReason, SshServerSession, SshSessionConfiguration, Stream, TraceLevel, WebSocketStream } from '@microsoft/dev-tunnels-ssh';
7951
import { importKey, importKeyBytes } from '@microsoft/dev-tunnels-ssh-keys';
@@ -127,27 +99,41 @@ interface SSHUserFlowTelemetry extends UserFlowTelemetryProperties {
12799
class WebSocketSSHProxy {
128100
private extensionIpc: Client<ExtensionServiceDefinition>;
129101

102+
private flow: SSHUserFlowTelemetry;
103+
130104
constructor(
131105
private readonly options: ClientOptions,
132106
private readonly telemetryService: ITelemetryService,
133107
private readonly metricsReporter: LocalSSHMetricsReporter,
134-
private readonly logService: ILogService,
135-
private readonly flow: SSHUserFlowTelemetry
108+
private readonly logService: ILogService
136109
) {
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();
139120
this.extensionIpc = createClient(ExtensionServiceDefinition, createChannel('127.0.0.1:' + this.options.extIpcPort));
140121
}
141122

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+
143131
const exitHandler = (signal?: NodeJS.Signals) => {
144-
exitProcess(false, signal);
132+
this.exitProcess(false, signal);
145133
};
146134
process.on('SIGINT', exitHandler);
147135
process.on('SIGTERM', exitHandler);
148-
}
149136

150-
private onException() {
151137
process.on('uncaughtException', (err) => {
152138
this.logService.error(err, 'uncaught exception');
153139
});
@@ -156,6 +142,20 @@ class WebSocketSSHProxy {
156142
});
157143
}
158144

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+
159159
async start() {
160160
// Create as Duplex from stdin and stdout as passing them separately to NodeStream
161161
// will result in an unhandled exception as NodeStream does not properly add
@@ -171,15 +171,9 @@ class WebSocketSSHProxy {
171171
// So let's just force kill here
172172
pipeSession?.close(SshDisconnectReason.byApplication);
173173
setTimeout(() => {
174-
exitProcess(true);
174+
this.exitProcess(true);
175175
}, 50);
176176
});
177-
// sshStream.on('end', () => {
178-
// setTimeout(() => doProcessExit(0), 50);
179-
// });
180-
// sshStream.on('close', () => {
181-
// setTimeout(() => doProcessExit(0), 50);
182-
// });
183177

184178
// This is expected to never throw as key is hardcoded
185179
const keys = await importKeyBytes(getHostKey());
@@ -202,7 +196,7 @@ class WebSocketSSHProxy {
202196
localSession.close(SshDisconnectReason.connectionLost);
203197
// but if not force exit
204198
setTimeout(() => {
205-
exitProcess(true);
199+
this.exitProcess(true);
206200
}, 50);
207201
})
208202
.then(async session => {
@@ -394,13 +388,56 @@ class WebSocketSSHProxy {
394388
}
395389
}
396390

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+
}
398439

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();
404441

405442
function fixSSHErrorName(err: any) {
406443
if (err instanceof SshConnectionError) {

src/local-ssh/telemetryService.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@ export class TelemetryService implements ITelemetryService {
6464
delete properties['flow'];
6565
return this.sendTelemetryEvent('vscode_desktop_' + flowProperties.flow, properties);
6666
}
67+
68+
updateCommonProperties(productJson: any, extensionsJson: any) {
69+
let remotesshextversion: string | undefined;
70+
if (Array.isArray(extensionsJson)) {
71+
const remoteSshExt = extensionsJson.find(i => i.identifier.id === 'ms-vscode-remote.remote-ssh');
72+
remotesshextversion = remoteSshExt?.version;
73+
}
74+
75+
this.commonProperties['common.vscodeversion'] = productJson.version;
76+
this.commonProperties['common.remotesshextversion'] = remotesshextversion;
77+
}
6778
}
6879

6980
function getCommonProperties(machineId: string, extensionId: string, extensionVersion: string) {

src/services/remoteService.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,21 +169,22 @@ export class RemoteService extends Disposable implements IRemoteService {
169169
}
170170

171171
private async configureSettings({ proxyScript, launcher }: { proxyScript: string; launcher: string }) {
172-
const extIpcPort = Configuration.getLocalSshExtensionIpcPort();
173-
const logLevel = Configuration.getSSHProxyLogLevel();
174-
const hostConfig = this.getHostSSHConfig(this.hostService.gitpodHost, launcher, proxyScript, extIpcPort, logLevel);
172+
const hostConfig = this.getHostSSHConfig(this.hostService.gitpodHost, launcher, proxyScript);
175173
await SSHConfiguration.ensureIncludeGitpodSSHConfig();
176174
const gitpodConfig = await SSHConfiguration.loadGitpodSSHConfig();
177175
gitpodConfig.addHostConfiguration(hostConfig);
178176
await SSHConfiguration.saveGitpodSSHConfig(gitpodConfig);
179177
}
180178

181-
private getHostSSHConfig(host: string, launcher: string, proxyScript: string, extIpcPort: number, logLevel: string) {
179+
private getHostSSHConfig(host: string, launcher: string, proxyScript: string) {
180+
const extIpcPort = Configuration.getLocalSshExtensionIpcPort();
181+
const logLevel = Configuration.getSSHProxyLogLevel();
182+
const extensionsDir = path.dirname(this.context.extensionMode === vscode.ExtensionMode.Production ? this.context.extensionPath : vscode.extensions.getExtension('ms-vscode-remote.remote-ssh')!.extensionPath);
182183
const extraArgs = (process.versions['electron'] && process.versions['microsoft-build']) ? '--ms-enable-electron-run-as-node' : '';
183184
return {
184185
Host: '*.' + getLocalSSHDomain(host),
185186
StrictHostKeyChecking: 'no',
186-
ProxyCommand: `"${launcher}" "${process.execPath}" "${proxyScript}" ${extraArgs} %h ${extIpcPort} ${vscode.env.machineId} ${logLevel}`
187+
ProxyCommand: `"${launcher}" "${process.execPath}" "${proxyScript}" ${extraArgs} %h ${extIpcPort} ${vscode.env.machineId} ${logLevel} "${vscode.env.appRoot}" "${extensionsDir}"`
187188
};
188189
}
189190

0 commit comments

Comments
 (0)