Skip to content

Commit ba9488c

Browse files
authored
Ping extension ipc server before use local ssh IDE-77 (#73)
* Ping extension ipc server before use local ssh
1 parent 7126165 commit ba9488c

File tree

5 files changed

+176
-6
lines changed

5 files changed

+176
-6
lines changed

src/local-ssh/ipc/extensionServiceServer.ts

Lines changed: 31 additions & 2 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 { ExtensionServiceDefinition, ExtensionServiceImplementation, GetWorkspaceAuthInfoRequest, GetWorkspaceAuthInfoResponse, SendErrorReportRequest, SendLocalSSHUserFlowStatusRequest } from '../../proto/typescript/ipc/v1/ipc';
6+
import { ExtensionServiceDefinition, ExtensionServiceImplementation, GetWorkspaceAuthInfoRequest, GetWorkspaceAuthInfoResponse, PingRequest, SendErrorReportRequest, SendLocalSSHUserFlowStatusRequest } from '../../proto/typescript/ipc/v1/ipc';
77
import { Disposable } from '../../common/dispose';
88
export { ExtensionServiceDefinition } from '../../proto/typescript/ipc/v1/ipc';
99
import { withServerApi } from '../../internalApi';
@@ -13,7 +13,7 @@ import { ILogService } from '../../services/logService';
1313
import { ISessionService } from '../../services/sessionService';
1414
import { CallContext, ServerError, Status } from 'nice-grpc-common';
1515
import { IHostService } from '../../services/hostService';
16-
import { Server, createServer } from 'nice-grpc';
16+
import { Server, createChannel, createClient, createServer } from 'nice-grpc';
1717
import { ITelemetryService, UserFlowTelemetryProperties } from '../../common/telemetry';
1818
import { ExperimentalSettings } from '../../experiments';
1919
import { Configuration } from '../../configuration';
@@ -24,6 +24,7 @@ import { NodeHttpTransport } from '@improbable-eng/grpc-web-node-http-transport'
2424
import { CreateSSHKeyPairRequest } from '@gitpod/supervisor-api-grpcweb/lib/control_pb';
2525
import * as ssh2 from 'ssh2';
2626
import { ParsedKey } from 'ssh2-streams';
27+
import { createServer as netCreateServer } from 'net';
2728

2829
const phaseMap: Record<WorkspaceInstanceStatus_Phase, WorkspaceInstancePhase | undefined> = {
2930
[WorkspaceInstanceStatus_Phase.CREATING]: 'pending',
@@ -150,6 +151,10 @@ class ExtensionServiceImpl implements ExtensionServiceImplementation {
150151
});
151152
return {};
152153
}
154+
155+
async ping(_request: PingRequest, _context: CallContext): Promise<{}> {
156+
return {};
157+
}
153158
}
154159

155160
export class ExtensionServiceServer extends Disposable {
@@ -198,3 +203,27 @@ export class ExtensionServiceServer extends Disposable {
198203
this.server.forceShutdown();
199204
}
200205
}
206+
207+
async function isPortUsed(port: number): Promise<boolean> {
208+
return new Promise<boolean>((resolve, _reject) => {
209+
const server = netCreateServer();
210+
server.once('error', () => {
211+
resolve(true);
212+
});
213+
server.once('listening', () => {
214+
server.close();
215+
resolve(false);
216+
});
217+
server.listen(port);
218+
});
219+
}
220+
221+
export async function canExtensionServiceServerWork(): Promise<true> {
222+
const port = Configuration.getLocalSshExtensionIpcPort();
223+
if (!(await isPortUsed(port))) {
224+
return true;
225+
}
226+
const extensionIpc = createClient(ExtensionServiceDefinition, createChannel(`127.0.0.1:${port}`));
227+
await extensionIpc.ping({});
228+
return true;
229+
}

