Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -0,0 +1,221 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ISocketFactory, IConnectCallback } from 'vs/platform/remote/common/remoteAgentConnection';
import { ISocket } from 'vs/base/parts/ipc/common/ipc.net';
import { VSBuffer } from 'vs/base/common/buffer';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import * as dom from 'vs/base/browser/dom';
import { RunOnceScheduler } from 'vs/base/common/async';
import { RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver';
export interface IWebSocketFactory {
create(url: string): IWebSocket;
}
export interface IWebSocket {
readonly onData: Event<ArrayBuffer>;
readonly onOpen: Event<void>;
readonly onClose: Event<void>;
readonly onError: Event<any>;
send(data: ArrayBuffer | ArrayBufferView): void;
close(): void;
}
class BrowserWebSocket extends Disposable implements IWebSocket {
private readonly _onData = new Emitter<ArrayBuffer>();
public readonly onData = this._onData.event;
public readonly onOpen: Event<void>;
private readonly _onClose = this._register(new Emitter<void>());
public readonly onClose = this._onClose.event;
private readonly _onError = this._register(new Emitter<any>());
public readonly onError = this._onError.event;
private readonly _socket: WebSocket;
private readonly _fileReader: FileReader;
private readonly _queue: Blob[];
private _isReading: boolean;
private _isClosed: boolean;
private readonly _socketMessageListener: (ev: MessageEvent) => void;
constructor(socket: WebSocket) {
super();
this._socket = socket;
this._fileReader = new FileReader();
this._queue = [];
this._isReading = false;
this._isClosed = false;
this._fileReader.onload = (event) => {
this._isReading = false;
const buff = <ArrayBuffer>(<any>event.target).result;
this._onData.fire(buff);
if (this._queue.length > 0) {
enqueue(this._queue.shift()!);
}
};
const enqueue = (blob: Blob) => {
if (this._isReading) {
this._queue.push(blob);
return;
}
this._isReading = true;
this._fileReader.readAsArrayBuffer(blob);
};
this._socketMessageListener = (ev: MessageEvent) => {
enqueue(<Blob>ev.data);
};
this._socket.addEventListener('message', this._socketMessageListener);
this.onOpen = Event.fromDOMEventEmitter(this._socket, 'open');
// WebSockets emit error events that do not contain any real information
// Our only chance of getting to the root cause of an error is to
// listen to the close event which gives out some real information:
// - https://www.w3.org/TR/websockets/#closeevent
// - https://tools.ietf.org/html/rfc6455#section-11.7
//
// But the error event is emitted before the close event, so we therefore
// delay the error event processing in the hope of receiving a close event
// with more information
let pendingErrorEvent: any | null = null;
const sendPendingErrorNow = () => {
const err = pendingErrorEvent;
pendingErrorEvent = null;
this._onError.fire(err);
};
const errorRunner = this._register(new RunOnceScheduler(sendPendingErrorNow, 0));
const sendErrorSoon = (err: any) => {
errorRunner.cancel();
pendingErrorEvent = err;
errorRunner.schedule();
};
const sendErrorNow = (err: any) => {
errorRunner.cancel();
pendingErrorEvent = err;
sendPendingErrorNow();
};
this._register(dom.addDisposableListener(this._socket, 'close', (e: CloseEvent) => {
this._isClosed = true;
if (pendingErrorEvent) {
if (!window.navigator.onLine) {
// The browser is offline => this is a temporary error which might resolve itself
sendErrorNow(new RemoteAuthorityResolverError('Browser is offline', RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable, e));
} else {
// An error event is pending
// The browser appears to be online...
if (!e.wasClean) {
// Let's be optimistic and hope that perhaps the server could not be reached or something
sendErrorNow(new RemoteAuthorityResolverError(e.reason || `WebSocket close with status code ${e.code}`, RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable, e));
} else {
// this was a clean close => send existing error
errorRunner.cancel();
sendPendingErrorNow();
}
}
}
this._onClose.fire();
}));
this._register(dom.addDisposableListener(this._socket, 'error', sendErrorSoon));
}
send(data: ArrayBuffer | ArrayBufferView): void {
if (this._isClosed) {
// Refuse to write data to closed WebSocket...
return;
}
this._socket.send(data);
}
close(): void {
this._isClosed = true;
this._socket.close();
this._socket.removeEventListener('message', this._socketMessageListener);
this.dispose();
}
}
export const defaultWebSocketFactory = new class implements IWebSocketFactory {
create(url: string): IWebSocket {
return new BrowserWebSocket(new WebSocket(url));
}
};
class BrowserSocket implements ISocket {
public readonly socket: IWebSocket;
constructor(socket: IWebSocket) {
this.socket = socket;
}
public dispose(): void {
this.socket.close();
}
public onData(listener: (e: VSBuffer) => void): IDisposable {
return this.socket.onData((data) => listener(VSBuffer.wrap(new Uint8Array(data))));
}
public onClose(listener: () => void): IDisposable {
return this.socket.onClose(listener);
}
public onEnd(listener: () => void): IDisposable {
return Disposable.None;
}
public write(buffer: VSBuffer): void {
this.socket.send(buffer.buffer);
}
public end(): void {
this.socket.close();
}
public drain(): Promise<void> {
return Promise.resolve();
}
}
export class BrowserSocketFactory implements ISocketFactory {
private readonly _webSocketFactory: IWebSocketFactory;
constructor(webSocketFactory: IWebSocketFactory | null | undefined) {
this._webSocketFactory = webSocketFactory || defaultWebSocketFactory;
}
connect(host: string, port: number, query: string, callback: IConnectCallback): void {
const socket = this._webSocketFactory.create(`ws://${host}:${port}/?${query}&skipWebSocketFrames=false`);
const errorListener = socket.onError((err) => callback(err, undefined));
socket.onOpen(() => {
errorListener.dispose();
callback(undefined, new BrowserSocket(socket));
});
}
}

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ResolvedAuthority, IRemoteAuthorityResolverService, ResolverResult, IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { RemoteAuthorities } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
export class RemoteAuthorityResolverService extends Disposable implements IRemoteAuthorityResolverService {
declare readonly _serviceBrand: undefined;
private readonly _onDidChangeConnectionData = this._register(new Emitter<void>());
public readonly onDidChangeConnectionData = this._onDidChangeConnectionData.event;
private readonly _cache: Map<string, ResolverResult>;
private readonly _connectionToken: string | undefined;
private readonly _connectionTokens: Map<string, string>;
constructor(connectionToken: string | undefined, resourceUriProvider: ((uri: URI) => URI) | undefined) {
super();
this._cache = new Map<string, ResolverResult>();
this._connectionToken = connectionToken;
this._connectionTokens = new Map<string, string>();
if (resourceUriProvider) {
RemoteAuthorities.setDelegate(resourceUriProvider);
}
}
async resolveAuthority(authority: string): Promise<ResolverResult> {
if (!this._cache.has(authority)) {
const result = this._doResolveAuthority(authority);
RemoteAuthorities.set(authority, result.authority.host, result.authority.port);
this._cache.set(authority, result);
this._onDidChangeConnectionData.fire();
}
return this._cache.get(authority)!;
}
getConnectionData(authority: string): IRemoteConnectionData | null {
if (!this._cache.has(authority)) {
return null;
}
const resolverResult = this._cache.get(authority)!;
const connectionToken = this._connectionTokens.get(authority) || this._connectionToken;
return {
host: resolverResult.authority.host,
port: resolverResult.authority.port,
connectionToken: connectionToken
};
}
private _doResolveAuthority(authority: string): ResolverResult {
const connectionToken = this._connectionTokens.get(authority) || this._connectionToken;
if (authority.indexOf(':') >= 0) {
const pieces = authority.split(':');
return { authority: { authority, host: pieces[0], port: parseInt(pieces[1], 10), connectionToken } };
}
return { authority: { authority, host: authority, port: 80, connectionToken } };
}
_clearResolvedAuthority(authority: string): void {
}
_setResolvedAuthority(resolvedAuthority: ResolvedAuthority) {
}
_setResolvedAuthorityError(authority: string, err: any): void {
}
_setAuthorityConnectionToken(authority: string, connectionToken: string): void {
this._connectionTokens.set(authority, connectionToken);
RemoteAuthorities.setConnectionToken(authority, connectionToken);
this._onDidChangeConnectionData.fire();
}
}

View File

@@ -0,0 +1,584 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Client, PersistentProtocol, ISocket, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net';
import { generateUuid } from 'vs/base/common/uuid';
import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { Disposable } from 'vs/base/common/lifecycle';
import { VSBuffer } from 'vs/base/common/buffer';
import { Emitter } from 'vs/base/common/event';
import { RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors';
import { ISignService } from 'vs/platform/sign/common/sign';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { ILogService } from 'vs/platform/log/common/log';
import { IIPCLogger } from 'vs/base/parts/ipc/common/ipc';
const INITIAL_CONNECT_TIMEOUT = 120 * 1000 /* 120s */;
const RECONNECT_TIMEOUT = 30 * 1000 /* 30s */;
export const enum ConnectionType {
Management = 1,
ExtensionHost = 2,
Tunnel = 3,
}
function connectionTypeToString(connectionType: ConnectionType): string {
switch (connectionType) {
case ConnectionType.Management:
return 'Management';
case ConnectionType.ExtensionHost:
return 'ExtensionHost';
case ConnectionType.Tunnel:
return 'Tunnel';
}
}
export interface AuthRequest {
type: 'auth';
auth: string;
}
export interface SignRequest {
type: 'sign';
data: string;
}
export interface ConnectionTypeRequest {
type: 'connectionType';
commit?: string;
signedData?: string;
desiredConnectionType?: ConnectionType;
args?: any;
}
export interface ErrorMessage {
type: 'error';
reason: string;
}
export interface OKMessage {
type: 'ok';
}
export type HandshakeMessage = AuthRequest | SignRequest | ConnectionTypeRequest | ErrorMessage | OKMessage;
interface ISimpleConnectionOptions {
commit: string | undefined;
host: string;
port: number;
connectionToken: string | undefined;
reconnectionToken: string;
reconnectionProtocol: PersistentProtocol | null;
socketFactory: ISocketFactory;
signService: ISignService;
logService: ILogService;
}
export interface IConnectCallback {
(err: any | undefined, socket: ISocket | undefined): void;
}
export interface ISocketFactory {
connect(host: string, port: number, query: string, callback: IConnectCallback): void;
}
async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }> {
const logPrefix = connectLogPrefix(options, connectionType);
const { protocol, ownsProtocol } = await new Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }>((c, e) => {
options.logService.trace(`${logPrefix} 1/6. invoking socketFactory.connect().`);
options.socketFactory.connect(
options.host,
options.port,
`reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`,
(err: any, socket: ISocket | undefined) => {
if (err || !socket) {
options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`);
options.logService.error(err);
e(err);
return;
}
options.logService.trace(`${logPrefix} 2/6. socketFactory.connect() was successful.`);
if (options.reconnectionProtocol) {
options.reconnectionProtocol.beginAcceptReconnection(socket, null);
c({ protocol: options.reconnectionProtocol, ownsProtocol: false });
} else {
c({ protocol: new PersistentProtocol(socket, null), ownsProtocol: true });
}
}
);
});
return new Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }>((c, e) => {
const errorTimeoutToken = setTimeout(() => {
const error: any = new Error('handshake timeout');
error.code = 'ETIMEDOUT';
error.syscall = 'connect';
options.logService.error(`${logPrefix} the handshake took longer than 10 seconds. Error:`);
options.logService.error(error);
if (ownsProtocol) {
safeDisposeProtocolAndSocket(protocol);
}
e(error);
}, 10000);
const messageRegistration = protocol.onControlMessage(async raw => {
const msg = <HandshakeMessage>JSON.parse(raw.toString());
// Stop listening for further events
messageRegistration.dispose();
const error = getErrorFromMessage(msg);
if (error) {
options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`);
options.logService.error(error);
if (ownsProtocol) {
safeDisposeProtocolAndSocket(protocol);
}
return e(error);
}
if (msg.type === 'sign') {
options.logService.trace(`${logPrefix} 4/6. received SignRequest control message.`);
const signed = await options.signService.sign(msg.data);
const connTypeRequest: ConnectionTypeRequest = {
type: 'connectionType',
commit: options.commit,
signedData: signed,
desiredConnectionType: connectionType
};
if (args) {
connTypeRequest.args = args;
}
options.logService.trace(`${logPrefix} 5/6. sending ConnectionTypeRequest control message.`);
protocol.sendControl(VSBuffer.fromString(JSON.stringify(connTypeRequest)));
clearTimeout(errorTimeoutToken);
c({ protocol, ownsProtocol });
} else {
const error = new Error('handshake error');
options.logService.error(`${logPrefix} received unexpected control message. Error:`);
options.logService.error(error);
if (ownsProtocol) {
safeDisposeProtocolAndSocket(protocol);
}
e(error);
}
});
options.logService.trace(`${logPrefix} 3/6. sending AuthRequest control message.`);
const authRequest: AuthRequest = {
type: 'auth',
auth: options.connectionToken || '00000000000000000000'
};
protocol.sendControl(VSBuffer.fromString(JSON.stringify(authRequest)));
});
}
interface IManagementConnectionResult {
protocol: PersistentProtocol;
}
async function connectToRemoteExtensionHostAgentAndReadOneMessage(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; firstMessage: any }> {
const startTime = Date.now();
const logPrefix = connectLogPrefix(options, connectionType);
const { protocol, ownsProtocol } = await connectToRemoteExtensionHostAgent(options, connectionType, args);
return new Promise<{ protocol: PersistentProtocol; firstMessage: any }>((c, e) => {
const registration = protocol.onControlMessage(raw => {
registration.dispose();
const msg = JSON.parse(raw.toString());
const error = getErrorFromMessage(msg);
if (error) {
options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`);
options.logService.error(error);
if (ownsProtocol) {
safeDisposeProtocolAndSocket(protocol);
}
return e(error);
}
if (options.reconnectionProtocol) {
options.reconnectionProtocol.endAcceptReconnection();
}
options.logService.trace(`${logPrefix} 6/6. handshake finished, connection is up and running after ${logElapsed(startTime)}!`);
c({ protocol, firstMessage: msg });
});
});
}
async function doConnectRemoteAgentManagement(options: ISimpleConnectionOptions): Promise<IManagementConnectionResult> {
const { protocol } = await connectToRemoteExtensionHostAgentAndReadOneMessage(options, ConnectionType.Management, undefined);
return { protocol };
}
export interface IRemoteExtensionHostStartParams {
language: string;
debugId?: string;
break?: boolean;
port?: number | null;
env?: { [key: string]: string | null };
}
interface IExtensionHostConnectionResult {
protocol: PersistentProtocol;
debugPort?: number;
}
async function doConnectRemoteAgentExtensionHost(options: ISimpleConnectionOptions, startArguments: IRemoteExtensionHostStartParams): Promise<IExtensionHostConnectionResult> {
const { protocol, firstMessage } = await connectToRemoteExtensionHostAgentAndReadOneMessage(options, ConnectionType.ExtensionHost, startArguments);
const debugPort = firstMessage && firstMessage.debugPort;
return { protocol, debugPort };
}
export interface ITunnelConnectionStartParams {
port: number;
}
async function doConnectRemoteAgentTunnel(options: ISimpleConnectionOptions, startParams: ITunnelConnectionStartParams): Promise<PersistentProtocol> {
const startTime = Date.now();
const logPrefix = connectLogPrefix(options, ConnectionType.Tunnel);
const { protocol } = await connectToRemoteExtensionHostAgent(options, ConnectionType.Tunnel, startParams);
options.logService.trace(`${logPrefix} 6/6. handshake finished, connection is up and running after ${logElapsed(startTime)}!`);
return protocol;
}
export interface IConnectionOptions {
commit: string | undefined;
socketFactory: ISocketFactory;
addressProvider: IAddressProvider;
signService: ISignService;
logService: ILogService;
ipcLogger: IIPCLogger | null;
}
async function resolveConnectionOptions(options: IConnectionOptions, reconnectionToken: string, reconnectionProtocol: PersistentProtocol | null): Promise<ISimpleConnectionOptions> {
const { host, port, connectionToken } = await options.addressProvider.getAddress();
return {
commit: options.commit,
host: host,
port: port,
connectionToken: connectionToken,
reconnectionToken: reconnectionToken,
reconnectionProtocol: reconnectionProtocol,
socketFactory: options.socketFactory,
signService: options.signService,
logService: options.logService
};
}
export interface IAddress {
host: string;
port: number;
connectionToken: string | undefined;
}
export interface IAddressProvider {
getAddress(): Promise<IAddress>;
}
export async function connectRemoteAgentManagement(options: IConnectionOptions, remoteAuthority: string, clientId: string): Promise<ManagementPersistentConnection> {
try {
const reconnectionToken = generateUuid();
const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null);
const { protocol } = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentManagement(simpleOptions), INITIAL_CONNECT_TIMEOUT);
return new ManagementPersistentConnection(options, remoteAuthority, clientId, reconnectionToken, protocol);
} catch (err) {
options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`);
options.logService.error(err);
PersistentConnection.triggerPermanentFailure();
throw err;
}
}
export async function connectRemoteAgentExtensionHost(options: IConnectionOptions, startArguments: IRemoteExtensionHostStartParams): Promise<ExtensionHostPersistentConnection> {
try {
const reconnectionToken = generateUuid();
const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null);
const { protocol, debugPort } = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentExtensionHost(simpleOptions, startArguments), INITIAL_CONNECT_TIMEOUT);
return new ExtensionHostPersistentConnection(options, startArguments, reconnectionToken, protocol, debugPort);
} catch (err) {
options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`);
options.logService.error(err);
PersistentConnection.triggerPermanentFailure();
throw err;
}
}
export async function connectRemoteAgentTunnel(options: IConnectionOptions, tunnelRemotePort: number): Promise<PersistentProtocol> {
const simpleOptions = await resolveConnectionOptions(options, generateUuid(), null);
const protocol = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentTunnel(simpleOptions, { port: tunnelRemotePort }), INITIAL_CONNECT_TIMEOUT);
return protocol;
}
function sleep(seconds: number): CancelablePromise<void> {
return createCancelablePromise(token => {
return new Promise((resolve, reject) => {
const timeout = setTimeout(resolve, seconds * 1000);
token.onCancellationRequested(() => {
clearTimeout(timeout);
resolve();
});
});
});
}
export const enum PersistentConnectionEventType {
ConnectionLost,
ReconnectionWait,
ReconnectionRunning,
ReconnectionPermanentFailure,
ConnectionGain
}
export class ConnectionLostEvent {
public readonly type = PersistentConnectionEventType.ConnectionLost;
}
export class ReconnectionWaitEvent {
public readonly type = PersistentConnectionEventType.ReconnectionWait;
constructor(
public readonly durationSeconds: number,
private readonly cancellableTimer: CancelablePromise<void>
) { }
public skipWait(): void {
this.cancellableTimer.cancel();
}
}
export class ReconnectionRunningEvent {
public readonly type = PersistentConnectionEventType.ReconnectionRunning;
}
export class ConnectionGainEvent {
public readonly type = PersistentConnectionEventType.ConnectionGain;
}
export class ReconnectionPermanentFailureEvent {
public readonly type = PersistentConnectionEventType.ReconnectionPermanentFailure;
}
export type PersistentConnectionEvent = ConnectionGainEvent | ConnectionLostEvent | ReconnectionWaitEvent | ReconnectionRunningEvent | ReconnectionPermanentFailureEvent;
abstract class PersistentConnection extends Disposable {
public static triggerPermanentFailure(): void {
this._permanentFailure = true;
this._instances.forEach(instance => instance._gotoPermanentFailure());
}
private static _permanentFailure: boolean = false;
private static _instances: PersistentConnection[] = [];
private readonly _onDidStateChange = this._register(new Emitter<PersistentConnectionEvent>());
public readonly onDidStateChange = this._onDidStateChange.event;
protected readonly _options: IConnectionOptions;
public readonly reconnectionToken: string;
public readonly protocol: PersistentProtocol;
private _isReconnecting: boolean;
constructor(private readonly _connectionType: ConnectionType, options: IConnectionOptions, reconnectionToken: string, protocol: PersistentProtocol) {
super();
this._options = options;
this.reconnectionToken = reconnectionToken;
this.protocol = protocol;
this._isReconnecting = false;
this._onDidStateChange.fire(new ConnectionGainEvent());
this._register(protocol.onSocketClose(() => this._beginReconnecting()));
this._register(protocol.onSocketTimeout(() => this._beginReconnecting()));
PersistentConnection._instances.push(this);
if (PersistentConnection._permanentFailure) {
this._gotoPermanentFailure();
}
}
private async _beginReconnecting(): Promise<void> {
// Only have one reconnection loop active at a time.
if (this._isReconnecting) {
return;
}
try {
this._isReconnecting = true;
await this._runReconnectingLoop();
} finally {
this._isReconnecting = false;
}
}
private async _runReconnectingLoop(): Promise<void> {
if (PersistentConnection._permanentFailure) {
// no more attempts!
return;
}
const logPrefix = commonLogPrefix(this._connectionType, this.reconnectionToken, true);
this._options.logService.info(`${logPrefix} starting reconnecting loop. You can get more information with the trace log level.`);
this._onDidStateChange.fire(new ConnectionLostEvent());
const TIMES = [5, 5, 10, 10, 10, 10, 10, 30];
const disconnectStartTime = Date.now();
let attempt = -1;
do {
attempt++;
const waitTime = (attempt < TIMES.length ? TIMES[attempt] : TIMES[TIMES.length - 1]);
try {
const sleepPromise = sleep(waitTime);
this._onDidStateChange.fire(new ReconnectionWaitEvent(waitTime, sleepPromise));
this._options.logService.info(`${logPrefix} waiting for ${waitTime} seconds before reconnecting...`);
try {
await sleepPromise;
} catch { } // User canceled timer
if (PersistentConnection._permanentFailure) {
this._options.logService.error(`${logPrefix} permanent failure occurred while running the reconnecting loop.`);
break;
}
// connection was lost, let's try to re-establish it
this._onDidStateChange.fire(new ReconnectionRunningEvent());
this._options.logService.info(`${logPrefix} resolving connection...`);
const simpleOptions = await resolveConnectionOptions(this._options, this.reconnectionToken, this.protocol);
this._options.logService.info(`${logPrefix} connecting to ${simpleOptions.host}:${simpleOptions.port}...`);
await connectWithTimeLimit(simpleOptions.logService, this._reconnect(simpleOptions), RECONNECT_TIMEOUT);
this._options.logService.info(`${logPrefix} reconnected!`);
this._onDidStateChange.fire(new ConnectionGainEvent());
break;
} catch (err) {
if (err.code === 'VSCODE_CONNECTION_ERROR') {
this._options.logService.error(`${logPrefix} A permanent error occurred in the reconnecting loop! Will give up now! Error:`);
this._options.logService.error(err);
PersistentConnection.triggerPermanentFailure();
break;
}
if (Date.now() - disconnectStartTime > ProtocolConstants.ReconnectionGraceTime) {
this._options.logService.error(`${logPrefix} An error occurred while reconnecting, but it will be treated as a permanent error because the reconnection grace time has expired! Will give up now! Error:`);
this._options.logService.error(err);
PersistentConnection.triggerPermanentFailure();
break;
}
if (RemoteAuthorityResolverError.isTemporarilyNotAvailable(err)) {
this._options.logService.info(`${logPrefix} A temporarily not available error occurred while trying to reconnect, will try again...`);
this._options.logService.trace(err);
// try again!
continue;
}
if ((err.code === 'ETIMEDOUT' || err.code === 'ENETUNREACH' || err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET') && err.syscall === 'connect') {
this._options.logService.info(`${logPrefix} A network error occurred while trying to reconnect, will try again...`);
this._options.logService.trace(err);
// try again!
continue;
}
if (isPromiseCanceledError(err)) {
this._options.logService.info(`${logPrefix} A promise cancelation error occurred while trying to reconnect, will try again...`);
this._options.logService.trace(err);
// try again!
continue;
}
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:`);
this._options.logService.error(err);
PersistentConnection.triggerPermanentFailure();
break;
}
} while (!PersistentConnection._permanentFailure);
}
private _gotoPermanentFailure(): void {
this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent());
safeDisposeProtocolAndSocket(this.protocol);
}
protected abstract _reconnect(options: ISimpleConnectionOptions): Promise<void>;
}
export class ManagementPersistentConnection extends PersistentConnection {
public readonly client: Client<RemoteAgentConnectionContext>;
constructor(options: IConnectionOptions, remoteAuthority: string, clientId: string, reconnectionToken: string, protocol: PersistentProtocol) {
super(ConnectionType.Management, options, reconnectionToken, protocol);
this.client = this._register(new Client<RemoteAgentConnectionContext>(protocol, {
remoteAuthority: remoteAuthority,
clientId: clientId
}, options.ipcLogger));
}
protected async _reconnect(options: ISimpleConnectionOptions): Promise<void> {
await doConnectRemoteAgentManagement(options);
}
}
export class ExtensionHostPersistentConnection extends PersistentConnection {
private readonly _startArguments: IRemoteExtensionHostStartParams;
public readonly debugPort: number | undefined;
constructor(options: IConnectionOptions, startArguments: IRemoteExtensionHostStartParams, reconnectionToken: string, protocol: PersistentProtocol, debugPort: number | undefined) {
super(ConnectionType.ExtensionHost, options, reconnectionToken, protocol);
this._startArguments = startArguments;
this.debugPort = debugPort;
}
protected async _reconnect(options: ISimpleConnectionOptions): Promise<void> {
await doConnectRemoteAgentExtensionHost(options, this._startArguments);
}
}
function connectWithTimeLimit<T>(logService: ILogService, p: Promise<T>, timeLimit: number): Promise<T> {
return new Promise<T>((resolve, reject) => {
let timeout = setTimeout(() => {
const err: any = new Error('Time limit reached');
err.code = 'ETIMEDOUT';
err.syscall = 'connect';
logService.error(`[remote-connection] The time limit has been reached for a connection. Error:`);
logService.error(err);
reject(err);
}, timeLimit);
p.then((value) => {
clearTimeout(timeout);
resolve(value);
}, (err) => {
clearTimeout(timeout);
reject(err);
});
});
}
function safeDisposeProtocolAndSocket(protocol: PersistentProtocol): void {
try {
protocol.acceptDisconnect();
const socket = protocol.getSocket();
protocol.dispose();
socket.dispose();
} catch (err) {
onUnexpectedError(err);
}
}
function getErrorFromMessage(msg: any): Error | null {
if (msg && msg.type === 'error') {
const error = new Error(`Connection error: ${msg.reason}`);
(<any>error).code = 'VSCODE_CONNECTION_ERROR';
return error;
}
return null;
}
function stringRightPad(str: string, len: number): string {
while (str.length < len) {
str += ' ';
}
return str;
}
function commonLogPrefix(connectionType: ConnectionType, reconnectionToken: string, isReconnect: boolean): string {
return `[remote-connection][${stringRightPad(connectionTypeToString(connectionType), 13)}][${reconnectionToken.substr(0, 5)}…][${isReconnect ? 'reconnect' : 'initial'}]`;
}
function connectLogPrefix(options: ISimpleConnectionOptions, connectionType: ConnectionType): string {
return `${commonLogPrefix(connectionType, options.reconnectionToken, !!options.reconnectionProtocol)}[${options.host}:${options.port}]`;
}
function logElapsed(startTime: number): string {
return `${Date.now() - startTime} ms`;
}

