Skip to content

Commit

Permalink
Add vscode version and remote ssh version to telemetry (#99)
Browse files Browse the repository at this point in the history
* Add vscode version and remote ssh version to telemetry

* Read files async
  • Loading branch information
jeanp413 authored Oct 5, 2023
1 parent deb627f commit 854045d
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 70 deletions.
167 changes: 102 additions & 65 deletions src/local-ssh/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,39 @@

import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import { NopeLogger, DebugLogger } from './logger';
import { TelemetryService } from './telemetryService';

interface ClientOptions {
host: string;
gitpodHost: string;
extIpcPort: number;
machineID: string;
debug: boolean;
appRoot: string;
extensionsDir: string;
}

function getClientOptions(): ClientOptions {
const args = process.argv.slice(2);
// %h is in the form of <ws_id>.vss.<gitpod_host>'
// add `https://` prefix since our gitpodHost is actually a url not host
const host = args[0];
const extIpcPort = Number.parseInt(args[1], 10);
const machineID = args[2] ?? '';
const debug = args[3] === 'debug';
const appRoot = args[4];
const extensionsDir = args[5];
const gitpodHost = 'https://' + args[0].split('.').splice(2).join('.');
return {
host,
gitpodHost,
extIpcPort: Number.parseInt(args[1], 10),
machineID: args[2] ?? '',
debug: args[3] === 'debug',
extIpcPort,
machineID,
debug,
appRoot,
extensionsDir
};
}

Expand All @@ -34,46 +46,6 @@ if (!options) {
process.exit(1);
}

import { NopeLogger, DebugLogger } from './logger';
const logService = options.debug ? new DebugLogger(path.join(os.tmpdir(), `lssh-${options.host}.log`)) : new NopeLogger();

import { TelemetryService } from './telemetryService';
const telemetryService = new TelemetryService(
process.env.SEGMENT_KEY!,
options.machineID,
process.env.EXT_NAME!,
process.env.EXT_VERSION!,
options.gitpodHost,
logService
);

const flow: SSHUserFlowTelemetry = {
flow: 'local_ssh',
gitpodHost: options.gitpodHost,
workspaceId: '',
processId: process.pid,
};

telemetryService.sendUserFlowStatus('started', flow);
const sendExited = (exitCode: number, forceExit: boolean, exitSignal?: NodeJS.Signals) => {
return telemetryService.sendUserFlowStatus('exited', {
...flow,
exitCode,
forceExit: String(forceExit),
signal: exitSignal
});
};
// best effort to intercept process exit
const beforeExitListener = (exitCode: number) => {
process.removeListener('beforeExit', beforeExitListener);
return sendExited(exitCode, false);
};
process.addListener('beforeExit', beforeExitListener);
const exitProcess = async (forceExit: boolean, signal?: NodeJS.Signals) => {
await sendExited(0, forceExit, signal);
process.exit(0);
};

import { SshClient } from '@microsoft/dev-tunnels-ssh-tcp';
import { NodeStream, ObjectDisposedError, SshChannelError, SshChannelOpenFailureReason, SshClientCredentials, SshClientSession, SshConnectionError, SshDisconnectReason, SshReconnectError, SshReconnectFailureReason, SshServerSession, SshSessionConfiguration, Stream, TraceLevel, WebSocketStream } from '@microsoft/dev-tunnels-ssh';
import { importKey, importKeyBytes } from '@microsoft/dev-tunnels-ssh-keys';
Expand Down Expand Up @@ -127,27 +99,41 @@ interface SSHUserFlowTelemetry extends UserFlowTelemetryProperties {
class WebSocketSSHProxy {
private extensionIpc: Client<ExtensionServiceDefinition>;

private flow: SSHUserFlowTelemetry;

constructor(
private readonly options: ClientOptions,
private readonly telemetryService: ITelemetryService,
private readonly metricsReporter: LocalSSHMetricsReporter,
private readonly logService: ILogService,
private readonly flow: SSHUserFlowTelemetry
private readonly logService: ILogService
) {
this.onExit();
this.onException();
this.flow = {
flow: 'local_ssh',
gitpodHost: options.gitpodHost,
workspaceId: '',
processId: process.pid,
};

telemetryService.sendUserFlowStatus('started', this.flow);

this.setupNativeHandlers();
this.extensionIpc = createClient(ExtensionServiceDefinition, createChannel('127.0.0.1:' + this.options.extIpcPort));
}

private onExit() {
private setupNativeHandlers() {
// best effort to intercept process exit
const beforeExitListener = (exitCode: number) => {
process.removeListener('beforeExit', beforeExitListener);
return this.sendExited(exitCode, false);
};
process.addListener('beforeExit', beforeExitListener);

const exitHandler = (signal?: NodeJS.Signals) => {
exitProcess(false, signal);
this.exitProcess(false, signal);
};
process.on('SIGINT', exitHandler);
process.on('SIGTERM', exitHandler);
}

private onException() {
process.on('uncaughtException', (err) => {
this.logService.error(err, 'uncaught exception');
});
Expand All @@ -156,6 +142,20 @@ class WebSocketSSHProxy {
});
}

private sendExited(exitCode: number, forceExit: boolean, exitSignal?: NodeJS.Signals) {
return this.telemetryService.sendUserFlowStatus('exited', {
...this.flow,
exitCode,
forceExit: String(forceExit),
signal: exitSignal
});
}

private async exitProcess(forceExit: boolean, signal?: NodeJS.Signals) {
await this.sendExited(0, forceExit, signal);
process.exit(0);
}

async start() {
// Create as Duplex from stdin and stdout as passing them separately to NodeStream
// will result in an unhandled exception as NodeStream does not properly add
Expand All @@ -171,15 +171,9 @@ class WebSocketSSHProxy {
// So let's just force kill here
pipeSession?.close(SshDisconnectReason.byApplication);
setTimeout(() => {
exitProcess(true);
this.exitProcess(true);
}, 50);
});
// sshStream.on('end', () => {
// setTimeout(() => doProcessExit(0), 50);
// });
// sshStream.on('close', () => {
// setTimeout(() => doProcessExit(0), 50);
// });

// This is expected to never throw as key is hardcoded
const keys = await importKeyBytes(getHostKey());
Expand All @@ -202,7 +196,7 @@ class WebSocketSSHProxy {
localSession.close(SshDisconnectReason.connectionLost);
// but if not force exit
setTimeout(() => {
exitProcess(true);
this.exitProcess(true);
}, 50);
})
.then(async session => {
Expand Down Expand Up @@ -394,13 +388,56 @@ class WebSocketSSHProxy {
}
}

const metricsReporter = new LocalSSHMetricsReporter(logService);
let vscodeProductJson: any;
async function getVSCodeProductJson(appRoot: string) {
if (!vscodeProductJson) {
try {
const productJsonStr = await fs.promises.readFile(path.join(appRoot, 'product.json'), 'utf8');
vscodeProductJson = JSON.parse(productJsonStr);
} catch {
return {};
}
}

return vscodeProductJson;
}

async function getExtensionsJson(extensionsDir: string) {
try {
const extensionJsonStr = await fs.promises.readFile(path.join(extensionsDir, 'extensions.json'), 'utf8');
return JSON.parse(extensionJsonStr);
} catch {
return [];
}
}

async function main() {
const logService = options.debug ? new DebugLogger(path.join(os.tmpdir(), `lssh-${options.host}.log`)) : new NopeLogger();
const telemetryService = new TelemetryService(
process.env.SEGMENT_KEY!,
options.machineID,
process.env.EXT_NAME!,
process.env.EXT_VERSION!,
options.gitpodHost,
logService
);

const metricsReporter = new LocalSSHMetricsReporter(logService);
const proxy = new WebSocketSSHProxy(options, telemetryService, metricsReporter, logService);
const promise = proxy.start().catch(e => {
const err = new WrapError('Uncaught exception on start method', e);
telemetryService.sendTelemetryException(err, { gitpodHost: options.gitpodHost });
});

Promise.all([getVSCodeProductJson(options.appRoot), getExtensionsJson(options.extensionsDir)])
.then(([productJson, extensionsJson]) => {
telemetryService.updateCommonProperties(productJson, extensionsJson);
});

await promise;
}

const proxy = new WebSocketSSHProxy(options, telemetryService, metricsReporter, logService, flow);
proxy.start().catch(e => {
const err = new WrapError('Uncaught exception on start method', e);
telemetryService.sendTelemetryException(err, { gitpodHost: options.gitpodHost });
});
main();

function fixSSHErrorName(err: any) {
if (err instanceof SshConnectionError) {
Expand Down
11 changes: 11 additions & 0 deletions src/local-ssh/telemetryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ export class TelemetryService implements ITelemetryService {
delete properties['flow'];
return this.sendTelemetryEvent('vscode_desktop_' + flowProperties.flow, properties);
}

updateCommonProperties(productJson: any, extensionsJson: any) {
let remotesshextversion: string | undefined;
if (Array.isArray(extensionsJson)) {
const remoteSshExt = extensionsJson.find(i => i.identifier.id === 'ms-vscode-remote.remote-ssh');
remotesshextversion = remoteSshExt?.version;
}

this.commonProperties['common.vscodeversion'] = productJson.version;
this.commonProperties['common.remotesshextversion'] = remotesshextversion;
}
}

function getCommonProperties(machineId: string, extensionId: string, extensionVersion: string) {
Expand Down
11 changes: 6 additions & 5 deletions src/services/remoteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,21 +169,22 @@ export class RemoteService extends Disposable implements IRemoteService {
}

private async configureSettings({ proxyScript, launcher }: { proxyScript: string; launcher: string }) {
const extIpcPort = Configuration.getLocalSshExtensionIpcPort();
const logLevel = Configuration.getSSHProxyLogLevel();
const hostConfig = this.getHostSSHConfig(this.hostService.gitpodHost, launcher, proxyScript, extIpcPort, logLevel);
const hostConfig = this.getHostSSHConfig(this.hostService.gitpodHost, launcher, proxyScript);
await SSHConfiguration.ensureIncludeGitpodSSHConfig();
const gitpodConfig = await SSHConfiguration.loadGitpodSSHConfig();
gitpodConfig.addHostConfiguration(hostConfig);
await SSHConfiguration.saveGitpodSSHConfig(gitpodConfig);
}

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

Expand Down

0 comments on commit 854045d

Please sign in to comment.