src/proto/ipc/v1/ipc.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ service ExtensionService {
66
rpc GetWorkspaceAuthInfo (GetWorkspaceAuthInfoRequest) returns (GetWorkspaceAuthInfoResponse) {}
77
rpc SendLocalSSHUserFlowStatus (SendLocalSSHUserFlowStatusRequest) returns (SendLocalSSHUserFlowStatusResponse) {}
88
rpc SendErrorReport (SendErrorReportRequest) returns (SendErrorReportResponse) {}
9+
rpc Ping (PingRequest) returns (PingResponse) {}
910
}
1011

12+
message PingRequest {}
13+
message PingResponse {}
14+
1115
message SendErrorReportRequest {
1216
string workspace_id = 1;
1317
string instance_id = 2;

src/proto/typescript/ipc/v1/ipc.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import * as _m0 from "protobufjs/minimal";
44

55
export const protobufPackage = "ipc.v1";
66

7+
export interface PingRequest {
8+
}
9+
10+
export interface PingResponse {
11+
}
12+
713
export interface SendErrorReportRequest {
814
workspaceId: string;
915
instanceId: string;
@@ -206,6 +212,92 @@ export interface GetWorkspaceAuthInfoResponse {
206212
phase: string;
207213
}
208214

215+
function createBasePingRequest(): PingRequest {
216+
return {};
217+
}
218+
219+
export const PingRequest = {
220+
encode(_: PingRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
221+
return writer;
222+
},
223+
224+
decode(input: _m0.Reader | Uint8Array, length?: number): PingRequest {
225+
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
226+
let end = length === undefined ? reader.len : reader.pos + length;
227+
const message = createBasePingRequest();
228+
while (reader.pos < end) {
229+
const tag = reader.uint32();
230+
switch (tag >>> 3) {
231+
default:
232+
reader.skipType(tag & 7);
233+
break;
234+
}
235+
}
236+
return message;
237+
},
238+
239+
fromJSON(_: any): PingRequest {
240+
return {};
241+
},
242+
243+
toJSON(_: PingRequest): unknown {
244+
const obj: any = {};
245+
return obj;
246+
},
247+
248+
create(base?: DeepPartial<PingRequest>): PingRequest {
249+
return PingRequest.fromPartial(base ?? {});
250+
},
251+
252+
fromPartial(_: DeepPartial<PingRequest>): PingRequest {
253+
const message = createBasePingRequest();
254+
return message;
255+
},
256+
};
257+
258+
function createBasePingResponse(): PingResponse {
259+
return {};
260+
}
261+
262+
export const PingResponse = {
263+
encode(_: PingResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
264+
return writer;
265+
},
266+
267+
decode(input: _m0.Reader | Uint8Array, length?: number): PingResponse {
268+
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
269+
let end = length === undefined ? reader.len : reader.pos + length;
270+
const message = createBasePingResponse();
271+
while (reader.pos < end) {
272+
const tag = reader.uint32();
273+
switch (tag >>> 3) {
274+
default:
275+
reader.skipType(tag & 7);
276+
break;
277+
}
278+
}
279+
return message;
280+
},
281+
282+
fromJSON(_: any): PingResponse {
283+
return {};
284+
},
285+
286+
toJSON(_: PingResponse): unknown {
287+
const obj: any = {};
288+
return obj;
289+
},
290+
291+
create(base?: DeepPartial<PingResponse>): PingResponse {
292+
return PingResponse.fromPartial(base ?? {});
293+
},
294+
295+
fromPartial(_: DeepPartial<PingResponse>): PingResponse {
296+
const message = createBasePingResponse();
297+
return message;
298+
},
299+
};
300+
209301
function createBaseSendErrorReportRequest(): SendErrorReportRequest {
210302
return {
211303
workspaceId: "",
@@ -790,6 +882,14 @@ export const ExtensionServiceDefinition = {
790882
responseStream: false,
791883
options: {},
792884
},
885+
ping: {
886+
name: "Ping",
887+
requestType: PingRequest,
888+
requestStream: false,
889+
responseType: PingResponse,
890+
responseStream: false,
891+
options: {},
892+
},
793893
},
794894
} as const;
795895

@@ -806,6 +906,7 @@ export interface ExtensionServiceImplementation<CallContextExt = {}> {
806906
request: SendErrorReportRequest,
807907
context: CallContext & CallContextExt,
808908
): Promise<DeepPartial<SendErrorReportResponse>>;
909+
ping(request: PingRequest, context: CallContext & CallContextExt): Promise<DeepPartial<PingResponse>>;
809910
}
810911

811912
export interface ExtensionServiceClient<CallOptionsExt = {}> {
@@ -821,6 +922,7 @@ export interface ExtensionServiceClient<CallOptionsExt = {}> {
821922
request: DeepPartial<SendErrorReportRequest>,
822923
options?: CallOptions & CallOptionsExt,
823924
): Promise<SendErrorReportResponse>;
925+
ping(request: DeepPartial<PingRequest>, options?: CallOptions & CallOptionsExt): Promise<PingResponse>;
824926
}
825927

826928
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;

src/remoteConnector.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -689,11 +689,20 @@ export class RemoteConnector extends Disposable {
689689

690690
let useLocalSSH = await this.experiments.getUseLocalSSHProxy();
691691
if (useLocalSSH) {
692-
// we need to update the remote ssh config first, since another call is too late for local-ssh
693-
await this.updateRemoteSSHConfig(true, undefined);
694692
this.localSSHService.flow = sshFlow;
695-
this.localSSHService.prepareInitialize();
696-
await this.localSSHService.initialized;
693+
const [_, isExtensionServerReady] = await Promise.all([
694+
(async () => {
695+
// we need to update the remote ssh config first, since another call is too late for local-ssh
696+
await this.updateRemoteSSHConfig(true, undefined);
697+
this.localSSHService.prepareInitialize();
698+
await this.localSSHService.initialized;
699+
})(),
700+
this.localSSHService.extensionServerReady(),
701+
]);
702+
if (!isExtensionServerReady) {
703+
this.logService.error('Extension IPC server is not ready');
704+
useLocalSSH = false;
705+
}
697706
if (!this.localSSHService.isSupportLocalSSH) {
698707
this.logService.error('Local SSH is not supported on this platform');
699708
useLocalSSH = false;

src/services/localSSHService.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ import { getLocalSSHDomain } from '../remote';
1616
import { ITelemetryService, UserFlowTelemetryProperties } from '../common/telemetry';
1717
import { ISessionService } from './sessionService';
1818
import { WrapError } from '../common/utils';
19+
import { canExtensionServiceServerWork } from '../local-ssh/ipc/extensionServiceServer';
1920

2021
export interface ILocalSSHService {
2122
flow?: UserFlowTelemetryProperties;
2223
isSupportLocalSSH: boolean;
2324
initialized: Promise<void>;
2425
prepareInitialize: () => void;
26+
extensionServerReady: () => Promise<boolean>;
2527
}
2628

2729
type FailedToInitializeCode = 'Unknown' | 'LockFailed' | string;
@@ -64,6 +66,30 @@ export class LocalSSHService extends Disposable implements ILocalSSHService {
6466
}
6567
}));
6668
}
69+
async extensionServerReady(): Promise<boolean> {
70+
try {
71+
await canExtensionServiceServerWork();
72+
return true;
73+
} catch (e) {
74+
const failureCode = 'ExtensionServerUnavailable';
75+
const err = new WrapError('cannot ping extension ipc service server', e, failureCode);
76+
const flow = {
77+
...this.flow,
78+
flow: 'ping_extension_server',
79+
gitpodHost: this.hostService.gitpodHost,
80+
userId: this.sessionService.safeGetUserId(),
81+
failureCode,
82+
};
83+
this.telemetryService.sendTelemetryException(err, {
84+
gitpodHost: flow.gitpodHost,
85+
userId: flow.userId,
86+
instanceId: flow.instanceId,
87+
workspaceId: flow.workspaceId,
88+
});
89+
this.telemetryService.sendUserFlowStatus('failure', flow);
90+
return false;
91+
}
92+
}
6793

6894
private async initialize() {
6995
let failureCode: FailedToInitializeCode | undefined;

0 commit comments

Comments
 (0)