src/vs/platform/remote/common/remoteAgentConnection.ts

839 LOC · 265 covered · 574 uncovered · 53 ranges · 7 concepts · 1 introducers · 6 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/platform/remote/common/managedSocket.ts · 145 LOCcommon/managedSocket.tssrc/vs/platform/remote/common/remoteSocketFactoryService.ts · 60 LOCcommon/remoteSocketFacto…src/vs/platform/sign/common/sign.ts · 22 LOCcommon/sign.tssrc/vs/platform/tunnel/node/tunnelService.ts · 265 LOCnode/tunnelService.tssrc/vs/workbench/api/node/extHostTunnelService.ts · 419 LOCnode/extHostTunnelServic…src/vs/workbench/services/remote/common/tunnelModel.ts · 1042 LOCcommon/tunnelModel.tsextHostTunnelService.ts ×3 · 22 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×2 · 21 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×4 · 38 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×2 · 23 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×1 · 9 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×1 · 17 introduced LOCextHostTunnelService.ts …remoteAgentConnection.ts ×53 · 823 introduced LOCremoteAgentConnection.ts…extHostTunnelService.test|title=ExtHostTunnelService findPorts|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService findPorts|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService getSockets|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService getSockets|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService loadConnectionTable|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService loadConnectionTable|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService loadListeningPorts|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService loadListeningPorts|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService parseIpAddress|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService parseIpAddress|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService tryFindRootPorts|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService tryFindRootPorts|occurrence=1extHostTunnelService.tes…Focused file · src/vs/platform/remote/common/remoteAgentConnection.ts · 839 LOCcommon/remoteAgentConnec…

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- remoteAgentConnection.ts ×53
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { CancelablePromise, createCancelablePromise, promiseWithResolvers } from '../../../base/common/async.js';
7 > import { VSBuffer } from '../../../base/common/buffer.js';
8 > import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
9 > import { isCancellationError, onUnexpectedError } from '../../../base/common/errors.js';
10 > import { Emitter } from '../../../base/common/event.js';
11 > import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
12 > import { RemoteAuthorities } from '../../../base/common/network.js';
13 > import * as performance from '../../../base/common/performance.js';
14 > import { StopWatch } from '../../../base/common/stopwatch.js';
15 > import { generateUuid } from '../../../base/common/uuid.js';
16 > import { IIPCLogger } from '../../../base/parts/ipc/common/ipc.js';
17 > import { Client, ISocket, PersistentProtocol, ProtocolConstants, SocketCloseEventType } from '../../../base/parts/ipc/common/ipc.net.js';
18 > import { ILogService } from '../../log/common/log.js';
19 > import { RemoteAgentConnectionContext } from './remoteAgentEnvironment.js';
20 > import { RemoteAuthorityResolverError, RemoteConnection } from './remoteAuthorityResolver.js';
21 > import { IRemoteSocketFactoryService } from './remoteSocketFactoryService.js';
22 > import { ISignService } from '../../sign/common/sign.js';
23 >
24 > const RECONNECT_TIMEOUT = 30 * 1000 /* 30s */;
25 >
26 > export const enum ConnectionType {
27 > Management = 1,
28 > ExtensionHost = 2,
29 > Tunnel = 3,
30 > }
31 >
32 function connectionTypeToString(connectionType: ConnectionType): string {
33 switch (connectionType) {
34 case ConnectionType.Management:
35 return 'Management';
36 case ConnectionType.ExtensionHost:
37 return 'ExtensionHost';
38 case ConnectionType.Tunnel:
39 return 'Tunnel';
40 }
41 }
43 > export interface AuthRequest {
44 > type: 'auth';
45 > auth: string;
46 > data: string;
47 > }
48 >
49 > export interface SignRequest {
50 > type: 'sign';
51 > data: string;
52 > signedData: string;
53 > }
54 >
55 > export interface ConnectionTypeRequest {
56 > type: 'connectionType';
57 > commit?: string;
58 > signedData: string;
59 > desiredConnectionType?: ConnectionType;
60 > args?: any;
61 > }
62 >
63 > export interface ErrorMessage {
64 > type: 'error';
65 > reason: string;
66 > }
67 >
68 > export interface OKMessage {
69 > type: 'ok';
70 > }
71 >
72 > export type HandshakeMessage = AuthRequest | SignRequest | ConnectionTypeRequest | ErrorMessage | OKMessage;
73 >
74 >
75 > interface ISimpleConnectionOptions<T extends RemoteConnection = RemoteConnection> {
76 > commit: string | undefined;
77 > quality: string | undefined;
78 > connectTo: T;
79 > connectionToken: string | undefined;
80 > reconnectionToken: string;
81 > reconnectionProtocol: PersistentProtocol | null;
82 > remoteSocketFactoryService: IRemoteSocketFactoryService;
83 > signService: ISignService;
84 > logService: ILogService;
85 > }
86 >
87 function createTimeoutCancellation(millis: number): CancellationToken {
88 const source = new CancellationTokenSource();
89 setTimeout(() => source.cancel(), millis);
90 return source.token;
91 }
93 function combineTimeoutCancellation(a: CancellationToken, b: CancellationToken): CancellationToken {
94 if (a.isCancellationRequested || b.isCancellationRequested) {
95 return CancellationToken.Cancelled;
96 }
97 const source = new CancellationTokenSource();
98 a.onCancellationRequested(() => source.cancel());
99 b.onCancellationRequested(() => source.cancel());
100 return source.token;
101 }
103 > class PromiseWithTimeout<T> {
104 >
105 > private _state: 'pending' | 'resolved' | 'rejected' | 'timedout';
106 > private readonly _disposables: DisposableStore;
107 > public readonly promise: Promise<T>;
108 > private readonly _resolvePromise: (value: T) => void;
109 > private readonly _rejectPromise: (err: any) => void;
110 >
111 > public get didTimeout(): boolean {
112 return (this._state === 'timedout');
113 }
115 > constructor(timeoutCancellationToken: CancellationToken) {
116 this._state = 'pending';
117 this._disposables = new DisposableStore();
118
119 ({ promise: this.promise, resolve: this._resolvePromise, reject: this._rejectPromise } = promiseWithResolvers<T>());
120
121 if (timeoutCancellationToken.isCancellationRequested) {
122 this._timeout();
123 } else {
124 this._disposables.add(timeoutCancellationToken.onCancellationRequested(() => this._timeout()));
125 }
126 }
128 > public registerDisposable(disposable: IDisposable): void {
129 if (this._state === 'pending') {
130 this._disposables.add(disposable);
131 } else {
132 disposable.dispose();
133 }
134 }
136 > private _timeout(): void {
137 if (this._state !== 'pending') {
138 return;
139 }
140 this._disposables.dispose();
141 this._state = 'timedout';
142 this._rejectPromise(this._createTimeoutError());
143 }
145 > private _createTimeoutError(): Error {
146 const err: any = new Error('Time limit reached');
147 err.code = 'ETIMEDOUT';
148 err.syscall = 'connect';
149 return err;
150 }
152 > public resolve(value: T): void {
153 if (this._state !== 'pending') {
154 return;
155 }
156 this._disposables.dispose();
157 this._state = 'resolved';
158 this._resolvePromise(value);
159 }
161 > public reject(err: any): void {
162 if (this._state !== 'pending') {
163 return;
164 }
165 this._disposables.dispose();
166 this._state = 'rejected';
167 this._rejectPromise(err);
168 }
170 >
171 function readOneControlMessage<T>(protocol: PersistentProtocol, timeoutCancellationToken: CancellationToken): Promise<T> {
172 const result = new PromiseWithTimeout<T>(timeoutCancellationToken);
173 result.registerDisposable(protocol.onControlMessage(raw => {
174 const msg: T = JSON.parse(raw.toString());
175 const error = getErrorFromMessage(msg);
176 if (error) {
177 result.reject(error);
178 } else {
179 result.resolve(msg);
180 }
181 }));
182 return result.promise;
183 }
185 function createSocket<T extends RemoteConnection>(logService: ILogService, remoteSocketFactoryService: IRemoteSocketFactoryService, connectTo: T, path: string, query: string, debugConnectionType: string, debugLabel: string, timeoutCancellationToken: CancellationToken): Promise<ISocket> {
186 const result = new PromiseWithTimeout<ISocket>(timeoutCancellationToken);
187 const sw = StopWatch.create(false);
188 logService.info(`Creating a socket (${debugLabel})...`);
189 performance.mark(`code/willCreateSocket/${debugConnectionType}`);
190
191 remoteSocketFactoryService.connect(connectTo, path, query, debugLabel).then((socket) => {
192 if (result.didTimeout) {
193 performance.mark(`code/didCreateSocketError/${debugConnectionType}`);
194 logService.info(`Creating a socket (${debugLabel}) finished after ${sw.elapsed()} ms, but this is too late and has timed out already.`);
195 socket?.dispose();
196 } else {
197 performance.mark(`code/didCreateSocketOK/${debugConnectionType}`);
198 logService.info(`Creating a socket (${debugLabel}) was successful after ${sw.elapsed()} ms.`);
199 result.resolve(socket);
200 }
201 }, (err) => {
202 performance.mark(`code/didCreateSocketError/${debugConnectionType}`);
203 logService.info(`Creating a socket (${debugLabel}) returned an error after ${sw.elapsed()} ms.`);
204 logService.error(err);
205 result.reject(err);
206 });
207
208 return result.promise;
209 }
211 function raceWithTimeoutCancellation<T>(promise: Promise<T>, timeoutCancellationToken: CancellationToken): Promise<T> {
212 const result = new PromiseWithTimeout<T>(timeoutCancellationToken);
213 promise.then(
214 (res) => {
215 if (!result.didTimeout) {
216 result.resolve(res);
217 }
218 },
219 (err) => {
220 if (!result.didTimeout) {
221 result.reject(err);
222 }
223 }
224 );
225 return result.promise;
226 }
228 async function connectToRemoteExtensionHostAgent<T extends RemoteConnection>(options: ISimpleConnectionOptions<T>, connectionType: ConnectionType, args: any | undefined, timeoutCancellationToken: CancellationToken): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean }> {
229 const logPrefix = connectLogPrefix(options, connectionType);
230
231 options.logService.trace(`${logPrefix} 1/6. invoking socketFactory.connect().`);
232
233 let socket: ISocket;
234 try {
235 socket = await createSocket(options.logService, options.remoteSocketFactoryService, options.connectTo, RemoteAuthorities.getServerRootPath(), `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, connectionTypeToString(connectionType), `renderer-${connectionTypeToString(connectionType)}-${options.reconnectionToken}`, timeoutCancellationToken);
236 } catch (error) {
237 options.logService.error(`${logPrefix} socketFactory.connect() failed or timed out. Error:`);
238 options.logService.error(error);
239 throw error;
240 }
241
242 options.logService.trace(`${logPrefix} 2/6. socketFactory.connect() was successful.`);
243
244 let protocol: PersistentProtocol;
245 let ownsProtocol: boolean;
246 if (options.reconnectionProtocol) {
247 options.reconnectionProtocol.beginAcceptReconnection(socket, null);
248 protocol = options.reconnectionProtocol;
249 ownsProtocol = false;
250 } else {
251 protocol = new PersistentProtocol({ socket });
252 ownsProtocol = true;
253 }
254
255 options.logService.trace(`${logPrefix} 3/6. sending AuthRequest control message.`);
256 const message = await raceWithTimeoutCancellation(options.signService.createNewMessage(generateUuid()), timeoutCancellationToken);
257
258 const authRequest: AuthRequest = {
259 type: 'auth',
260 auth: options.connectionToken || '00000000000000000000',
261 data: message.data
262 };
263 protocol.sendControl(VSBuffer.fromString(JSON.stringify(authRequest)));
264
265 try {
266 const msg = await readOneControlMessage<HandshakeMessage>(protocol, combineTimeoutCancellation(timeoutCancellationToken, createTimeoutCancellation(10000)));
267
268 if (msg.type !== 'sign' || typeof msg.data !== 'string') {
269 const error: any = new Error('Unexpected handshake message');
270 error.code = 'VSCODE_CONNECTION_ERROR';
271 throw error;
272 }
273
274 options.logService.trace(`${logPrefix} 4/6. received SignRequest control message.`);
275
276 const isValid = await raceWithTimeoutCancellation(options.signService.validate(message, msg.signedData), timeoutCancellationToken);
277 if (!isValid) {
278 const error: any = new Error('Refused to connect to unsupported server');
279 error.code = 'VSCODE_CONNECTION_ERROR';
280 throw error;
281 }
282
283 const signed = await raceWithTimeoutCancellation(options.signService.sign(msg.data), timeoutCancellationToken);
284 const connTypeRequest: ConnectionTypeRequest = {
285 type: 'connectionType',
286 commit: options.commit,
287 signedData: signed,
288 desiredConnectionType: connectionType
289 };
290 if (args) {
291 connTypeRequest.args = args;
292 }
293
294 options.logService.trace(`${logPrefix} 5/6. sending ConnectionTypeRequest control message.`);
295 protocol.sendControl(VSBuffer.fromString(JSON.stringify(connTypeRequest)));
296
297 return { protocol, ownsProtocol };
298
299 } catch (error) {
300 if (error && error.code === 'ETIMEDOUT') {
301 options.logService.error(`${logPrefix} the handshake timed out. Error:`);
302 options.logService.error(error);
303 }
304 if (error && error.code === 'VSCODE_CONNECTION_ERROR') {
305 options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`);
306 options.logService.error(error);
307 }
308 if (ownsProtocol) {
309 safeDisposeProtocolAndSocket(protocol);
310 }
311 throw error;
312 }
313 }
315 > interface IManagementConnectionResult {
316 > protocol: PersistentProtocol;
317 > }
318 >
319 async function connectToRemoteExtensionHostAgentAndReadOneMessage<T>(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined, timeoutCancellationToken: CancellationToken): Promise<{ protocol: PersistentProtocol; firstMessage: T }> {
320 const startTime = Date.now();
321 const logPrefix = connectLogPrefix(options, connectionType);
322 const { protocol, ownsProtocol } = await connectToRemoteExtensionHostAgent(options, connectionType, args, timeoutCancellationToken);
323 const result = new PromiseWithTimeout<{ protocol: PersistentProtocol; firstMessage: T }>(timeoutCancellationToken);
324 result.registerDisposable(protocol.onControlMessage(raw => {
325 const msg: T = JSON.parse(raw.toString());
326 const error = getErrorFromMessage(msg);
327 if (error) {
328 options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`);
329 options.logService.error(error);
330 if (ownsProtocol) {
331 safeDisposeProtocolAndSocket(protocol);
332 }
333 result.reject(error);
334 } else {
335 options.reconnectionProtocol?.endAcceptReconnection();
336 options.logService.trace(`${logPrefix} 6/6. handshake finished, connection is up and running after ${logElapsed(startTime)}!`);
337 result.resolve({ protocol, firstMessage: msg });
338 }
339 }));
340 return result.promise;
341 }
343 async function doConnectRemoteAgentManagement(options: ISimpleConnectionOptions, timeoutCancellationToken: CancellationToken): Promise<IManagementConnectionResult> {
344 const { protocol } = await connectToRemoteExtensionHostAgentAndReadOneMessage(options, ConnectionType.Management, undefined, timeoutCancellationToken);
345 return { protocol };
346 }
348 > export interface IRemoteExtensionHostStartParams {
349 > language: string;
350 > debugId?: string;
351 > break?: boolean;
352 > port?: number | null;
353 > env?: { [key: string]: string | null };
354 > }
355 >
356 > interface IExtensionHostConnectionResult {
357 > protocol: PersistentProtocol;
358 > debugPort?: number;
359 > }
360 >
361 async function doConnectRemoteAgentExtensionHost(options: ISimpleConnectionOptions, startArguments: IRemoteExtensionHostStartParams, timeoutCancellationToken: CancellationToken): Promise<IExtensionHostConnectionResult> {
362 const { protocol, firstMessage } = await connectToRemoteExtensionHostAgentAndReadOneMessage<{ debugPort?: number }>(options, ConnectionType.ExtensionHost, startArguments, timeoutCancellationToken);
363 const debugPort = firstMessage && firstMessage.debugPort;
364 return { protocol, debugPort };
365 }
367 > export interface ITunnelConnectionStartParams {
368 > host: string;
369 > port: number;
370 > }
371 >
372 async function doConnectRemoteAgentTunnel(options: ISimpleConnectionOptions, startParams: ITunnelConnectionStartParams, timeoutCancellationToken: CancellationToken): Promise<PersistentProtocol> {
373 const startTime = Date.now();
374 const logPrefix = connectLogPrefix(options, ConnectionType.Tunnel);
375 const { protocol } = await connectToRemoteExtensionHostAgentAndReadOneMessage(options, ConnectionType.Tunnel, startParams, timeoutCancellationToken);
376 options.logService.trace(`${logPrefix} 6/6. handshake finished, connection is up and running after ${logElapsed(startTime)}!`);
377 return protocol;
378 }
380 > export interface IConnectionOptions<T extends RemoteConnection = RemoteConnection> {
381 > commit: string | undefined;
382 > quality: string | undefined;
383 > addressProvider: IAddressProvider<T>;
384 > remoteSocketFactoryService: IRemoteSocketFactoryService;
385 > signService: ISignService;
386 > logService: ILogService;
387 > ipcLogger: IIPCLogger | null;
388 > }
389 >
390 async function resolveConnectionOptions<T extends RemoteConnection>(options: IConnectionOptions<T>, reconnectionToken: string, reconnectionProtocol: PersistentProtocol | null): Promise<ISimpleConnectionOptions<T>> {
391 const { connectTo, connectionToken } = await options.addressProvider.getAddress();
392 return {
393 commit: options.commit,
394 quality: options.quality,
395 connectTo,
396 connectionToken: connectionToken,
397 reconnectionToken: reconnectionToken,
398 reconnectionProtocol: reconnectionProtocol,
399 remoteSocketFactoryService: options.remoteSocketFactoryService,
400 signService: options.signService,
401 logService: options.logService
402 };
403 }
405 > export interface IAddress<T extends RemoteConnection = RemoteConnection> {
406 > connectTo: T;
407 > connectionToken: string | undefined;
408 > }
409 >
410 > export interface IAddressProvider<T extends RemoteConnection = RemoteConnection> {
411 > getAddress(): Promise<IAddress<T>>;
412 > }
413 >
414 export async function connectRemoteAgentManagement(options: IConnectionOptions, remoteAuthority: string, clientId: string): Promise<ManagementPersistentConnection> {
415 return createInitialConnection(
416 options,
417 async (simpleOptions) => {
418 const { protocol } = await doConnectRemoteAgentManagement(simpleOptions, CancellationToken.None);
419 return new ManagementPersistentConnection(options, remoteAuthority, clientId, simpleOptions.reconnectionToken, protocol);
420 }
421 );
422 }
424 export async function connectRemoteAgentExtensionHost(options: IConnectionOptions, startArguments: IRemoteExtensionHostStartParams): Promise<ExtensionHostPersistentConnection> {
425 return createInitialConnection(
426 options,
427 async (simpleOptions) => {
428 const { protocol, debugPort } = await doConnectRemoteAgentExtensionHost(simpleOptions, startArguments, CancellationToken.None);
429 return new ExtensionHostPersistentConnection(options, startArguments, simpleOptions.reconnectionToken, protocol, debugPort);
430 }
431 );
432 }
434 > /**
435 > * Will attempt to connect 5 times. If it fails 5 consecutive times, it will give up.
436 > */
437 async function createInitialConnection<T extends PersistentConnection, O extends RemoteConnection>(options: IConnectionOptions<O>, connectionFactory: (simpleOptions: ISimpleConnectionOptions<O>) => Promise<T>): Promise<T> {
438 const MAX_ATTEMPTS = 5;
439
440 for (let attempt = 1; ; attempt++) {
441 try {
442 const reconnectionToken = generateUuid();
443 const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null);
444 const result = await connectionFactory(simpleOptions);
445 return result;
446 } catch (err) {
447 if (attempt < MAX_ATTEMPTS) {
448 options.logService.error(`[remote-connection][attempt ${attempt}] An error occurred in initial connection! Will retry... Error:`);
449 options.logService.error(err);
450 } else {
451 options.logService.error(`[remote-connection][attempt ${attempt}] An error occurred in initial connection! It will be treated as a permanent error. Error:`);
452 options.logService.error(err);
453 PersistentConnection.triggerPermanentFailure(0, 0, RemoteAuthorityResolverError.isHandled(err));
454 throw err;
455 }
456 }
457 }
458 }
460 export async function connectRemoteAgentTunnel(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number): Promise<PersistentProtocol> {
461 const simpleOptions = await resolveConnectionOptions(options, generateUuid(), null);
462 const protocol = await doConnectRemoteAgentTunnel(simpleOptions, { host: tunnelRemoteHost, port: tunnelRemotePort }, CancellationToken.None);
463 return protocol;
464 }
466 function sleep(seconds: number): CancelablePromise<void> {
467 return createCancelablePromise(token => {
468 return new Promise((resolve, reject) => {
469 const timeout = setTimeout(resolve, seconds * 1000);
470 token.onCancellationRequested(() => {
471 clearTimeout(timeout);
472 resolve();
473 });
474 });
475 });
476 }
478 > export const enum PersistentConnectionEventType {
479 > ConnectionLost,
480 > ReconnectionWait,
481 > ReconnectionRunning,
482 > ReconnectionPermanentFailure,
483 > ConnectionGain
484 > }
485 > export class ConnectionLostEvent {
486 > public readonly type = PersistentConnectionEventType.ConnectionLost;
487 > constructor(
488 public readonly reconnectionToken: string,
489 public readonly millisSinceLastIncomingData: number
490 ) { }
492 > export class ReconnectionWaitEvent {
493 > public readonly type = PersistentConnectionEventType.ReconnectionWait;
494 > constructor(
495 public readonly reconnectionToken: string,
496 public readonly millisSinceLastIncomingData: number,
497 public readonly durationSeconds: number,
498 private readonly cancellableTimer: CancelablePromise<void>
499 ) { }
501 > public skipWait(): void {
502 this.cancellableTimer.cancel();
503 }
505 > export class ReconnectionRunningEvent {
506 > public readonly type = PersistentConnectionEventType.ReconnectionRunning;
507 > constructor(
508 public readonly reconnectionToken: string,
509 public readonly millisSinceLastIncomingData: number,
510 public readonly attempt: number
511 ) { }
513 > export class ConnectionGainEvent {
514 > public readonly type = PersistentConnectionEventType.ConnectionGain;
515 > constructor(
516 public readonly reconnectionToken: string,
517 public readonly millisSinceLastIncomingData: number,
518 public readonly attempt: number
519 ) { }
521 > export class ReconnectionPermanentFailureEvent {
522 > public readonly type = PersistentConnectionEventType.ReconnectionPermanentFailure;
523 > constructor(
524 public readonly reconnectionToken: string,
525 public readonly millisSinceLastIncomingData: number,
526 public readonly attempt: number,
527 public readonly handled: boolean
528 ) { }
530 > export type PersistentConnectionEvent = ConnectionGainEvent | ConnectionLostEvent | ReconnectionWaitEvent | ReconnectionRunningEvent | ReconnectionPermanentFailureEvent;
531 >
532 > export abstract class PersistentConnection extends Disposable {
533 >
534 > public static triggerPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void {
535 > this._permanentFailure = true;
536 > this._permanentFailureMillisSinceLastIncomingData = millisSinceLastIncomingData;
537 > this._permanentFailureAttempt = attempt;
538 > this._permanentFailureHandled = handled;
539 > this._instances.forEach(instance => instance._gotoPermanentFailure(this._permanentFailureMillisSinceLastIncomingData, this._permanentFailureAttempt, this._permanentFailureHandled));
540 > }
541 >
542 > public static debugTriggerReconnection() {
543 this._instances.forEach(instance => instance._beginReconnecting());
544 }
546 > public static debugPauseSocketWriting() {
547 this._instances.forEach(instance => instance._pauseSocketWriting());
548 }
550 > private static _permanentFailure: boolean = false;
551 > private static _permanentFailureMillisSinceLastIncomingData: number = 0;
552 > private static _permanentFailureAttempt: number = 0;
553 > private static _permanentFailureHandled: boolean = false;
554 > private static _instances: PersistentConnection[] = [];
555 >
556 > private readonly _onDidStateChange = this._register(new Emitter<PersistentConnectionEvent>());
557 > public readonly onDidStateChange = this._onDidStateChange.event;
558 >
559 > private _permanentFailure: boolean = false;
560 > private get _isPermanentFailure(): boolean {
561 return this._permanentFailure || PersistentConnection._permanentFailure;
562 }
564 > private _isReconnecting: boolean = false;
565 > private _isDisposed: boolean = false;
566 > private _reconnectionGraceTime: number = ProtocolConstants.ReconnectionGraceTime;
567 >
568 > constructor(
569 private readonly _connectionType: ConnectionType,
570 protected readonly _options: IConnectionOptions,
571 public readonly reconnectionToken: string,
572 public readonly protocol: PersistentProtocol,
573 private readonly _reconnectionFailureIsFatal: boolean
574 ) {
575 super();
576
577
578 this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, 0, 0));
579
580 this._register(protocol.onSocketClose((e) => {
581 const logPrefix = commonLogPrefix(this._connectionType, this.reconnectionToken, true);
582 if (!e) {
583 this._options.logService.info(`${logPrefix} received socket close event.`);
584 } else if (e.type === SocketCloseEventType.NodeSocketCloseEvent) {
585 this._options.logService.info(`${logPrefix} received socket close event (hadError: ${e.hadError}).`);
586 if (e.error) {
587 this._options.logService.error(e.error);
588 }
589 } else {
590 this._options.logService.info(`${logPrefix} received socket close event (wasClean: ${e.wasClean}, code: ${e.code}, reason: ${e.reason}).`);
591 if (e.event) {
592 this._options.logService.error(e.event);
593 }
594 }
595 this._beginReconnecting();
596 }));
597 this._register(protocol.onSocketTimeout((e) => {
598 const logPrefix = commonLogPrefix(this._connectionType, this.reconnectionToken, true);
599 this._options.logService.info(`${logPrefix} received socket timeout event (reason: ${e.reason}, unacknowledgedMsgCount: ${e.unacknowledgedMsgCount}, timeSinceOldestUnacknowledgedMsg: ${e.timeSinceOldestUnacknowledgedMsg}, timeSinceLastReceivedSomeData: ${e.timeSinceLastReceivedSomeData}).`);
600 this._beginReconnecting();
601 }));
602
603 PersistentConnection._instances.push(this);
604 this._register(toDisposable(() => {
605 const myIndex = PersistentConnection._instances.indexOf(this);
606 if (myIndex >= 0) {
607 PersistentConnection._instances.splice(myIndex, 1);
608 }
609 }));
610
611 if (this._isPermanentFailure) {
612 this._gotoPermanentFailure(PersistentConnection._permanentFailureMillisSinceLastIncomingData, PersistentConnection._permanentFailureAttempt, PersistentConnection._permanentFailureHandled);
613 }
614 }
616 > public updateGraceTime(graceTime: number): void {
617 const sanitizedGrace = sanitizeGraceTime(graceTime, ProtocolConstants.ReconnectionGraceTime);
618 const logPrefix = commonLogPrefix(this._connectionType, this.reconnectionToken, false);
619 this._options.logService.trace(`${logPrefix} Applying reconnection grace time: ${sanitizedGrace}ms (${Math.floor(sanitizedGrace / 1000)}s)`);
620 this._reconnectionGraceTime = sanitizedGrace;
621 }
623 > public override dispose(): void {
624 super.dispose();
625 this._isDisposed = true;
626 }
628 > private async _beginReconnecting(): Promise<void> {
629 // Only have one reconnection loop active at a time.
630 if (this._isReconnecting) {
631 return;
632 }
633 try {
634 this._isReconnecting = true;
635 await this._runReconnectingLoop();
636 } finally {
637 this._isReconnecting = false;
638 }
639 }
641 > private async _runReconnectingLoop(): Promise<void> {
642 if (this._isPermanentFailure || this._isDisposed) {
643 // no more attempts!
644 return;
645 }
646 const logPrefix = commonLogPrefix(this._connectionType, this.reconnectionToken, true);
647 this._options.logService.info(`${logPrefix} starting reconnecting loop. You can get more information with the trace log level.`);
648 this._onDidStateChange.fire(new ConnectionLostEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData()));
649 const TIMES = [0, 5, 5, 10, 10, 10, 10, 10, 30];
650 const graceTime = this._reconnectionGraceTime;
651 this._options.logService.info(`${logPrefix} starting reconnection with grace time: ${graceTime}ms (${Math.floor(graceTime / 1000)}s)`);
652 if (graceTime <= 0) {
653 this._options.logService.error(`${logPrefix} reconnection grace time is set to 0ms, will not attempt to reconnect.`);
654 this._onReconnectionPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), 0, false);
655 return;
656 }
657 const loopStartTime = Date.now();
658 let attempt = -1;
659 do {
660 attempt++;
661 const waitTime = (attempt < TIMES.length ? TIMES[attempt] : TIMES[TIMES.length - 1]);
662 try {
663 if (waitTime > 0) {
664 const sleepPromise = sleep(waitTime);
665 this._onDidStateChange.fire(new ReconnectionWaitEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), waitTime, sleepPromise));
666
667 this._options.logService.info(`${logPrefix} waiting for ${waitTime} seconds before reconnecting...`);
668 try {
669 await sleepPromise;
670 } catch { } // User canceled timer
671 }
672
673 if (this._isPermanentFailure) {
674 this._options.logService.error(`${logPrefix} permanent failure occurred while running the reconnecting loop.`);
675 break;
676 }
677
678 // connection was lost, let's try to re-establish it
679 this._onDidStateChange.fire(new ReconnectionRunningEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1));
680 this._options.logService.info(`${logPrefix} resolving connection...`);
681 const simpleOptions = await resolveConnectionOptions(this._options, this.reconnectionToken, this.protocol);
682 this._options.logService.info(`${logPrefix} connecting to ${simpleOptions.connectTo}...`);
683 await this._reconnect(simpleOptions, createTimeoutCancellation(RECONNECT_TIMEOUT));
684 this._options.logService.info(`${logPrefix} reconnected!`);
685 this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1));
686
687 break;
688 } catch (err) {
689 if (err.code === 'VSCODE_CONNECTION_ERROR') {
690 this._options.logService.error(`${logPrefix} A permanent error occurred in the reconnecting loop! Will give up now! Error:`);
691 this._options.logService.error(err);
692 this._onReconnectionPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false);
693 break;
694 }
695 if (Date.now() - loopStartTime >= graceTime) {
696 const graceSeconds = Math.round(graceTime / 1000);
697 this._options.logService.error(`${logPrefix} An error occurred while reconnecting, but it will be treated as a permanent error because the reconnection grace time (${graceSeconds}s) has expired! Will give up now! Error:`);
698 this._options.logService.error(err);
699 this._onReconnectionPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false);
700 break;
701 }
702 if (RemoteAuthorityResolverError.isTemporarilyNotAvailable(err)) {
703 this._options.logService.info(`${logPrefix} A temporarily not available error occurred while trying to reconnect, will try again...`);
704 this._options.logService.trace(err);
705 // try again!
706 continue;
707 }
708 if ((err.code === 'ETIMEDOUT' || err.code === 'ENETUNREACH' || err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET') && err.syscall === 'connect') {
709 this._options.logService.info(`${logPrefix} A network error occurred while trying to reconnect, will try again...`);
710 this._options.logService.trace(err);
711 // try again!
712 continue;
713 }
714 if (isCancellationError(err)) {
715 this._options.logService.info(`${logPrefix} A promise cancelation error occurred while trying to reconnect, will try again...`);
716 this._options.logService.trace(err);
717 // try again!
718 continue;
719 }
720 if (err instanceof RemoteAuthorityResolverError) {
721 this._options.logService.error(`${logPrefix} A RemoteAuthorityResolverError occurred while trying to reconnect. Will give up now! Error:`);
722 this._options.logService.error(err);
723 this._onReconnectionPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, RemoteAuthorityResolverError.isHandled(err));
724 break;
725 }
726 this._options.logService.error(`${logPrefix} An unknown error occurred while trying to reconnect, since this is an unknown case, it will be treated as a permanent error! Will give up now! Error:`);
727 this._options.logService.error(err);
728 this._onReconnectionPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false);
729 break;
730 }
731 } while (!this._isPermanentFailure && !this._isDisposed);
732 }
734 > private _onReconnectionPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void {
735 if (this._reconnectionFailureIsFatal) {
736 PersistentConnection.triggerPermanentFailure(millisSinceLastIncomingData, attempt, handled);
737 } else {
738 this._gotoPermanentFailure(millisSinceLastIncomingData, attempt, handled);
739 }
740 }
742 > private _gotoPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void {
743 this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent(this.reconnectionToken, millisSinceLastIncomingData, attempt, handled));
744 safeDisposeProtocolAndSocket(this.protocol);
745 }
747 > private _pauseSocketWriting(): void {
748 this.protocol.pauseSocketWriting();
749 }
751 > protected abstract _reconnect(options: ISimpleConnectionOptions, timeoutCancellationToken: CancellationToken): Promise<void>;
752 > }
753 >
754 > export class ManagementPersistentConnection extends PersistentConnection {
755 >
756 > public readonly client: Client<RemoteAgentConnectionContext>;
757 >
758 > constructor(options: IConnectionOptions, remoteAuthority: string, clientId: string, reconnectionToken: string, protocol: PersistentProtocol) {
759 super(ConnectionType.Management, options, reconnectionToken, protocol, /*reconnectionFailureIsFatal*/true);
760 this.client = this._register(new Client<RemoteAgentConnectionContext>(protocol, {
761 remoteAuthority: remoteAuthority,
762 clientId: clientId
763 }, options.ipcLogger));
764 }
766 > protected async _reconnect(options: ISimpleConnectionOptions, timeoutCancellationToken: CancellationToken): Promise<void> {
767 await doConnectRemoteAgentManagement(options, timeoutCancellationToken);
768 }
770 >
771 > export class ExtensionHostPersistentConnection extends PersistentConnection {
772 >
773 > private readonly _startArguments: IRemoteExtensionHostStartParams;
774 > public readonly debugPort: number | undefined;
775 >
776 > constructor(options: IConnectionOptions, startArguments: IRemoteExtensionHostStartParams, reconnectionToken: string, protocol: PersistentProtocol, debugPort: number | undefined) {
777 super(ConnectionType.ExtensionHost, options, reconnectionToken, protocol, /*reconnectionFailureIsFatal*/false);
778 this._startArguments = startArguments;
779 this.debugPort = debugPort;
780 }
782 > protected async _reconnect(options: ISimpleConnectionOptions, timeoutCancellationToken: CancellationToken): Promise<void> {
783 await doConnectRemoteAgentExtensionHost(options, this._startArguments, timeoutCancellationToken);
784 }
786 >
787 function safeDisposeProtocolAndSocket(protocol: PersistentProtocol): void {
788 try {
789 protocol.acceptDisconnect();
790 const socket = protocol.getSocket();
791 protocol.dispose();
792 socket.dispose();
793 } catch (err) {
794 onUnexpectedError(err);
795 }
796 }
798 function getErrorFromMessage(msg: any): Error | null {
799 if (msg && msg.type === 'error') {
800 const error = new Error(`Connection error: ${msg.reason}`);
801 // eslint-disable-next-line local/code-no-any-casts
802 (<any>error).code = 'VSCODE_CONNECTION_ERROR';
803 return error;
804 }
805 return null;
806 }
808 function sanitizeGraceTime(candidate: number, fallback: number): number {
809 if (typeof candidate !== 'number' || !isFinite(candidate) || candidate < 0) {
810 return fallback;
811 }
812 if (candidate > Number.MAX_SAFE_INTEGER) {
813 return Number.MAX_SAFE_INTEGER;
814 }
815 return Math.floor(candidate);
816 }
818 function stringRightPad(str: string, len: number): string {
819 while (str.length < len) {
820 str += ' ';
821 }
822 return str;
823 }
825 function _commonLogPrefix(connectionType: ConnectionType, reconnectionToken: string): string {
826 return `[remote-connection][${stringRightPad(connectionTypeToString(connectionType), 13)}][${reconnectionToken.substr(0, 5)}…]`;
827 }
829 function commonLogPrefix(connectionType: ConnectionType, reconnectionToken: string, isReconnect: boolean): string {
830 return `${_commonLogPrefix(connectionType, reconnectionToken)}[${isReconnect ? 'reconnect' : 'initial'}]`;
831 }
833 function connectLogPrefix(options: ISimpleConnectionOptions, connectionType: ConnectionType): string {
834 return `${commonLogPrefix(connectionType, options.reconnectionToken, !!options.reconnectionProtocol)}[${options.connectTo}]`;
835 }
837 function logElapsed(startTime: number): string {
838 return `${Date.now() - startTime} ms`;
839 }