View File

@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { OperatingSystem } from 'vs/base/common/platform';
export interface IRemoteAgentEnvironment {
pid: number;
connectionToken: string;
appRoot: URI;
settingsPath: URI;
logsPath: URI;
extensionsPath: URI;
extensionHostLogsPath: URI;
globalStorageHome: URI;
workspaceStorageHome: URI;
userHome: URI;
os: OperatingSystem;
}
export interface RemoteAgentConnectionContext {
remoteAuthority: string;
clientId: string;
}

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
export const IRemoteAuthorityResolverService = createDecorator<IRemoteAuthorityResolverService>('remoteAuthorityResolverService');
export interface ResolvedAuthority {
readonly authority: string;
readonly host: string;
readonly port: number;
readonly connectionToken: string | undefined;
}
export interface ResolvedOptions {
readonly extensionHostEnv?: { [key: string]: string | null };
}
export interface TunnelDescription {
remoteAddress: { port: number, host: string };
localAddress: { port: number, host: string } | string;
}
export interface TunnelInformation {
environmentTunnels?: TunnelDescription[];
}
export interface ResolverResult {
authority: ResolvedAuthority;
options?: ResolvedOptions;
tunnelInformation?: TunnelInformation;
}
export interface IRemoteConnectionData {
host: string;
port: number;
connectionToken: string | undefined;
}
export enum RemoteAuthorityResolverErrorCode {
Unknown = 'Unknown',
NotAvailable = 'NotAvailable',
TemporarilyNotAvailable = 'TemporarilyNotAvailable',
NoResolverFound = 'NoResolverFound'
}
export class RemoteAuthorityResolverError extends Error {
public static isTemporarilyNotAvailable(err: any): boolean {
return (err instanceof RemoteAuthorityResolverError) && err._code === RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable;
}
public static isNoResolverFound(err: any): err is RemoteAuthorityResolverError {
return (err instanceof RemoteAuthorityResolverError) && err._code === RemoteAuthorityResolverErrorCode.NoResolverFound;
}
public static isHandled(err: any): boolean {
return (err instanceof RemoteAuthorityResolverError) && err.isHandled;
}
public readonly _message: string | undefined;
public readonly _code: RemoteAuthorityResolverErrorCode;
public readonly _detail: any;
public isHandled: boolean;
constructor(message?: string, code: RemoteAuthorityResolverErrorCode = RemoteAuthorityResolverErrorCode.Unknown, detail?: any) {
super(message);
this._message = message;
this._code = code;
this._detail = detail;
this.isHandled = (code === RemoteAuthorityResolverErrorCode.NotAvailable) && detail === true;
// workaround when extending builtin objects and when compiling to ES5, see:
// https://github.com/microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
if (typeof (<any>Object).setPrototypeOf === 'function') {
(<any>Object).setPrototypeOf(this, RemoteAuthorityResolverError.prototype);
}
}
}
export interface IRemoteAuthorityResolverService {
readonly _serviceBrand: undefined;
readonly onDidChangeConnectionData: Event<void>;
resolveAuthority(authority: string): Promise<ResolverResult>;
getConnectionData(authority: string): IRemoteConnectionData | null;
_clearResolvedAuthority(authority: string): void;
_setResolvedAuthority(resolvedAuthority: ResolvedAuthority, resolvedOptions?: ResolvedOptions): void;
_setResolvedAuthorityError(authority: string, err: any): void;
_setAuthorityConnectionToken(authority: string, connectionToken: string): void;
}

