Skip to content

Commit 9b25868

Browse files
committed
Add setting for ssh proxy logs
1 parent 3cdda93 commit 9b25868

File tree

8 files changed

+112
-53
lines changed

8 files changed

+112
-53
lines changed

package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@
6767
"type": "number",
6868
"description": "The port to use for the local SSH ipc server.",
6969
"scope": "application"
70+
},
71+
"gitpod.lssh.logLevel": {
72+
"type": "string",
73+
"description": "The log level for ssh proxy.",
74+
"scope": "application",
75+
"enum": ["none", "debug"],
76+
"default": "none"
7077
}
7178
}
7279
}

src/commands/logs.ts

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import { Command } from '../commandManager';
1212
import { ILogService } from '../services/logService';
1313
import { INotificationService } from '../services/notificationService';
1414
import { ITelemetryService, UserFlowTelemetryProperties } from '../common/telemetry';
15-
import { HostService } from '../services/hostService';
15+
import { IHostService } from '../services/hostService';
16+
import { getGitpodRemoteWindowConnectionInfo } from '../remote';
17+
import SSHDestination from '../ssh/sshDestination';
1618

1719
interface IFile {
1820
path: string;
@@ -23,11 +25,12 @@ export class ExportLogsCommand implements Command {
2325
readonly id = 'gitpod.exportLogs';
2426

2527
constructor(
28+
private readonly context: vscode.ExtensionContext,
2629
private readonly extLocalLogsUri: vscode.Uri,
2730
private readonly notificationService: INotificationService,
2831
private readonly telemetryService: ITelemetryService,
2932
private readonly logService: ILogService,
30-
private readonly hostService: HostService,
33+
private readonly hostService: IHostService,
3134
) { }
3235

3336
async execute() {
@@ -44,10 +47,28 @@ export class ExportLogsCommand implements Command {
4447
}
4548
}
4649

50+
private getAdditionalRemoteLogs() {
51+
return [
52+
'/tmp/gitpod-git-credential-helper.log',
53+
'/var/log/gitpod/supervisor.log',
54+
'/workspace/.gitpod/logs/docker-up.log'
55+
];
56+
}
57+
58+
private getAdditionalLocalLogs() {
59+
const additionalLocalLogs = [];
60+
const sshDestStr = getGitpodRemoteWindowConnectionInfo(this.context)?.sshDestStr;
61+
if (sshDestStr) {
62+
const sshDest = SSHDestination.fromRemoteSSHString(sshDestStr);
63+
additionalLocalLogs.push(path.join(os.tmpdir(), `lssh-${sshDest.hostname}.log`));
64+
}
65+
return additionalLocalLogs;
66+
}
67+
4768
async exportLogs() {
4869
const saveUri = await vscode.window.showSaveDialog({
4970
title: 'Choose save location ...',
50-
defaultUri: vscode.Uri.file(path.posix.join(os.homedir(), `vscode-desktop-logs-${new Date().toISOString().replace(/-|:|\.\d+Z$/g, '')}.zip`)),
71+
defaultUri: vscode.Uri.file(path.join(os.homedir(), `vscode-desktop-logs-${new Date().toISOString().replace(/-|:|\.\d+Z$/g, '')}.zip`)),
5172
});
5273
if (!saveUri) {
5374
return;
@@ -61,8 +82,8 @@ export class ExportLogsCommand implements Command {
6182
// Ignore if not found
6283
}
6384

64-
const remoteLogsUri = extRemoteLogsUri?.with({ path: path.posix.dirname(path.posix.dirname(extRemoteLogsUri.path)) });
65-
const localLogsUri = this.extLocalLogsUri.with({ path: path.posix.dirname(path.posix.dirname(this.extLocalLogsUri.path)) });
85+
const remoteLogsUri = extRemoteLogsUri?.with({ path: path.dirname(path.dirname(extRemoteLogsUri.path)) });
86+
const localLogsUri = this.extLocalLogsUri.with({ path: path.dirname(path.dirname(this.extLocalLogsUri.path)) });
6687

6788
return vscode.window.withProgress({
6889
location: vscode.ProgressLocation.Notification,
@@ -72,23 +93,19 @@ export class ExportLogsCommand implements Command {
7293
const remoteLogFiles: IFile[] = [];
7394
if (remoteLogsUri) {
7495
await traverseFolder(remoteLogsUri, remoteLogFiles, token);
75-
remoteLogFiles.forEach(file => file.path = path.posix.join('./remote', path.posix.relative(remoteLogsUri.path, file.path)));
96+
remoteLogFiles.forEach(file => file.path = path.join('./remote', path.relative(remoteLogsUri.path, file.path)));
7697
if (token.isCancellationRequested) {
7798
return;
7899
}
79100
}
80101

81-
for (const logFilePath of [
82-
'/tmp/gitpod-git-credential-helper.log',
83-
'/var/log/gitpod/supervisor.log',
84-
'/workspace/.gitpod/logs/docker-up.log'
85-
]) {
102+
for (const logFilePath of this.getAdditionalRemoteLogs()) {
86103
try {
87-
const logFileUri = vscode.Uri.file(logFilePath).with({ scheme: "vscode-remote" });
88-
const fileContent = await vscode.workspace.fs.readFile(logFileUri)
104+
const logFileUri = vscode.Uri.file(logFilePath).with({ scheme: 'vscode-remote' });
105+
const fileContent = await vscode.workspace.fs.readFile(logFileUri);
89106
if (fileContent.byteLength > 0) {
90107
remoteLogFiles.push({
91-
path: path.posix.join('./remote', path.posix.basename(logFileUri.path)),
108+
path: path.join('./remote', path.basename(logFileUri.path)),
92109
contents: Buffer.from(fileContent)
93110
});
94111
}
@@ -99,11 +116,26 @@ export class ExportLogsCommand implements Command {
99116

100117
const localLogFiles: IFile[] = [];
101118
await traverseFolder(localLogsUri, localLogFiles, token);
102-
localLogFiles.forEach(file => file.path = path.posix.join('./local', path.posix.relative(localLogsUri.path, file.path)));
119+
localLogFiles.forEach(file => file.path = path.join('./local', path.relative(localLogsUri.path, file.path)));
103120
if (token.isCancellationRequested) {
104121
return;
105122
}
106123

124+
for (const logFilePath of this.getAdditionalLocalLogs()) {
125+
try {
126+
const logFileUri = vscode.Uri.file(logFilePath);
127+
const fileContent = await vscode.workspace.fs.readFile(logFileUri);
128+
if (fileContent.byteLength > 0) {
129+
remoteLogFiles.push({
130+
path: path.join('./local', path.basename(logFileUri.path)),
131+
contents: Buffer.from(fileContent)
132+
});
133+
}
134+
} catch {
135+
// no-op
136+
}
137+
}
138+
107139
return zip(saveUri.fsPath, remoteLogFiles.concat(localLogFiles));
108140
});
109141
}

src/configuration.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,13 @@ function getLocalSshExtensionIpcPort() {
2525
return vscode.workspace.getConfiguration('gitpod').get<number>('lsshExtensionIpcPort') || defaultPort;
2626
}
2727

28+
function getSSHProxyLogLevel() {
29+
return vscode.workspace.getConfiguration('gitpod').get<string>('lssh.logLevel') || 'none';
30+
}
31+
2832
export const Configuration = {
2933
getGitpodHost,
3034
getUseLocalApp,
3135
getLocalSshExtensionIpcPort,
36+
getSSHProxyLogLevel
3237
};

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export async function activate(context: vscode.ExtensionContext) {
117117
// Register global commands
118118
commandManager.register(new SignInCommand(sessionService));
119119
commandManager.register(new InstallLocalExtensionsOnRemoteCommand(remoteService));
120-
commandManager.register(new ExportLogsCommand(context.logUri, notificationService, telemetryService, logger, hostService));
120+
commandManager.register(new ExportLogsCommand(context, context.logUri, notificationService, telemetryService, logger, hostService));
121121

122122
if (!context.globalState.get<boolean>(FIRST_INSTALL_KEY, false)) {
123123
context.globalState.update(FIRST_INSTALL_KEY, true);

src/local-ssh/logger.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { WriteStream, createWriteStream } from 'fs';
6+
import * as fs from 'fs';
77
import { inspect } from 'util';
88
import { ILogService } from '../services/logService';
99

@@ -17,13 +17,13 @@ export class NopeLogger implements ILogService {
1717
}
1818

1919
export class DebugLogger implements ILogService {
20-
private readonly stream?: WriteStream;
20+
private readonly stream?: fs.WriteStream;
2121

22-
constructor() {
22+
constructor(logFilePath: string) {
2323
try {
2424
// no need to consider target file for different platform
2525
// since we use in only for debug local ssh proxy
26-
this.stream = createWriteStream('/tmp/lssh.log');
26+
this.stream = fs.createWriteStream(logFilePath, { flags: 'a' });
2727
} catch (_e) { }
2828
}
2929

@@ -32,28 +32,28 @@ export class DebugLogger implements ILogService {
3232
}
3333

3434
trace(message: string, ...args: any[]): void {
35-
this.stream?.write(`${new Date()}TRACE: ${message} ${this.parseArgs(...args)}\n`);
35+
this.stream?.write(`${new Date().toISOString()} pid[${process.pid}] TRACE: ${message} ${this.parseArgs(...args)}\n`);
3636
}
3737

3838
debug(message: string, ...args: any[]): void {
39-
this.stream?.write(`${new Date()}DEBUG: ${message} ${this.parseArgs(...args)}\n`);
39+
this.stream?.write(`${new Date().toISOString()} pid[${process.pid}] DEBUG: ${message} ${this.parseArgs(...args)}\n`);
4040
}
4141

4242
info(message: string, ...args: any[]): void {
43-
this.stream?.write(`${new Date()}INFO: ${message} ${this.parseArgs(...args)}\n`);
43+
this.stream?.write(`${new Date().toISOString()} pid[${process.pid}] INFO: ${message} ${this.parseArgs(...args)}\n`);
4444
}
4545

4646
warn(message: string, ...args: any[]): void {
47-
this.stream?.write(`${new Date()}WARN: ${message} ${this.parseArgs(...args)}\n`);
47+
this.stream?.write(`${new Date().toISOString()} pid[${process.pid}] WARN: ${message} ${this.parseArgs(...args)}\n`);
4848
}
4949

5050
error(error: string | Error, ...args: any[]): void {
5151
if (error instanceof Error) {
52-
this.stream?.write(`${new Date()}ERROR: ${error.toString()}\n${error.stack}\n${this.parseArgs(...args)}\n`);
52+
this.stream?.write(`${new Date().toISOString()} pid[${process.pid}] ERROR: ${error.toString()}\n${error.stack}\n${this.parseArgs(...args)}\n`);
5353
} else {
54-
this.stream?.write(`${new Date()}ERROR: ${error.toString()} ${this.parseArgs(...args)}\n`);
54+
this.stream?.write(`${new Date().toISOString()} pid[${process.pid}] ERROR: ${error.toString()} ${this.parseArgs(...args)}\n`);
5555
}
5656
}
5757

5858
show(): void { }
59-
}
59+
}

src/local-ssh/proxy.ts

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,29 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import * as os from 'os';
7+
import * as path from 'path';
8+
69
interface ClientOptions {
710
host: string;
11+
gitpodHost: string;
812
extIpcPort: number;
913
machineID: string;
14+
debug: boolean;
1015
}
1116

1217
function getClientOptions(): ClientOptions {
1318
const args = process.argv.slice(2);
1419
// %h is in the form of <ws_id>.vss.<gitpod_host>'
1520
// add `https://` prefix since our gitpodHost is actually a url not host
16-
const host = 'https://' + args[0].split('.').splice(2).join('.');
21+
const host = args[0];
22+
const gitpodHost = 'https://' + args[0].split('.').splice(2).join('.');
1723
return {
1824
host,
25+
gitpodHost,
1926
extIpcPort: Number.parseInt(args[1], 10),
2027
machineID: args[2] ?? '',
28+
debug: args[3] === 'debug',
2129
};
2230
}
2331

@@ -26,26 +34,22 @@ if (!options) {
2634
process.exit(1);
2735
}
2836

29-
import { NopeLogger } from './logger';
30-
const logService = new NopeLogger();
31-
32-
// DO NOT PUSH CHANGES BELOW TO PRODUCTION
33-
// import { DebugLogger } from './logger';
34-
// const logService = new DebugLogger();
37+
import { NopeLogger, DebugLogger } from './logger';
38+
const logService = options.debug ? new DebugLogger(path.join(os.tmpdir(), `lssh-${options.host}.log`)) : new NopeLogger();
3539

3640
import { TelemetryService } from './telemetryService';
3741
const telemetryService = new TelemetryService(
3842
process.env.SEGMENT_KEY!,
3943
options.machineID,
4044
process.env.EXT_NAME!,
4145
process.env.EXT_VERSION!,
42-
options.host,
46+
options.gitpodHost,
4347
logService
4448
);
4549

4650
const flow: SSHUserFlowTelemetry = {
4751
flow: 'local_ssh',
48-
gitpodHost: options.host,
52+
gitpodHost: options.gitpodHost,
4953
workspaceId: '',
5054
processId: process.pid,
5155
};
@@ -71,7 +75,7 @@ const exitProcess = async (forceExit: boolean, signal?: NodeJS.Signals) => {
7175
};
7276

7377
import { SshClient } from '@microsoft/dev-tunnels-ssh-tcp';
74-
import { NodeStream, ObjectDisposedError, SshChannelError, SshChannelOpenFailureReason, SshClientCredentials, SshClientSession, SshConnectionError, SshDisconnectReason, SshReconnectError, SshReconnectFailureReason, SshServerSession, SshSessionConfiguration, Stream, WebSocketStream } from '@microsoft/dev-tunnels-ssh';
78+
import { NodeStream, ObjectDisposedError, SshChannelError, SshChannelOpenFailureReason, SshClientCredentials, SshClientSession, SshConnectionError, SshDisconnectReason, SshReconnectError, SshReconnectFailureReason, SshServerSession, SshSessionConfiguration, Stream, TraceLevel, WebSocketStream } from '@microsoft/dev-tunnels-ssh';
7579
import { importKey, importKeyBytes } from '@microsoft/dev-tunnels-ssh-keys';
7680
import { ExtensionServiceDefinition, GetWorkspaceAuthInfoResponse } from '../proto/typescript/ipc/v1/ipc';
7781
import { Client, ClientError, Status, createChannel, createClient } from 'nice-grpc';
@@ -165,6 +169,7 @@ class WebSocketSSHProxy {
165169
// Seems there's a bug in the ssh library that could hang forever when the stream gets closed
166170
// so the below `await pipePromise` will never return and the node process will never exit.
167171
// So let's just force kill here
172+
pipeSession?.close(SshDisconnectReason.byApplication);
168173
setTimeout(() => {
169174
exitProcess(true);
170175
}, 50);
@@ -180,17 +185,22 @@ class WebSocketSSHProxy {
180185
const keys = await importKeyBytes(getHostKey());
181186
const config = new SshSessionConfiguration();
182187
config.maxClientAuthenticationAttempts = 1;
183-
const session = new SshServerSession(config);
184-
session.credentials.publicKeys.push(keys);
188+
const localSession = new SshServerSession(config);
189+
localSession.credentials.publicKeys.push(keys);
190+
localSession.trace = (_: TraceLevel, eventId: number, msg: string, err?: Error) => {
191+
this.logService.trace(`sshsession [local] eventId[${eventId}]`, msg, err);
192+
};
185193

194+
let pipeSession: SshClientSession | undefined;
186195
let pipePromise: Promise<void> | undefined;
187-
session.onAuthenticating(async (e) => {
196+
localSession.onAuthenticating(async (e) => {
188197
this.flow.workspaceId = e.username ?? '';
189198
this.sendUserStatusFlow('connecting');
190199
e.authenticationPromise = this.authenticateClient(e.username ?? '')
191-
.then(async pipeSession => {
200+
.then(async session => {
192201
this.sendUserStatusFlow('connected');
193-
pipePromise = session.pipe(pipeSession);
202+
pipeSession = session;
203+
pipePromise = localSession.pipe(pipeSession);
194204
return {};
195205
}).catch(async err => {
196206
this.logService.error('failed to authenticate proxy with username: ' + e.username ?? '', err);
@@ -209,21 +219,21 @@ class WebSocketSSHProxy {
209219
// Await a few seconds to delay showing ssh extension error modal dialog
210220
await timeout(5000);
211221

212-
await session.close(SshDisconnectReason.byApplication, err.toString(), err instanceof Error ? err : undefined);
222+
await localSession.close(SshDisconnectReason.byApplication, err.toString(), err instanceof Error ? err : undefined);
213223
return null;
214224
});
215225
});
216226
try {
217-
await session.connect(new NodeStream(sshStream));
227+
await localSession.connect(new NodeStream(sshStream));
218228
await pipePromise;
219229
} catch (e) {
220-
if (session.isClosed) {
230+
if (localSession.isClosed) {
221231
return;
222232
}
223233
e = fixSSHErrorName(e);
224234
this.logService.error(e, 'failed to connect to client');
225235
this.sendErrorReport(this.flow, e, 'failed to connect to client');
226-
await session.close(SshDisconnectReason.byApplication, e.toString(), e instanceof Error ? e : undefined);
236+
await localSession.close(SshDisconnectReason.byApplication, e.toString(), e instanceof Error ? e : undefined);
227237
}
228238
}
229239

@@ -325,6 +335,9 @@ class WebSocketSSHProxy {
325335
const config = new SshSessionConfiguration();
326336
const session = new SshClientSession(config);
327337
session.onAuthenticating((e) => e.authenticationPromise = Promise.resolve({}));
338+
session.trace = (_: TraceLevel, eventId: number, msg: string, err?: Error) => {
339+
this.logService.trace(`sshsession [websocket] eventId[${eventId}]`, msg, err);
340+
};
328341

329342
await session.connect(stream);
330343

@@ -340,7 +353,7 @@ class WebSocketSSHProxy {
340353

341354
async retryGetWorkspaceInfo(username: string) {
342355
return retry(async () => {
343-
return this.extensionIpc.getWorkspaceAuthInfo({ workspaceId: username, gitpodHost: this.options.host }).catch(e => {
356+
return this.extensionIpc.getWorkspaceAuthInfo({ workspaceId: username, gitpodHost: this.options.gitpodHost }).catch(e => {
344357
let failureCode = 'FailedToGetAuthInfo';
345358
if (e instanceof ClientError) {
346359
if (e.code === Status.FAILED_PRECONDITION && e.message.includes('gitpod host mismatch')) {
@@ -377,7 +390,7 @@ const metricsReporter = new LocalSSHMetricsReporter(logService);
377390
const proxy = new WebSocketSSHProxy(options, telemetryService, metricsReporter, logService, flow);
378391
proxy.start().catch(e => {
379392
const err = new WrapError('Uncaught exception on start method', e);
380-
telemetryService.sendTelemetryException(err, { gitpodHost: options.host });
393+
telemetryService.sendTelemetryException(err, { gitpodHost: options.gitpodHost });
381394
});
382395

383396
function fixSSHErrorName(err: any) {

0 commit comments

Comments
 (0)