mirror of
https://github.com/coder/code-server.git
synced 2026-05-13 07:47:26 +02:00
chore(vscode): update to 1.53.2
These conflicts will be resolved in the following commits. We do it this way so that PR review is possible.
This commit is contained in:
@@ -8,7 +8,7 @@ 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 { Emitter, Event } 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';
|
||||
@@ -86,6 +86,7 @@ export interface ISocketFactory {
|
||||
connect(host: string, port: number, query: string, callback: IConnectCallback): void;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
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) => {
|
||||
@@ -101,81 +102,126 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio
|
||||
e(err);
|
||||
return;
|
||||
}
|
||||
=======
|
||||
async function readOneControlMessage<T>(protocol: PersistentProtocol): Promise<T> {
|
||||
const raw = await Event.toPromise(protocol.onControlMessage);
|
||||
const msg = JSON.parse(raw.toString());
|
||||
const error = getErrorFromMessage(msg);
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
>>>>>>> 89b6e0164fa770333755b11504e19a4232b1a2d4
|
||||
|
||||
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 });
|
||||
}
|
||||
function waitWithTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timeoutToken = setTimeout(() => {
|
||||
const error: any = new Error('Timeout');
|
||||
error.code = 'ETIMEDOUT';
|
||||
error.syscall = 'connect';
|
||||
reject(error);
|
||||
}, timeout);
|
||||
|
||||
promise.then(
|
||||
(result) => {
|
||||
clearTimeout(timeoutToken);
|
||||
resolve(result);
|
||||
},
|
||||
(error) => {
|
||||
clearTimeout(timeoutToken);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }>((c, e) => {
|
||||
function createSocket(socketFactory: ISocketFactory, host: string, port: number, query: string): Promise<ISocket> {
|
||||
return new Promise<ISocket>((resolve, reject) => {
|
||||
socketFactory.connect(host, port, query, (err: any, socket: ISocket | undefined) => {
|
||||
if (err || !socket) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(socket);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const errorTimeoutToken = setTimeout(() => {
|
||||
const error: any = new Error('handshake timeout');
|
||||
error.code = 'ETIMEDOUT';
|
||||
error.syscall = 'connect';
|
||||
async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean; }> {
|
||||
const logPrefix = connectLogPrefix(options, connectionType);
|
||||
|
||||
options.logService.trace(`${logPrefix} 1/6. invoking socketFactory.connect().`);
|
||||
|
||||
let socket: ISocket;
|
||||
try {
|
||||
socket = await createSocket(options.socketFactory, options.host, options.port, `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`);
|
||||
} catch (error) {
|
||||
options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`);
|
||||
options.logService.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
options.logService.trace(`${logPrefix} 2/6. socketFactory.connect() was successful.`);
|
||||
|
||||
let protocol: PersistentProtocol;
|
||||
let ownsProtocol: boolean;
|
||||
if (options.reconnectionProtocol) {
|
||||
options.reconnectionProtocol.beginAcceptReconnection(socket, null);
|
||||
protocol = options.reconnectionProtocol;
|
||||
ownsProtocol = false;
|
||||
} else {
|
||||
protocol = new PersistentProtocol(socket, null);
|
||||
ownsProtocol = true;
|
||||
}
|
||||
|
||||
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)));
|
||||
|
||||
try {
|
||||
const msg = await waitWithTimeout(readOneControlMessage<HandshakeMessage>(protocol), 10000);
|
||||
|
||||
if (msg.type !== 'sign' || typeof msg.data !== 'string') {
|
||||
const error: any = new Error('Unexpected handshake message');
|
||||
error.code = 'VSCODE_CONNECTION_ERROR';
|
||||
throw error;
|
||||
}
|
||||
|
||||
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)));
|
||||
|
||||
return { protocol, ownsProtocol };
|
||||
|
||||
} catch (error) {
|
||||
if (error && error.code === 'ETIMEDOUT') {
|
||||
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)));
|
||||
});
|
||||
}
|
||||
if (error && error.code === 'VSCODE_CONNECTION_ERROR') {
|
||||
options.logService.error(`${logPrefix} received error control message when negotiating connection. Error:`);
|
||||
options.logService.error(error);
|
||||
}
|
||||
if (ownsProtocol) {
|
||||
safeDisposeProtocolAndSocket(protocol);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
interface IManagementConnectionResult {
|
||||
@@ -287,7 +333,7 @@ export async function connectRemoteAgentManagement(options: IConnectionOptions,
|
||||
} 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();
|
||||
PersistentConnection.triggerPermanentFailure(0, 0, RemoteAuthorityResolverError.isHandled(err));
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -301,7 +347,7 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption
|
||||
} 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();
|
||||
PersistentConnection.triggerPermanentFailure(0, 0, RemoteAuthorityResolverError.isHandled(err));
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -333,10 +379,16 @@ export const enum PersistentConnectionEventType {
|
||||
}
|
||||
export class ConnectionLostEvent {
|
||||
public readonly type = PersistentConnectionEventType.ConnectionLost;
|
||||
constructor(
|
||||
public readonly reconnectionToken: string,
|
||||
public readonly millisSinceLastIncomingData: number
|
||||
) { }
|
||||
}
|
||||
export class ReconnectionWaitEvent {
|
||||
public readonly type = PersistentConnectionEventType.ReconnectionWait;
|
||||
constructor(
|
||||
public readonly reconnectionToken: string,
|
||||
public readonly millisSinceLastIncomingData: number,
|
||||
public readonly durationSeconds: number,
|
||||
private readonly cancellableTimer: CancelablePromise<void>,
|
||||
public readonly connectionAttempt: number
|
||||
@@ -348,22 +400,44 @@ export class ReconnectionWaitEvent {
|
||||
}
|
||||
export class ReconnectionRunningEvent {
|
||||
public readonly type = PersistentConnectionEventType.ReconnectionRunning;
|
||||
constructor(
|
||||
public readonly reconnectionToken: string,
|
||||
public readonly millisSinceLastIncomingData: number,
|
||||
public readonly attempt: number
|
||||
) { }
|
||||
}
|
||||
export class ConnectionGainEvent {
|
||||
public readonly type = PersistentConnectionEventType.ConnectionGain;
|
||||
constructor(
|
||||
public readonly reconnectionToken: string,
|
||||
public readonly millisSinceLastIncomingData: number,
|
||||
public readonly attempt: number
|
||||
) { }
|
||||
}
|
||||
export class ReconnectionPermanentFailureEvent {
|
||||
public readonly type = PersistentConnectionEventType.ReconnectionPermanentFailure;
|
||||
constructor(
|
||||
public readonly reconnectionToken: string,
|
||||
public readonly millisSinceLastIncomingData: number,
|
||||
public readonly attempt: number,
|
||||
public readonly handled: boolean
|
||||
) { }
|
||||
}
|
||||
export type PersistentConnectionEvent = ConnectionGainEvent | ConnectionLostEvent | ReconnectionWaitEvent | ReconnectionRunningEvent | ReconnectionPermanentFailureEvent;
|
||||
|
||||
abstract class PersistentConnection extends Disposable {
|
||||
|
||||
public static triggerPermanentFailure(): void {
|
||||
public static triggerPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void {
|
||||
this._permanentFailure = true;
|
||||
this._instances.forEach(instance => instance._gotoPermanentFailure());
|
||||
this._permanentFailureMillisSinceLastIncomingData = millisSinceLastIncomingData;
|
||||
this._permanentFailureAttempt = attempt;
|
||||
this._permanentFailureHandled = handled;
|
||||
this._instances.forEach(instance => instance._gotoPermanentFailure(this._permanentFailureMillisSinceLastIncomingData, this._permanentFailureAttempt, this._permanentFailureHandled));
|
||||
}
|
||||
private static _permanentFailure: boolean = false;
|
||||
private static _permanentFailureMillisSinceLastIncomingData: number = 0;
|
||||
private static _permanentFailureAttempt: number = 0;
|
||||
private static _permanentFailureHandled: boolean = false;
|
||||
private static _instances: PersistentConnection[] = [];
|
||||
|
||||
private readonly _onDidStateChange = this._register(new Emitter<PersistentConnectionEvent>());
|
||||
@@ -382,7 +456,7 @@ abstract class PersistentConnection extends Disposable {
|
||||
this.protocol = protocol;
|
||||
this._isReconnecting = false;
|
||||
|
||||
this._onDidStateChange.fire(new ConnectionGainEvent());
|
||||
this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, 0, 0));
|
||||
|
||||
this._register(protocol.onSocketClose(() => this._beginReconnecting()));
|
||||
this._register(protocol.onSocketTimeout(() => this._beginReconnecting()));
|
||||
@@ -390,7 +464,7 @@ abstract class PersistentConnection extends Disposable {
|
||||
PersistentConnection._instances.push(this);
|
||||
|
||||
if (PersistentConnection._permanentFailure) {
|
||||
this._gotoPermanentFailure();
|
||||
this._gotoPermanentFailure(PersistentConnection._permanentFailureMillisSinceLastIncomingData, PersistentConnection._permanentFailureAttempt, PersistentConnection._permanentFailureHandled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,14 +488,15 @@ abstract class PersistentConnection extends Disposable {
|
||||
}
|
||||
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];
|
||||
this._onDidStateChange.fire(new ConnectionLostEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData()));
|
||||
const TIMES = [0, 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 {
|
||||
<<<<<<< HEAD
|
||||
const sleepPromise = sleep(waitTime);
|
||||
this._onDidStateChange.fire(new ReconnectionWaitEvent(waitTime, sleepPromise, attempt+1));
|
||||
|
||||
@@ -429,6 +504,17 @@ abstract class PersistentConnection extends Disposable {
|
||||
try {
|
||||
await sleepPromise;
|
||||
} catch { } // User canceled timer
|
||||
=======
|
||||
if (waitTime > 0) {
|
||||
const sleepPromise = sleep(waitTime);
|
||||
this._onDidStateChange.fire(new ReconnectionWaitEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), waitTime, sleepPromise));
|
||||
|
||||
this._options.logService.info(`${logPrefix} waiting for ${waitTime} seconds before reconnecting...`);
|
||||
try {
|
||||
await sleepPromise;
|
||||
} catch { } // User canceled timer
|
||||
}
|
||||
>>>>>>> 89b6e0164fa770333755b11504e19a4232b1a2d4
|
||||
|
||||
if (PersistentConnection._permanentFailure) {
|
||||
this._options.logService.error(`${logPrefix} permanent failure occurred while running the reconnecting loop.`);
|
||||
@@ -436,26 +522,26 @@ abstract class PersistentConnection extends Disposable {
|
||||
}
|
||||
|
||||
// connection was lost, let's try to re-establish it
|
||||
this._onDidStateChange.fire(new ReconnectionRunningEvent());
|
||||
this._onDidStateChange.fire(new ReconnectionRunningEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1));
|
||||
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());
|
||||
this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1));
|
||||
|
||||
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();
|
||||
PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false);
|
||||
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();
|
||||
PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false);
|
||||
break;
|
||||
}
|
||||
if (RemoteAuthorityResolverError.isTemporarilyNotAvailable(err)) {
|
||||
@@ -476,16 +562,22 @@ abstract class PersistentConnection extends Disposable {
|
||||
// try again!
|
||||
continue;
|
||||
}
|
||||
if (err instanceof RemoteAuthorityResolverError) {
|
||||
this._options.logService.error(`${logPrefix} A RemoteAuthorityResolverError occurred while trying to reconnect. Will give up now! Error:`);
|
||||
this._options.logService.error(err);
|
||||
PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, RemoteAuthorityResolverError.isHandled(err));
|
||||
break;
|
||||
}
|
||||
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();
|
||||
PersistentConnection.triggerPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false);
|
||||
break;
|
||||
}
|
||||
} while (!PersistentConnection._permanentFailure);
|
||||
}
|
||||
|
||||
private _gotoPermanentFailure(): void {
|
||||
this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent());
|
||||
private _gotoPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void {
|
||||
this._onDidStateChange.fire(new ReconnectionPermanentFailureEvent(this.reconnectionToken, millisSinceLastIncomingData, attempt, handled));
|
||||
safeDisposeProtocolAndSocket(this.protocol);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import * as performance from 'vs/base/common/performance';
|
||||
|
||||
export interface IRemoteAgentEnvironment {
|
||||
pid: number;
|
||||
@@ -18,6 +19,7 @@ export interface IRemoteAgentEnvironment {
|
||||
workspaceStorageHome: URI;
|
||||
userHome: URI;
|
||||
os: OperatingSystem;
|
||||
marks: performance.PerformanceMark[];
|
||||
}
|
||||
|
||||
export interface RemoteAgentConnectionContext {
|
||||
|
||||
@@ -19,7 +19,7 @@ export function getRemoteName(authority: string | undefined): string | undefined
|
||||
}
|
||||
const pos = authority.indexOf('+');
|
||||
if (pos < 0) {
|
||||
// funky? bad authority?
|
||||
// e.g. localhost:8000
|
||||
return authority;
|
||||
}
|
||||
return authority.substr(0, pos);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { isWindows, OperatingSystem } from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
@@ -17,36 +18,64 @@ export interface RemoteTunnel {
|
||||
readonly tunnelRemoteHost: string;
|
||||
readonly tunnelLocalPort?: number;
|
||||
readonly localAddress: string;
|
||||
dispose(silent?: boolean): void;
|
||||
readonly public: boolean;
|
||||
dispose(silent?: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export interface TunnelOptions {
|
||||
remoteAddress: { port: number, host: string };
|
||||
remoteAddress: { port: number, host: string; };
|
||||
localAddressPort?: number;
|
||||
label?: string;
|
||||
public?: boolean;
|
||||
}
|
||||
|
||||
export interface TunnelCreationOptions {
|
||||
elevationRequired?: boolean;
|
||||
}
|
||||
|
||||
export interface TunnelProviderFeatures {
|
||||
elevation: boolean;
|
||||
public: boolean;
|
||||
}
|
||||
|
||||
export interface ITunnelProvider {
|
||||
forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<RemoteTunnel> | undefined;
|
||||
forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<RemoteTunnel | undefined> | undefined;
|
||||
}
|
||||
|
||||
export interface ITunnel {
|
||||
remoteAddress: { port: number, host: string };
|
||||
|
||||
/**
|
||||
* The complete local address(ex. localhost:1234)
|
||||
*/
|
||||
localAddress: string;
|
||||
|
||||
public?: boolean;
|
||||
|
||||
/**
|
||||
* Implementers of Tunnel should fire onDidDispose when dispose is called.
|
||||
*/
|
||||
onDidDispose: Event<void>;
|
||||
|
||||
dispose(): Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface ITunnelService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly tunnels: Promise<readonly RemoteTunnel[]>;
|
||||
readonly canMakePublic: boolean;
|
||||
readonly onTunnelOpened: Event<RemoteTunnel>;
|
||||
readonly onTunnelClosed: Event<{ host: string, port: number }>;
|
||||
readonly onTunnelClosed: Event<{ host: string, port: number; }>;
|
||||
readonly canElevate: boolean;
|
||||
|
||||
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
|
||||
canTunnel(uri: URI): boolean;
|
||||
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, isPublic?: boolean): Promise<RemoteTunnel | undefined> | undefined;
|
||||
closeTunnel(remoteHost: string, remotePort: number): Promise<void>;
|
||||
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable;
|
||||
setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable;
|
||||
}
|
||||
|
||||
export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number } | undefined {
|
||||
export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number; } | undefined {
|
||||
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
|
||||
return undefined;
|
||||
}
|
||||
@@ -74,51 +103,88 @@ function getOtherLocalhost(host: string): string | undefined {
|
||||
return (host === 'localhost') ? '127.0.0.1' : ((host === '127.0.0.1') ? 'localhost' : undefined);
|
||||
}
|
||||
|
||||
export function isPortPrivileged(port: number, os?: OperatingSystem): boolean {
|
||||
if (os) {
|
||||
return os !== OperatingSystem.Windows && (port < 1024);
|
||||
} else {
|
||||
return !isWindows && (port < 1024);
|
||||
}
|
||||
}
|
||||
|
||||
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> }>>();
|
||||
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 | undefined>; }>>();
|
||||
protected _tunnelProvider: ITunnelProvider | undefined;
|
||||
protected _canElevate: boolean = false;
|
||||
private _canMakePublic: boolean = false;
|
||||
|
||||
public constructor(
|
||||
@ILogService protected readonly logService: ILogService
|
||||
) { }
|
||||
|
||||
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable {
|
||||
setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable {
|
||||
this._tunnelProvider = provider;
|
||||
if (!provider) {
|
||||
// clear features
|
||||
this._canElevate = false;
|
||||
this._canMakePublic = false;
|
||||
return {
|
||||
dispose: () => { }
|
||||
};
|
||||
}
|
||||
this._tunnelProvider = provider;
|
||||
this._canElevate = features.elevation;
|
||||
this._canMakePublic = features.public;
|
||||
return {
|
||||
dispose: () => {
|
||||
this._tunnelProvider = undefined;
|
||||
this._canElevate = false;
|
||||
this._canMakePublic = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
public get canElevate(): boolean {
|
||||
return this._canElevate;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
public get canMakePublic() {
|
||||
return this._canMakePublic;
|
||||
}
|
||||
|
||||
public get tunnels(): Promise<readonly RemoteTunnel[]> {
|
||||
return new Promise(async (resolve) => {
|
||||
const tunnels: RemoteTunnel[] = [];
|
||||
const tunnelArray = Array.from(this._tunnels.values());
|
||||
for (let portMap of tunnelArray) {
|
||||
const portArray = Array.from(portMap.values());
|
||||
for (let x of portArray) {
|
||||
const tunnelValue = await x.value;
|
||||
if (tunnelValue) {
|
||||
tunnels.push(tunnelValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve(tunnels);
|
||||
});
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
for (const portMap of this._tunnels.values()) {
|
||||
for (const { value } of portMap.values()) {
|
||||
value.then(tunnel => tunnel.dispose());
|
||||
await value.then(tunnel => tunnel?.dispose());
|
||||
}
|
||||
portMap.clear();
|
||||
}
|
||||
this._tunnels.clear();
|
||||
}
|
||||
|
||||
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort: number): Promise<RemoteTunnel> | undefined {
|
||||
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false, isPublic: boolean = false): Promise<RemoteTunnel | undefined> | undefined {
|
||||
this.logService.trace(`openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`);
|
||||
if (!addressProvider) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -127,12 +193,19 @@ export abstract class AbstractTunnelService implements ITunnelService {
|
||||
remoteHost = 'localhost';
|
||||
}
|
||||
|
||||
const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort);
|
||||
const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic);
|
||||
if (!resolvedTunnel) {
|
||||
this.logService.trace(`Tunnel was not created.`);
|
||||
return resolvedTunnel;
|
||||
}
|
||||
|
||||
return resolvedTunnel.then(tunnel => {
|
||||
if (!tunnel) {
|
||||
this.logService.trace('New tunnel is undefined.');
|
||||
this.removeEmptyTunnelFromMap(remoteHost!, remotePort);
|
||||
return undefined;
|
||||
}
|
||||
this.logService.trace('New tunnel established.');
|
||||
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.');
|
||||
@@ -148,24 +221,28 @@ export abstract class AbstractTunnelService implements ITunnelService {
|
||||
tunnelRemoteHost: tunnel.tunnelRemoteHost,
|
||||
tunnelLocalPort: tunnel.tunnelLocalPort,
|
||||
localAddress: tunnel.localAddress,
|
||||
dispose: () => {
|
||||
public: tunnel.public,
|
||||
dispose: async () => {
|
||||
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);
|
||||
await this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise<RemoteTunnel> }): Promise<void> {
|
||||
private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise<RemoteTunnel | undefined> }): 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 });
|
||||
this.logService.trace(`Tunnel is being disposed ${remoteHost}:${remotePort}.`);
|
||||
const disposePromise: Promise<void> = tunnel.value.then(async (tunnel) => {
|
||||
if (tunnel) {
|
||||
await tunnel.dispose(true);
|
||||
this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort });
|
||||
}
|
||||
});
|
||||
if (this._tunnels.has(remoteHost)) {
|
||||
this._tunnels.get(remoteHost)!.delete(remotePort);
|
||||
@@ -183,16 +260,30 @@ export abstract class AbstractTunnelService implements ITunnelService {
|
||||
}
|
||||
}
|
||||
|
||||
protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise<RemoteTunnel>) {
|
||||
protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise<RemoteTunnel | undefined>) {
|
||||
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 {
|
||||
private async removeEmptyTunnelFromMap(remoteHost: string, remotePort: number) {
|
||||
const hostMap = this._tunnels.get(remoteHost);
|
||||
if (hostMap) {
|
||||
const tunnel = hostMap.get(remotePort);
|
||||
const tunnelResult = await tunnel;
|
||||
if (!tunnelResult) {
|
||||
hostMap.delete(remotePort);
|
||||
}
|
||||
if (hostMap.size === 0) {
|
||||
this._tunnels.delete(remoteHost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise<RemoteTunnel | undefined> } | undefined {
|
||||
const otherLocalhost = getOtherLocalhost(remoteHost);
|
||||
let portMap: Map<number, { refcount: number, readonly value: Promise<RemoteTunnel> }> | undefined;
|
||||
let portMap: Map<number, { refcount: number, readonly value: Promise<RemoteTunnel | undefined> }> | undefined;
|
||||
if (otherLocalhost) {
|
||||
const firstMap = this._tunnels.get(remoteHost);
|
||||
const secondMap = this._tunnels.get(otherLocalhost);
|
||||
@@ -207,31 +298,25 @@ export abstract class AbstractTunnelService implements ITunnelService {
|
||||
return portMap ? portMap.get(remotePort) : undefined;
|
||||
}
|
||||
|
||||
protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
|
||||
canTunnel(uri: URI): boolean {
|
||||
return !!extractLocalHostUriMetaDataForPortMapping(uri);
|
||||
}
|
||||
|
||||
protected isPortPrivileged(port: number): boolean {
|
||||
return port < 1024;
|
||||
protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise<RemoteTunnel | undefined> | undefined;
|
||||
|
||||
protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise<RemoteTunnel | undefined> | undefined {
|
||||
this.logService.trace(`Creating tunnel with provider ${remoteHost}:${remotePort} on local port ${localPort}.`);
|
||||
|
||||
const preferredLocalPort = localPort === undefined ? remotePort : localPort;
|
||||
const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false };
|
||||
const tunnelOptions: TunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort, public: isPublic };
|
||||
const tunnel = tunnelProvider.forwardPort(tunnelOptions, creationInfo);
|
||||
this.logService.trace('Tunnel created by provider.');
|
||||
if (tunnel) {
|
||||
this.addTunnelToMap(remoteHost, remotePort, tunnel);
|
||||
}
|
||||
return tunnel;
|
||||
}
|
||||
}
|
||||
|
||||
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 preferredLocalPort = localPort === undefined ? remotePort : localPort;
|
||||
const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort };
|
||||
const creationInfo = { elevationRequired: this.isPortPrivileged(preferredLocalPort) };
|
||||
const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo);
|
||||
if (tunnel) {
|
||||
this.addTunnelToMap(remoteHost, remotePort, tunnel);
|
||||
}
|
||||
return tunnel;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user