View File

@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
export function getRemoteAuthority(uri: URI): string | undefined {
return uri.scheme === Schemas.vscodeRemote ? uri.authority : undefined;
}
export function getRemoteName(authority: string): string;
export function getRemoteName(authority: undefined): undefined;
export function getRemoteName(authority: string | undefined): string | undefined;
export function getRemoteName(authority: string | undefined): string | undefined {
if (!authority) {
return undefined;
}
const pos = authority.indexOf('+');
if (pos < 0) {
// funky? bad authority?
return authority;
}
return authority.substr(0, pos);
}

View File

@@ -0,0 +1,220 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection';
export const ITunnelService = createDecorator<ITunnelService>('tunnelService');
export interface RemoteTunnel {
readonly tunnelRemotePort: number;
readonly tunnelRemoteHost: string;
readonly tunnelLocalPort?: number;
readonly localAddress: string;
dispose(silent?: boolean): void;
}
export interface TunnelOptions {
remoteAddress: { port: number, host: string };
localAddressPort?: number;
label?: string;
}
export interface ITunnelProvider {
forwardPort(tunnelOptions: TunnelOptions): Promise<RemoteTunnel> | undefined;
}
export interface ITunnelService {
readonly _serviceBrand: undefined;
readonly tunnels: Promise<readonly RemoteTunnel[]>;
readonly onTunnelOpened: Event<RemoteTunnel>;
readonly onTunnelClosed: Event<{ host: string, port: number }>;
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
closeTunnel(remoteHost: string, remotePort: number): Promise<void>;
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable;
}
export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number } | undefined {
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
return undefined;
}
const localhostMatch = /^(localhost|127\.0\.0\.1|0\.0\.0\.0):(\d+)$/.exec(uri.authority);
if (!localhostMatch) {
return undefined;
}
return {
address: localhostMatch[1],
port: +localhostMatch[2],
};
}
export function isLocalhost(host: string): boolean {
return host === 'localhost' || host === '127.0.0.1';
}
function getOtherLocalhost(host: string): string | undefined {
return (host === 'localhost') ? '127.0.0.1' : ((host === '127.0.0.1') ? 'localhost' : undefined);
}
export abstract class AbstractTunnelService implements ITunnelService {
declare readonly _serviceBrand: undefined;
private _onTunnelOpened: Emitter<RemoteTunnel> = new Emitter();
public onTunnelOpened: Event<RemoteTunnel> = this._onTunnelOpened.event;
private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter();
public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event;
protected readonly _tunnels = new Map</*host*/ string, Map</* port */ number, { refcount: number, readonly value: Promise<RemoteTunnel> }>>();
protected _tunnelProvider: ITunnelProvider | undefined;
public constructor(
@ILogService protected readonly logService: ILogService
) { }
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable {
if (!provider) {
return {
dispose: () => { }
};
}
this._tunnelProvider = provider;
return {
dispose: () => {
this._tunnelProvider = undefined;
}
};
}
public get tunnels(): Promise<readonly RemoteTunnel[]> {
const promises: Promise<RemoteTunnel>[] = [];
Array.from(this._tunnels.values()).forEach(portMap => Array.from(portMap.values()).forEach(x => promises.push(x.value)));
return Promise.all(promises);
}
dispose(): void {
for (const portMap of this._tunnels.values()) {
for (const { value } of portMap.values()) {
value.then(tunnel => tunnel.dispose());
}
portMap.clear();
}
this._tunnels.clear();
}
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort: number): Promise<RemoteTunnel> | undefined {
if (!addressProvider) {
return undefined;
}
if (!remoteHost) {
remoteHost = 'localhost';
}
const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort);
if (!resolvedTunnel) {
return resolvedTunnel;
}
return resolvedTunnel.then(tunnel => {
const newTunnel = this.makeTunnel(tunnel);
if (tunnel.tunnelRemoteHost !== remoteHost || tunnel.tunnelRemotePort !== remotePort) {
this.logService.warn('Created tunnel does not match requirements of requested tunnel. Host or port mismatch.');
}
this._onTunnelOpened.fire(newTunnel);
return newTunnel;
});
}
private makeTunnel(tunnel: RemoteTunnel): RemoteTunnel {
return {
tunnelRemotePort: tunnel.tunnelRemotePort,
tunnelRemoteHost: tunnel.tunnelRemoteHost,
tunnelLocalPort: tunnel.tunnelLocalPort,
localAddress: tunnel.localAddress,
dispose: () => {
const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost);
if (existingHost) {
const existing = existingHost.get(tunnel.tunnelRemotePort);
if (existing) {
existing.refcount--;
this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing);
}
}
}
};
}
private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise<RemoteTunnel> }): Promise<void> {
if (tunnel.refcount <= 0) {
const disposePromise: Promise<void> = tunnel.value.then(tunnel => {
tunnel.dispose(true);
this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort });
});
if (this._tunnels.has(remoteHost)) {
this._tunnels.get(remoteHost)!.delete(remotePort);
}
return disposePromise;
}
}
async closeTunnel(remoteHost: string, remotePort: number): Promise<void> {
const portMap = this._tunnels.get(remoteHost);
if (portMap && portMap.has(remotePort)) {
const value = portMap.get(remotePort)!;
value.refcount = 0;
await this.tryDisposeTunnel(remoteHost, remotePort, value);
}
}
protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise<RemoteTunnel>) {
if (!this._tunnels.has(remoteHost)) {
this._tunnels.set(remoteHost, new Map());
}
this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel });
}
protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise<RemoteTunnel> } | undefined {
const otherLocalhost = getOtherLocalhost(remoteHost);
let portMap: Map<number, { refcount: number, readonly value: Promise<RemoteTunnel> }> | undefined;
if (otherLocalhost) {
const firstMap = this._tunnels.get(remoteHost);
const secondMap = this._tunnels.get(otherLocalhost);
if (firstMap && secondMap) {
portMap = new Map([...Array.from(firstMap.entries()), ...Array.from(secondMap.entries())]);
} else {
portMap = firstMap ?? secondMap;
}
} else {
portMap = this._tunnels.get(remoteHost);
}
return portMap ? portMap.get(remotePort) : undefined;
}
protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
}
export class TunnelService extends AbstractTunnelService {
protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise<RemoteTunnel> | undefined {
const existing = this.getTunnelFromMap(remoteHost, remotePort);
if (existing) {
++existing.refcount;
return existing.value;
}
if (this._tunnelProvider) {
const tunnel = this._tunnelProvider.forwardPort({ remoteAddress: { host: remoteHost, port: remotePort } });
if (tunnel) {
this.addTunnelToMap(remoteHost, remotePort, tunnel);
}
return tunnel;
}
return undefined;
}
}

