Skip to content

Commit 094e601

Browse files
jeanp413mustard-mh
andauthored
Add local ssh test connection (#91)
Co-authored-by: hwen <[email protected]>
1 parent 26215d3 commit 094e601

File tree

9 files changed

+225
-125
lines changed

9 files changed

+225
-125
lines changed

src/commands/workspaces.ts

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
77
import { Command } from '../commandManager';
88
import { ISessionService } from '../services/sessionService';
99
import { WorkspaceData, rawWorkspaceToWorkspaceData } from '../publicApi';
10-
import { NoExtensionIPCServerError, NoLocalSSHSupportError, SSHConnectionParams, SSH_DEST_KEY, getLocalSSHDomain } from '../remote';
10+
import { SSHConnectionParams, SSH_DEST_KEY, getLocalSSHDomain } from '../remote';
1111
import SSHDestination from '../ssh/sshDestination';
1212
import { IHostService } from '../services/hostService';
1313
import { WorkspaceState } from '../workspaceState';
@@ -16,7 +16,7 @@ import { eventToPromise, raceCancellationError } from '../common/event';
1616
import { ITelemetryService } from '../common/telemetry';
1717
import { IRemoteService } from '../services/remoteService';
1818
import { WrapError } from '../common/utils';
19-
import { getOpenSSHVersion } from '../ssh/sshVersion';
19+
import { getOpenSSHVersion, testSSHConnection as testLocalSSHConnection } from '../ssh/nativeSSH';
2020
import { IExperimentsService } from '../experiments';
2121

2222
function getCommandName(command: string) {
@@ -124,12 +124,27 @@ export class ConnectInNewWindowCommand implements Command {
124124
wsData = wsState.workspaceData; // Update wsData with latest info after workspace is running
125125
}
126126

127+
const domain = getLocalSSHDomain(this.hostService.gitpodHost);
128+
const sshHostname = `${wsData!.id}.${domain}`;
129+
const localSSHDestination = new SSHDestination(sshHostname, wsData!.id);
130+
let localSSHTestSuccess: boolean = false;
131+
try {
132+
await testLocalSSHConnection(localSSHDestination.user!, localSSHDestination.hostname);
133+
localSSHTestSuccess = true;
134+
} catch (e) {
135+
this.telemetryService.sendTelemetryException(
136+
new WrapError('Local SSH: failed to connect to workspace', e, 'Unknown'),
137+
{
138+
gitpodHost: this.hostService.gitpodHost,
139+
workspaceId: wsData!.id,
140+
}
141+
);
142+
}
143+
127144
let sshDest: SSHDestination;
128145
let password: string | undefined;
129-
if (await this.experimentsService.getUseLocalSSHProxy()) {
130-
const domain = getLocalSSHDomain(this.hostService.gitpodHost);
131-
const sshHostname = `${wsData!.id}.${domain}`;
132-
sshDest = new SSHDestination(sshHostname, wsData!.id);
146+
if (await this.experimentsService.getUseLocalSSHProxy() && localSSHTestSuccess) {
147+
sshDest = localSSHDestination;
133148
} else {
134149
({ destination: sshDest, password } = await this.remoteService.getWorkspaceSSHDestination(wsData!));
135150
}
@@ -163,16 +178,10 @@ export class ConnectInNewWindowCommand implements Command {
163178

164179
private async initializeLocalSSH(workspaceId: string) {
165180
try {
166-
const [isSupportLocalSSH, isExtensionServerReady] = await Promise.all([
181+
await Promise.all([
167182
this.remoteService.setupSSHProxy(),
168-
this.remoteService.extensionServerReady()
183+
this.remoteService.startLocalSSHServiceServer()
169184
]);
170-
if (!isExtensionServerReady) {
171-
throw new NoExtensionIPCServerError();
172-
}
173-
if (!isSupportLocalSSH) {
174-
throw new NoLocalSSHSupportError();
175-
}
176185
} catch (e) {
177186
const openSSHVersion = await getOpenSSHVersion();
178187
this.telemetryService.sendTelemetryException(new WrapError('Local SSH: failed to initialize local SSH', e), {
@@ -279,12 +288,27 @@ export class ConnectInCurrentWindowCommand implements Command {
279288
wsData = wsState.workspaceData; // Update wsData with latest info after workspace is running
280289
}
281290

291+
const domain = getLocalSSHDomain(this.hostService.gitpodHost);
292+
const sshHostname = `${wsData!.id}.${domain}`;
293+
const localSSHDestination = new SSHDestination(sshHostname, wsData!.id);
294+
let localSSHTestSuccess: boolean = false;
295+
try {
296+
await testLocalSSHConnection(localSSHDestination.user!, localSSHDestination.hostname);
297+
localSSHTestSuccess = true;
298+
} catch (e) {
299+
this.telemetryService.sendTelemetryException(
300+
new WrapError('Local SSH: failed to connect to workspace', e, 'Unknown'),
301+
{
302+
gitpodHost: this.hostService.gitpodHost,
303+
workspaceId: wsData!.id,
304+
}
305+
);
306+
}
307+
282308
let sshDest: SSHDestination;
283309
let password: string | undefined;
284-
if (await this.experimentsService.getUseLocalSSHProxy()) {
285-
const domain = getLocalSSHDomain(this.hostService.gitpodHost);
286-
const sshHostname = `${wsData!.id}.${domain}`;
287-
sshDest = new SSHDestination(sshHostname, wsData!.id);
310+
if (await this.experimentsService.getUseLocalSSHProxy() && localSSHTestSuccess) {
311+
sshDest = localSSHDestination;
288312
} else {
289313
({ destination: sshDest, password } = await this.remoteService.getWorkspaceSSHDestination(wsData!));
290314
}
@@ -318,16 +342,10 @@ export class ConnectInCurrentWindowCommand implements Command {
318342

319343
private async initializeLocalSSH(workspaceId: string) {
320344
try {
321-
const [isSupportLocalSSH, isExtensionServerReady] = await Promise.all([
345+
await Promise.all([
322346
this.remoteService.setupSSHProxy(),
323-
this.remoteService.extensionServerReady()
347+
this.remoteService.startLocalSSHServiceServer()
324348
]);
325-
if (!isExtensionServerReady) {
326-
throw new NoExtensionIPCServerError();
327-
}
328-
if (!isSupportLocalSSH) {
329-
throw new NoLocalSSHSupportError();
330-
}
331349
} catch (e) {
332350
const openSSHVersion = await getOpenSSHVersion();
333351
this.telemetryService.sendTelemetryException(new WrapError('Local SSH: failed to initialize local SSH', e), {

src/local-ssh/ipc/extensionServiceServer.ts

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { NodeHttpTransport } from '@improbable-eng/grpc-web-node-http-transport'
1919
import { CreateSSHKeyPairRequest } from '@gitpod/supervisor-api-grpcweb/lib/control_pb';
2020
import * as ssh2 from 'ssh2';
2121
import { ParsedKey } from 'ssh2-streams';
22-
import { isPortUsed } from '../../common/ports';
2322
import { WrapError } from '../../common/utils';
2423
import { ConnectError, Code } from '@bufbuild/connect';
2524
import { rawWorkspaceToWorkspaceData } from '../../publicApi';
@@ -211,10 +210,8 @@ class ExtensionServiceImpl implements ExtensionServiceImplementation {
211210
}
212211

213212
export class ExtensionServiceServer extends Disposable {
214-
static MAX_LOCAL_SSH_PING_RETRY_COUNT = 10;
215-
static MAX_EXTENSION_ACTIVE_RETRY_COUNT = 10;
216-
217213
private server: Server;
214+
private isListeningPromise: Promise<boolean>;
218215

219216
constructor(
220217
private readonly logService: ILogService,
@@ -224,7 +221,7 @@ export class ExtensionServiceServer extends Disposable {
224221
) {
225222
super();
226223
this.server = this.getServer();
227-
this.tryActive();
224+
this.isListeningPromise = this.tryActive();
228225
}
229226

230227
private getServer(): Server {
@@ -234,34 +231,32 @@ export class ExtensionServiceServer extends Disposable {
234231
return server;
235232
}
236233

237-
private async tryActive() {
234+
private tryActive() {
238235
const port = Configuration.getLocalSshExtensionIpcPort();
239-
// TODO:
240-
// commenting this as it pollutes extension logs
241-
// verify port is used by our extension or show message to user
242-
// this.logService.debug('going to try active extension ipc service server on port ' + port);
243-
this.server.listen('127.0.0.1:' + port).then(() => {
244-
this.logService.info('extension ipc service server started to listen');
245-
}).catch(_e => {
246-
// this.logService.debug(`extension ipc service server failed to listen`, e);
247-
// TODO(lssh): listen to port and wait until disconnect and try again
248-
timeout(1000).then(() => {
249-
this.tryActive();
250-
});
236+
const promise = this.server.listen('127.0.0.1:' + port);
237+
promise.catch(async () => {
238+
if (this.isDisposed) {
239+
return;
240+
}
241+
242+
await timeout(1000);
243+
return this.isListeningPromise = this.tryActive();
251244
});
245+
return promise.then(() => true, () => false);
252246
}
253247

254-
public override dispose() {
255-
this.server.forceShutdown();
256-
}
257-
}
248+
async canExtensionServiceServerWork(): Promise<true> {
249+
if ((await this.isListeningPromise)) {
250+
return true;
251+
}
258252

259-
export async function canExtensionServiceServerWork(): Promise<true> {
260-
const port = Configuration.getLocalSshExtensionIpcPort();
261-
if (!(await isPortUsed(port))) {
253+
const port = Configuration.getLocalSshExtensionIpcPort();
254+
const extensionIpc = createClient(ExtensionServiceDefinition, createChannel(`127.0.0.1:${port}`));
255+
await extensionIpc.ping({});
262256
return true;
263257
}
264-
const extensionIpc = createClient(ExtensionServiceDefinition, createChannel(`127.0.0.1:${port}`));
265-
await extensionIpc.ping({});
266-
return true;
258+
259+
public override dispose() {
260+
this.server.forceShutdown();
261+
}
267262
}

src/remoteConnector.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ import { addHostToHostFile, checkNewHostInHostkeys } from './ssh/hostfile';
2626
import { ScopeFeature } from './featureSupport';
2727
import SSHConfiguration from './ssh/sshConfig';
2828
import { ExperimentalSettings, isUserOverrideSetting } from './experiments';
29-
import { getOpenSSHVersion } from './ssh/sshVersion';
29+
import { getOpenSSHVersion, testSSHConnection as testLocalSSHConnection } from './ssh/nativeSSH';
3030
import { INotificationService } from './services/notificationService';
3131
import { SSHKey } from '@gitpod/public-api/lib/gitpod/experimental/v1/user_pb';
32-
import { getAgentSock, SSHError, testSSHConnection } from './sshTestConnection';
32+
import { getAgentSock, SSHError, testSSHConnection as testSSHGatewayConnection } from './sshTestConnection';
3333
import { gatherIdentityFiles } from './ssh/identityFiles';
3434
import { isWindows } from './common/platform';
3535
import SSHDestination from './ssh/sshDestination';
36-
import { NoExtensionIPCServerError, NoLocalSSHSupportError, NoRunningInstanceError, NoSSHGatewayError, SSHConnectionParams, SSH_DEST_KEY, getLocalSSHDomain } from './remote';
36+
import { NoRunningInstanceError, NoSSHGatewayError, SSHConnectionParams, SSH_DEST_KEY, getLocalSSHDomain } from './remote';
3737
import { ISessionService } from './services/sessionService';
3838
import { ILogService } from './services/logService';
3939
import { IHostService } from './services/hostService';
@@ -461,7 +461,7 @@ export class RemoteConnector extends Disposable {
461461

462462
const sshConfiguration = await SSHConfiguration.loadFromFS();
463463

464-
const verifiedHostKey = await testSSHConnection({
464+
const verifiedHostKey = await testSSHGatewayConnection({
465465
host: hostname,
466466
username: user,
467467
readyTimeout: 40000,
@@ -692,33 +692,27 @@ export class RemoteConnector extends Disposable {
692692

693693
const forceUseLocalApp = Configuration.getUseLocalApp();
694694
const userOverride = String(isUserOverrideSetting('gitpod.remote.useLocalApp'));
695-
let sshDestination: SSHDestination | undefined;
696-
const useLocalSSH = await this.experiments.getUseLocalSSHProxy();
697-
sshFlow.useLocalSSH = String(useLocalSSH);
698-
if (!forceUseLocalApp && useLocalSSH) {
699-
const openSSHVersion = await getOpenSSHVersion();
695+
const openSSHVersion = await getOpenSSHVersion();
696+
697+
// Always try to run a local ssh connection collect success metrics
698+
let localSSHDestination: SSHDestination | undefined;
699+
let localSSHTestSuccess: boolean = false;
700+
if (!forceUseLocalApp) {
700701
const localSSHFlow: UserFlowTelemetryProperties = { kind: 'local-ssh', openSSHVersion, userOverride, ...sshFlow };
701702
try {
702703
this.telemetryService.sendUserFlowStatus('connecting', localSSHFlow);
703704
// If needed, revert local-app changes first
704705
await this.updateRemoteSSHConfig(true, undefined);
705706

706707
this.remoteService.flow = sshFlow;
707-
const [isSupportLocalSSH, isExtensionServerReady] = await Promise.all([
708+
await Promise.all([
708709
this.remoteService.setupSSHProxy(),
709-
this.remoteService.extensionServerReady()
710+
this.remoteService.startLocalSSHServiceServer()
710711
]);
711-
if (!isExtensionServerReady) {
712-
throw new NoExtensionIPCServerError();
713-
}
714-
if (!isSupportLocalSSH) {
715-
throw new NoLocalSSHSupportError();
716-
}
717-
this.logService.info('Going to use lssh');
718712

719-
const { destination } = await this.getLocalSSHWorkspaceSSHDestination(params);
720-
params.connType = 'local-ssh';
721-
sshDestination = destination;
713+
({ destination: localSSHDestination } = await this.getLocalSSHWorkspaceSSHDestination(params));
714+
await testLocalSSHConnection(localSSHDestination.user!, localSSHDestination.hostname);
715+
localSSHTestSuccess = true;
722716

723717
this.telemetryService.sendUserFlowStatus('connected', localSSHFlow);
724718
} catch (e) {
@@ -731,8 +725,15 @@ export class RemoteConnector extends Disposable {
731725
}
732726
}
733727

728+
let sshDestination: SSHDestination | undefined;
729+
730+
if (await this.experiments.getUseLocalSSHProxy() && localSSHTestSuccess) {
731+
this.logService.info('Going to use lssh');
732+
sshDestination = localSSHDestination;
733+
params.connType = 'local-ssh';
734+
}
735+
734736
if (!forceUseLocalApp && sshDestination === undefined) {
735-
const openSSHVersion = await getOpenSSHVersion();
736737
const gatewayFlow: UserFlowTelemetryProperties = { kind: 'gateway', openSSHVersion, userOverride, ...sshFlow };
737738
try {
738739
this.telemetryService.sendUserFlowStatus('connecting', gatewayFlow);

src/remoteSession.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class RemoteSession extends Disposable {
6767
try {
6868
const useLocalSSH = await this.experiments.getUseLocalSSHProxy();
6969
if (useLocalSSH) {
70-
this.extensionServiceServer = new ExtensionServiceServer(this.logService, this.sessionService, this.hostService, this.telemetryService);
70+
this.remoteService.startLocalSSHServiceServer().catch(() => {/* ignore */ });
7171
}
7272

7373
this.usePublicApi = await this.experiments.getUsePublicAPI(this.connectionInfo.gitpodHost);

0 commit comments

Comments
 (0)