View File

@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//
import { ResolvedAuthority, IRemoteAuthorityResolverService, ResolverResult, ResolvedOptions, IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
import * as errors from 'vs/base/common/errors';
import { RemoteAuthorities } from 'vs/base/common/network';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
class PendingResolveAuthorityRequest {
public value: ResolverResult | null;
constructor(
private readonly _resolve: (value: ResolverResult) => void,
private readonly _reject: (err: any) => void,
public readonly promise: Promise<ResolverResult>,
) {
this.value = null;
}
resolve(value: ResolverResult): void {
this.value = value;
this._resolve(this.value);
}
reject(err: any): void {
this._reject(err);
}
}
export class RemoteAuthorityResolverService extends Disposable implements IRemoteAuthorityResolverService {
declare readonly _serviceBrand: undefined;
private readonly _onDidChangeConnectionData = this._register(new Emitter<void>());
public readonly onDidChangeConnectionData = this._onDidChangeConnectionData.event;
private readonly _resolveAuthorityRequests: Map<string, PendingResolveAuthorityRequest>;
private readonly _connectionTokens: Map<string, string>;
constructor() {
super();
this._resolveAuthorityRequests = new Map<string, PendingResolveAuthorityRequest>();
this._connectionTokens = new Map<string, string>();
}
resolveAuthority(authority: string): Promise<ResolverResult> {
if (!this._resolveAuthorityRequests.has(authority)) {
let resolve: (value: ResolverResult) => void;
let reject: (err: any) => void;
const promise = new Promise<ResolverResult>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
this._resolveAuthorityRequests.set(authority, new PendingResolveAuthorityRequest(resolve!, reject!, promise));
}
return this._resolveAuthorityRequests.get(authority)!.promise;
}
getConnectionData(authority: string): IRemoteConnectionData | null {
if (!this._resolveAuthorityRequests.has(authority)) {
return null;
}
const request = this._resolveAuthorityRequests.get(authority)!;
if (!request.value) {
return null;
}
const connectionToken = this._connectionTokens.get(authority);
return {
host: request.value.authority.host,
port: request.value.authority.port,
connectionToken: connectionToken
};
}
_clearResolvedAuthority(authority: string): void {
if (this._resolveAuthorityRequests.has(authority)) {
this._resolveAuthorityRequests.get(authority)!.reject(errors.canceled());
this._resolveAuthorityRequests.delete(authority);
}
}
_setResolvedAuthority(resolvedAuthority: ResolvedAuthority, options?: ResolvedOptions): void {
if (this._resolveAuthorityRequests.has(resolvedAuthority.authority)) {
const request = this._resolveAuthorityRequests.get(resolvedAuthority.authority)!;
RemoteAuthorities.set(resolvedAuthority.authority, resolvedAuthority.host, resolvedAuthority.port);
if (resolvedAuthority.connectionToken) {
RemoteAuthorities.setConnectionToken(resolvedAuthority.authority, resolvedAuthority.connectionToken);
}
request.resolve({ authority: resolvedAuthority, options });
this._onDidChangeConnectionData.fire();
}
}
_setResolvedAuthorityError(authority: string, err: any): void {
if (this._resolveAuthorityRequests.has(authority)) {
const request = this._resolveAuthorityRequests.get(authority)!;
request.reject(err);
}
}
_setAuthorityConnectionToken(authority: string, connectionToken: string): void {
this._connectionTokens.set(authority, connectionToken);
RemoteAuthorities.setConnectionToken(authority, connectionToken);
this._onDidChangeConnectionData.fire();
}
}

View File

@@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as net from 'net';
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { ISocketFactory, IConnectCallback } from 'vs/platform/remote/common/remoteAgentConnection';
export const nodeSocketFactory = new class implements ISocketFactory {
connect(host: string, port: number, query: string, callback: IConnectCallback): void {
const errorListener = (err: any) => callback(err, undefined);
const socket = net.createConnection({ host: host, port: port }, () => {
socket.removeListener('error', errorListener);
// https://tools.ietf.org/html/rfc6455#section-4
const buffer = Buffer.alloc(16);
for (let i = 0; i < 16; i++) {
buffer[i] = Math.round(Math.random() * 256);
}
const nonce = buffer.toString('base64');
let headers = [
`GET ws://${host}:${port}/?${query}&skipWebSocketFrames=true HTTP/1.1`,
`Connection: Upgrade`,
`Upgrade: websocket`,
`Sec-WebSocket-Key: ${nonce}`
];
socket.write(headers.join('\r\n') + '\r\n\r\n');
const onData = (data: Buffer) => {
const strData = data.toString();
if (strData.indexOf('\r\n\r\n') >= 0) {
// headers received OK
socket.off('data', onData);
callback(undefined, new NodeSocket(socket));
}
};
socket.on('data', onData);
});
socket.once('error', errorListener);
}
};

View File

@@ -0,0 +1,169 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as net from 'net';
import { Barrier } from 'vs/base/common/async';
import { Disposable } from 'vs/base/common/lifecycle';
import { findFreePortFaster } from 'vs/base/node/ports';
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection';
import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory';
import { ISignService } from 'vs/platform/sign/common/sign';
async function createRemoteTunnel(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise<RemoteTunnel> {
const tunnel = new NodeRemoteTunnel(options, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort);
return tunnel.waitForReady();
}
class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
public readonly tunnelRemotePort: number;
public tunnelLocalPort!: number;
public tunnelRemoteHost: string;
public localAddress!: string;
private readonly _options: IConnectionOptions;
private readonly _server: net.Server;
private readonly _barrier: Barrier;
private readonly _listeningListener: () => void;
private readonly _connectionListener: (socket: net.Socket) => void;
private readonly _errorListener: () => void;
private readonly _socketsDispose: Map<string, () => void> = new Map();
constructor(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, private readonly suggestedLocalPort?: number) {
super();
this._options = options;
this._server = net.createServer();
this._barrier = new Barrier();
this._listeningListener = () => this._barrier.open();
this._server.on('listening', this._listeningListener);
this._connectionListener = (socket) => this._onConnection(socket);
this._server.on('connection', this._connectionListener);
// If there is no error listener and there is an error it will crash the whole window
this._errorListener = () => { };
this._server.on('error', this._errorListener);
this.tunnelRemotePort = tunnelRemotePort;
this.tunnelRemoteHost = tunnelRemoteHost;
}
public dispose(): void {
super.dispose();
this._server.removeListener('listening', this._listeningListener);
this._server.removeListener('connection', this._connectionListener);
this._server.removeListener('error', this._errorListener);
this._server.close();
const disposers = Array.from(this._socketsDispose.values());
disposers.forEach(disposer => {
disposer();
});
}
public async waitForReady(): Promise<this> {
// try to get the same port number as the remote port number...
let localPort = await findFreePortFaster(this.suggestedLocalPort ?? this.tunnelRemotePort, 2, 1000);
// if that fails, the method above returns 0, which works out fine below...
let address: string | net.AddressInfo | null = null;
address = (<net.AddressInfo>this._server.listen(localPort).address());
// It is possible for findFreePortFaster to return a port that there is already a server listening on. This causes the previous listen call to error out.
if (!address) {
localPort = 0;
address = (<net.AddressInfo>this._server.listen(localPort).address());
}
this.tunnelLocalPort = address.port;
await this._barrier.wait();
this.localAddress = `${this.tunnelRemoteHost === '127.0.0.1' ? '127.0.0.1' : 'localhost'}:${address.port}`;
return this;
}
private async _onConnection(localSocket: net.Socket): Promise<void> {
// pause reading on the socket until we have a chance to forward its data
localSocket.pause();
const protocol = await connectRemoteAgentTunnel(this._options, this.tunnelRemotePort);
const remoteSocket = (<NodeSocket>protocol.getSocket()).socket;
const dataChunk = protocol.readEntireBuffer();
protocol.dispose();
if (dataChunk.byteLength > 0) {
localSocket.write(dataChunk.buffer);
}
localSocket.on('end', () => {
this._socketsDispose.delete(localSocket.localAddress);
remoteSocket.end();
});
localSocket.on('close', () => remoteSocket.end());
localSocket.on('error', () => {
this._socketsDispose.delete(localSocket.localAddress);
remoteSocket.destroy();
});
remoteSocket.on('end', () => localSocket.end());
remoteSocket.on('close', () => localSocket.end());
remoteSocket.on('error', () => {
localSocket.destroy();
});
localSocket.pipe(remoteSocket);
remoteSocket.pipe(localSocket);
this._socketsDispose.set(localSocket.localAddress, () => {
// Need to end instead of unpipe, otherwise whatever is connected locally could end up "stuck" with whatever state it had until manually exited.
localSocket.end();
remoteSocket.end();
});
}
}
export class TunnelService extends AbstractTunnelService {
public constructor(
@ILogService logService: ILogService,
@ISignService private readonly signService: ISignService,
@IProductService private readonly productService: IProductService
) {
super(logService);
}
protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined {
const existing = this.getTunnelFromMap(remoteHost, remotePort);
if (existing) {
++existing.refcount;
return existing.value;
}
if (this._tunnelProvider) {
const tunnel = this._tunnelProvider.forwardPort({ remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort });
if (tunnel) {
this.addTunnelToMap(remoteHost, remotePort, tunnel);
}
return tunnel;
} else {
const options: IConnectionOptions = {
commit: this.productService.commit,
socketFactory: nodeSocketFactory,
addressProvider,
signService: this.signService,
logService: this.logService,
ipcLogger: null
};
const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort);
this.addTunnelToMap(remoteHost, remotePort, tunnel);
return tunnel;
}
}
}