diff --git a/extensions/vscode-test-resolver/package.json b/extensions/vscode-test-resolver/package.json index 167275aa275..921bbba555c 100644 --- a/extensions/vscode-test-resolver/package.json +++ b/extensions/vscode-test-resolver/package.json @@ -66,6 +66,11 @@ "category": "Remote-TestResolver", "command": "vscode-testresolver.currentWindow" }, + { + "title": "Connect to TestResolver in Current Window with Managed Connection", + "category": "Remote-TestResolver", + "command": "vscode-testresolver.currentWindowManaged" + }, { "title": "Show TestResolver Log", "category": "Remote-TestResolver", diff --git a/extensions/vscode-test-resolver/src/extension.ts b/extensions/vscode-test-resolver/src/extension.ts index 05fd267d2bc..46f95f14f1f 100644 --- a/extensions/vscode-test-resolver/src/extension.ts +++ b/extensions/vscode-test-resolver/src/extension.ts @@ -27,7 +27,30 @@ export function activate(context: vscode.ExtensionContext) { let connectionPaused = false; const connectionPausedEvent = new vscode.EventEmitter(); - function doResolve(_authority: string, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise { + function getTunnelFeatures(): vscode.TunnelInformation['tunnelFeatures'] { + return { + elevation: true, + privacyOptions: vscode.workspace.getConfiguration('testresolver').get('supportPublicPorts') ? [ + { + id: 'public', + label: 'Public', + themeIcon: 'eye' + }, + { + id: 'other', + label: 'Other', + themeIcon: 'circuit-board' + }, + { + id: 'private', + label: 'Private', + themeIcon: 'eye-closed' + } + ] : [] + }; + } + + function doResolve(authority: string, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise { if (connectionPaused) { throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable('Not available right now'); } @@ -150,7 +173,35 @@ export function activate(context: vscode.ExtensionContext) { } }); }); - return serverPromise.then(serverAddr => { + + return serverPromise.then((serverAddr): Promise => { + if (authority.includes('managed')) { + console.log('Connecting via a managed authority'); + return Promise.resolve(new vscode.ManagedResolvedAuthority(async () => { + const remoteSocket = net.createConnection({ port: serverAddr.port }); + const dataEmitter = new vscode.EventEmitter(); + const closeEmitter = new vscode.EventEmitter(); + const endEmitter = new vscode.EventEmitter(); + + await new Promise((res, rej) => { + remoteSocket.on('data', d => dataEmitter.fire(d)) + .on('error', err => { rej(); closeEmitter.fire(err); }) + .on('close', () => endEmitter.fire()) + .on('end', () => endEmitter.fire()) + .on('connect', res); + }); + + + return { + onDidReceiveMessage: dataEmitter.event, + onDidClose: closeEmitter.event, + onDidEnd: endEmitter.event, + dataHandler: d => remoteSocket.write(d), + endHandler: () => remoteSocket.end(), + }; + }, connectionToken)); + } + return new Promise((res, _rej) => { const proxyServer = net.createServer(proxySocket => { outputChannel.appendLine(`Proxy connection accepted`); @@ -228,28 +279,7 @@ export function activate(context: vscode.ExtensionContext) { proxyServer.listen(0, '127.0.0.1', () => { const port = (proxyServer.address()).port; outputChannel.appendLine(`Going through proxy at port ${port}`); - const r: vscode.ResolverResult = new vscode.ResolvedAuthority('127.0.0.1', port, connectionToken); - r.tunnelFeatures = { - elevation: true, - privacyOptions: vscode.workspace.getConfiguration('testresolver').get('supportPublicPorts') ? [ - { - id: 'public', - label: 'Public', - themeIcon: 'eye' - }, - { - id: 'other', - label: 'Other', - themeIcon: 'circuit-board' - }, - { - id: 'private', - label: 'Private', - themeIcon: 'eye-closed' - } - ] : [] - }; - res(r); + res(new vscode.ResolvedAuthority('127.0.0.1', port, connectionToken)); }); context.subscriptions.push({ dispose: () => { @@ -264,12 +294,16 @@ export function activate(context: vscode.ExtensionContext) { async getCanonicalURI(uri: vscode.Uri): Promise { return vscode.Uri.file(uri.path); }, - resolve(_authority: string): Thenable { + resolve(_authority: string): Thenable { return vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 'Open TestResolver Remote ([details](command:vscode-testresolver.showLog))', cancellable: false - }, (progress) => doResolve(_authority, progress)); + }, async (progress) => { + const rr = await doResolve(_authority, progress); + rr.tunnelFeatures = getTunnelFeatures(); + return rr; + }); }, tunnelFactory, showCandidatePort @@ -282,6 +316,9 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.currentWindow', () => { return vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: 'test+test', reuseWindow: true }); })); + context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.currentWindowManaged', () => { + return vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: 'test+managed', reuseWindow: true }); + })); context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.newWindowWithError', () => { return vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: 'test+error' }); })); diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index c476468e7fe..f1af44098af 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -1406,6 +1406,11 @@ export class IntervalCounter { export type ValueCallback = (value: T | Promise) => void; +const enum DeferredOutcome { + Resolved, + Rejected +} + /** * Creates a promise whose resolution or rejection can be controlled imperatively. */ @@ -1413,19 +1418,22 @@ export class DeferredPromise { private completeCallback!: ValueCallback; private errorCallback!: (err: unknown) => void; - private rejected = false; - private resolved = false; + private outcome?: { outcome: DeferredOutcome.Rejected; value: any } | { outcome: DeferredOutcome.Resolved; value: T }; public get isRejected() { - return this.rejected; + return this.outcome?.outcome === DeferredOutcome.Rejected; } public get isResolved() { - return this.resolved; + return this.outcome?.outcome === DeferredOutcome.Resolved; } public get isSettled() { - return this.rejected || this.resolved; + return !!this.outcome; + } + + public get value() { + return this.outcome?.outcome === DeferredOutcome.Resolved ? this.outcome?.value : undefined; } public readonly p: Promise; @@ -1440,7 +1448,7 @@ export class DeferredPromise { public complete(value: T) { return new Promise(resolve => { this.completeCallback(value); - this.resolved = true; + this.outcome = { outcome: DeferredOutcome.Resolved, value }; resolve(); }); } @@ -1448,17 +1456,13 @@ export class DeferredPromise { public error(err: unknown) { return new Promise(resolve => { this.errorCallback(err); - this.rejected = true; + this.outcome = { outcome: DeferredOutcome.Rejected, value: err }; resolve(); }); } public cancel() { - new Promise(resolve => { - this.errorCallback(new CancellationError()); - this.rejected = true; - resolve(); - }); + return this.error(new CancellationError()); } } diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts index 765a788327b..ff61eb5c9e2 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Lazy } from 'vs/base/common/lazy'; import * as streams from 'vs/base/common/stream'; declare const Buffer: any; const hasBuffer = (typeof Buffer !== 'undefined'); +const indexOfTable = new Lazy(() => new Uint8Array(256)); let textEncoder: TextEncoder | null; let textDecoder: TextDecoder | null; @@ -169,6 +171,52 @@ export class VSBuffer { writeUInt8(value: number, offset: number): void { writeUInt8(this.buffer, value, offset); } + + indexOf(subarray: VSBuffer | Uint8Array) { + const needle = subarray instanceof VSBuffer ? subarray.buffer : subarray; + const needleLen = needle.byteLength; + const haystack = this.buffer; + const haystackLen = haystack.byteLength; + + if (needleLen === 0) { + return 0; + } + + if (needleLen === 1) { + return haystack.indexOf(needle[0]); + } + + if (needleLen > haystackLen) { + return -1; + } + + // find index of the subarray using boyer-moore-horspool algorithm + const table = indexOfTable.value; + table.fill(needle.length); + for (let i = 0; i < needle.length; i++) { + table[needle[i]] = needle.length - i - 1; + } + + let i = needle.length - 1; + let j = i; + let result = -1; + while (i < haystackLen) { + if (haystack[i] === needle[j]) { + if (j === 0) { + result = i; + break; + } + + i--; + j--; + } else { + i += Math.max(needle.length - j, table[haystack[i]]); + j = needle.length - 1; + } + } + + return result; + } } export function readUInt16LE(source: Uint8Array, offset: number): number { diff --git a/src/vs/base/test/common/buffer.test.ts b/src/vs/base/test/common/buffer.test.ts index ae210a549b3..5a37943b658 100644 --- a/src/vs/base/test/common/buffer.test.ts +++ b/src/vs/base/test/common/buffer.test.ts @@ -413,6 +413,22 @@ suite('Buffer', () => { } }); + test('indexOf', () => { + const haystack = VSBuffer.fromString('abcaabbccaaabbbccc'); + assert.strictEqual(haystack.indexOf(VSBuffer.fromString('')), 0); + assert.strictEqual(haystack.indexOf(VSBuffer.fromString('a'.repeat(100))), -1); + + assert.strictEqual(haystack.indexOf(VSBuffer.fromString('a')), 0); + assert.strictEqual(haystack.indexOf(VSBuffer.fromString('c')), 2); + + assert.strictEqual(haystack.indexOf(VSBuffer.fromString('abcaa')), 0); + assert.strictEqual(haystack.indexOf(VSBuffer.fromString('caaab')), 8); + assert.strictEqual(haystack.indexOf(VSBuffer.fromString('ccc')), 15); + + assert.strictEqual(haystack.indexOf(VSBuffer.fromString('cccb')), -1); + + }); + suite('base64', () => { /* Generated with: diff --git a/src/vs/platform/remote/browser/browserSocketFactory.ts b/src/vs/platform/remote/browser/browserSocketFactory.ts index 0998b898ea9..b1069a7d1ee 100644 --- a/src/vs/platform/remote/browser/browserSocketFactory.ts +++ b/src/vs/platform/remote/browser/browserSocketFactory.ts @@ -10,7 +10,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ISocket, SocketCloseEvent, SocketCloseEventType, SocketDiagnostics, SocketDiagnosticsEventType } from 'vs/base/parts/ipc/common/ipc.net'; import { IConnectCallback, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; -import { RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode, WebSocketMessagingPassing } from 'vs/platform/remote/common/remoteAuthorityResolver'; export interface IWebSocketFactory { create(url: string, debugLabel: string): IWebSocket; @@ -265,14 +265,14 @@ class BrowserSocket implements ISocket { } -export class BrowserSocketFactory implements ISocketFactory { +export class BrowserSocketFactory implements ISocketFactory { private readonly _webSocketFactory: IWebSocketFactory; constructor(webSocketFactory: IWebSocketFactory | null | undefined) { this._webSocketFactory = webSocketFactory || defaultWebSocketFactory; } - connect(host: string, port: number, path: string, query: string, debugLabel: string, callback: IConnectCallback): void { + connect({ host, port }: WebSocketMessagingPassing, path: string, query: string, debugLabel: string, callback: IConnectCallback): void { const webSocketSchema = (/^https:/.test(window.location.href) ? 'wss' : 'ws'); const socket = this._webSocketFactory.create(`${webSocketSchema}://${(/:/.test(host) && !/\[/.test(host)) ? `[${host}]` : host}:${port}${path}?${query}&skipWebSocketFrames=false`, debugLabel); const errorListener = socket.onError((err) => callback(err, undefined)); diff --git a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts index 183790bd295..bdcf72ae0da 100644 --- a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts +++ b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DeferredPromise } from 'vs/base/common/async'; +import * as errors from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { RemoteAuthorities } from 'vs/base/common/network'; @@ -11,7 +13,7 @@ import { StopWatch } from 'vs/base/common/stopwatch'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IRemoteAuthorityResolverService, IRemoteConnectionData, ResolvedAuthority, ResolverResult, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAuthorityResolverService, IRemoteConnectionData, MessagePassingType, ResolvedAuthority, ResolvedOptions, ResolverResult, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { getRemoteServerRootPath, parseAuthorityWithOptionalPort } from 'vs/platform/remote/common/remoteHosts'; export class RemoteAuthorityResolverService extends Disposable implements IRemoteAuthorityResolverService { @@ -21,12 +23,14 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot private readonly _onDidChangeConnectionData = this._register(new Emitter()); public readonly onDidChangeConnectionData = this._onDidChangeConnectionData.event; - private readonly _promiseCache = new Map>(); + private readonly _resolveAuthorityRequests = new Map>(); private readonly _cache = new Map(); private readonly _connectionToken: Promise | string | undefined; private readonly _connectionTokens: Map; + private readonly _isWorkbenchOptionsBasedResolution: boolean; constructor( + isWorkbenchOptionsBasedResolution: boolean, connectionToken: Promise | string | undefined, resourceUriProvider: ((uri: URI) => URI) | undefined, @IProductService productService: IProductService, @@ -35,6 +39,7 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot super(); this._connectionToken = connectionToken; this._connectionTokens = new Map(); + this._isWorkbenchOptionsBasedResolution = isWorkbenchOptionsBasedResolution; if (resourceUriProvider) { RemoteAuthorities.setDelegate(resourceUriProvider); } @@ -42,15 +47,20 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot } async resolveAuthority(authority: string): Promise { - let result = this._promiseCache.get(authority); + let result = this._resolveAuthorityRequests.get(authority); if (!result) { - result = this._doResolveAuthority(authority); - this._promiseCache.set(authority, result); + result = new DeferredPromise(); + this._resolveAuthorityRequests.set(authority, result); + if (this._isWorkbenchOptionsBasedResolution) { + this._doResolveAuthority(authority).then(v => result!.complete(v), (err) => result!.error(err)); + } } - return result; + + return result.p; } async getCanonicalURI(uri: URI): Promise { + // todo@connor4312 make this work for web return uri; } @@ -61,8 +71,7 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot const resolverResult = this._cache.get(authority)!; const connectionToken = this._connectionTokens.get(authority) || resolverResult.authority.connectionToken; return { - host: resolverResult.authority.host, - port: resolverResult.authority.port, + connectTo: resolverResult.authority.messaging, connectionToken: connectionToken }; } @@ -77,20 +86,42 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot this._logService.info(`Resolved connection token (${authorityPrefix}) after ${sw.elapsed()} ms`); const defaultPort = (/^https:/.test(window.location.href) ? 443 : 80); const { host, port } = parseAuthorityWithOptionalPort(authority, defaultPort); - const result: ResolverResult = { authority: { authority, host: host, port: port, connectionToken } }; - RemoteAuthorities.set(authority, result.authority.host, result.authority.port); + const result: ResolverResult = { authority: { authority, messaging: { type: MessagePassingType.WebSocket, host: host, port: port }, connectionToken } }; + RemoteAuthorities.set(authority, host, port); this._cache.set(authority, result); this._onDidChangeConnectionData.fire(); return result; } + _clearResolvedAuthority(authority: string): void { + if (this._resolveAuthorityRequests.has(authority)) { + this._resolveAuthorityRequests.get(authority)!.cancel(); + this._resolveAuthorityRequests.delete(authority); + } } - _setResolvedAuthority(resolvedAuthority: ResolvedAuthority) { + _setResolvedAuthority(resolvedAuthority: ResolvedAuthority, options?: ResolvedOptions): void { + if (this._resolveAuthorityRequests.has(resolvedAuthority.authority)) { + const request = this._resolveAuthorityRequests.get(resolvedAuthority.authority)!; + if (resolvedAuthority.messaging.type === MessagePassingType.WebSocket) { + // todo@connor4312 need to implement some kind of loopback for ext host based messaging + RemoteAuthorities.set(resolvedAuthority.authority, resolvedAuthority.messaging.host, resolvedAuthority.messaging.port); + } + if (resolvedAuthority.connectionToken) { + RemoteAuthorities.setConnectionToken(resolvedAuthority.authority, resolvedAuthority.connectionToken); + } + request.complete({ authority: resolvedAuthority, options }); + this._onDidChangeConnectionData.fire(); + } } _setResolvedAuthorityError(authority: string, err: any): void { + if (this._resolveAuthorityRequests.has(authority)) { + const request = this._resolveAuthorityRequests.get(authority)!; + // Avoid that this error makes it to telemetry + request.error(errors.ErrorNoTelemetry.fromError(err)); + } } _setAuthorityConnectionToken(authority: string, connectionToken: string): void { diff --git a/src/vs/platform/remote/common/managedSocket.ts b/src/vs/platform/remote/common/managedSocket.ts new file mode 100644 index 00000000000..9cbf5f326e8 --- /dev/null +++ b/src/vs/platform/remote/common/managedSocket.ts @@ -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 { VSBuffer, encodeBase64 } from 'vs/base/common/buffer'; + +export const makeRawSocketHeaders = (path: string, query: string, deubgLabel: string) => { + // https://tools.ietf.org/html/rfc6455#section-4 + const buffer = new Uint8Array(16); + for (let i = 0; i < 16; i++) { + buffer[i] = Math.round(Math.random() * 256); + } + const nonce = encodeBase64(VSBuffer.wrap(buffer)); + + const headers = [ + `GET ws://localhost${path}?${query}&skipWebSocketFrames=true HTTP/1.1`, + `Connection: Upgrade`, + `Upgrade: websocket`, + `Sec-WebSocket-Key: ${nonce}` + ]; + + return headers.join('\r\n') + '\r\n\r\n'; +}; + +export const socketRawEndHeaderSequence = VSBuffer.fromString('\r\n\r\n'); diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts index bbbddcc39d5..2b526520983 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -16,7 +16,7 @@ import { IIPCLogger } from 'vs/base/parts/ipc/common/ipc'; import { Client, ConnectionHealth, ISocket, PersistentProtocol, ProtocolConstants, SocketCloseEventType } from 'vs/base/parts/ipc/common/ipc.net'; import { ILogService } from 'vs/platform/log/common/log'; import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; -import { RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { RemoteAuthorityResolverError, ResolvedAuthorityMessagePassing } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts'; import { ISignService } from 'vs/platform/sign/common/sign'; @@ -71,15 +71,14 @@ export interface OKMessage { export type HandshakeMessage = AuthRequest | SignRequest | ConnectionTypeRequest | ErrorMessage | OKMessage; -interface ISimpleConnectionOptions { +interface ISimpleConnectionOptions { commit: string | undefined; quality: string | undefined; - host: string; - port: number; + connectTo: T; connectionToken: string | undefined; reconnectionToken: string; reconnectionProtocol: PersistentProtocol | null; - socketFactory: ISocketFactory; + socketFactory: ISocketFactory; signService: ISignService; logService: ILogService; } @@ -88,8 +87,8 @@ export interface IConnectCallback { (err: any | undefined, socket: ISocket | undefined): void; } -export interface ISocketFactory { - connect(host: string, port: number, path: string, query: string, debugLabel: string, callback: IConnectCallback): void; +export interface ISocketFactory { + connect(connectTo: T, path: string, query: string, debugLabel: string, callback: IConnectCallback): void; } function createTimeoutCancellation(millis: number): CancellationToken { @@ -192,12 +191,12 @@ function readOneControlMessage(protocol: PersistentProtocol, timeoutCancellat return result.promise; } -function createSocket(logService: ILogService, socketFactory: ISocketFactory, host: string, port: number, path: string, query: string, debugConnectionType: string, debugLabel: string, timeoutCancellationToken: CancellationToken): Promise { +function createSocket(logService: ILogService, socketFactory: ISocketFactory, connectTo: T, path: string, query: string, debugConnectionType: string, debugLabel: string, timeoutCancellationToken: CancellationToken): Promise { const result = new PromiseWithTimeout(timeoutCancellationToken); const sw = StopWatch.create(false); logService.info(`Creating a socket (${debugLabel})...`); performance.mark(`code/willCreateSocket/${debugConnectionType}`); - socketFactory.connect(host, port, path, query, debugLabel, (err: any, socket: ISocket | undefined) => { + socketFactory.connect(connectTo, path, query, debugLabel, (err: any, socket: ISocket | undefined) => { if (result.didTimeout) { performance.mark(`code/didCreateSocketError/${debugConnectionType}`); logService.info(`Creating a socket (${debugLabel}) finished after ${sw.elapsed()} ms, but this is too late and has timed out already.`); @@ -237,14 +236,14 @@ function raceWithTimeoutCancellation(promise: Promise, timeoutCancellation return result.promise; } -async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined, timeoutCancellationToken: CancellationToken): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean }> { +async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined, timeoutCancellationToken: CancellationToken): 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.logService, options.socketFactory, options.host, options.port, getRemoteServerRootPath(options), `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, connectionTypeToString(connectionType), `renderer-${connectionTypeToString(connectionType)}-${options.reconnectionToken}`, timeoutCancellationToken); + socket = await createSocket(options.logService, options.socketFactory, options.connectTo, getRemoteServerRootPath(options), `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, connectionTypeToString(connectionType), `renderer-${connectionTypeToString(connectionType)}-${options.reconnectionToken}`, timeoutCancellationToken); } catch (error) { options.logService.error(`${logPrefix} socketFactory.connect() failed or timed out. Error:`); options.logService.error(error); @@ -389,23 +388,22 @@ async function doConnectRemoteAgentTunnel(options: ISimpleConnectionOptions, sta return protocol; } -export interface IConnectionOptions { +export interface IConnectionOptions { commit: string | undefined; quality: string | undefined; - socketFactory: ISocketFactory; - addressProvider: IAddressProvider; + socketFactory: ISocketFactory; + addressProvider: IAddressProvider; signService: ISignService; logService: ILogService; ipcLogger: IIPCLogger | null; } -async function resolveConnectionOptions(options: IConnectionOptions, reconnectionToken: string, reconnectionProtocol: PersistentProtocol | null): Promise { - const { host, port, connectionToken } = await options.addressProvider.getAddress(); +async function resolveConnectionOptions(options: IConnectionOptions, reconnectionToken: string, reconnectionProtocol: PersistentProtocol | null): Promise> { + const { connectTo, connectionToken } = await options.addressProvider.getAddress(); return { commit: options.commit, quality: options.quality, - host: host, - port: port, + connectTo, connectionToken: connectionToken, reconnectionToken: reconnectionToken, reconnectionProtocol: reconnectionProtocol, @@ -415,14 +413,13 @@ async function resolveConnectionOptions(options: IConnectionOptions, reconnectio }; } -export interface IAddress { - host: string; - port: number; +export interface IAddress { + connectTo: T; connectionToken: string | undefined; } -export interface IAddressProvider { - getAddress(): Promise; +export interface IAddressProvider { + getAddress(): Promise>; } export async function connectRemoteAgentManagement(options: IConnectionOptions, remoteAuthority: string, clientId: string): Promise { @@ -448,7 +445,7 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption /** * Will attempt to connect 5 times. If it fails 5 consecutive times, it will give up. */ -async function createInitialConnection(options: IConnectionOptions, connectionFactory: (simpleOptions: ISimpleConnectionOptions) => Promise): Promise { +async function createInitialConnection(options: IConnectionOptions, connectionFactory: (simpleOptions: ISimpleConnectionOptions) => Promise): Promise { const MAX_ATTEMPTS = 5; for (let attempt = 1; ; attempt++) { @@ -691,7 +688,7 @@ export abstract class PersistentConnection extends Disposable { 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}...`); + this._options.logService.info(`${logPrefix} connecting to ${simpleOptions.connectTo}...`); await this._reconnect(simpleOptions, createTimeoutCancellation(RECONNECT_TIMEOUT)); this._options.logService.info(`${logPrefix} reconnected!`); this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1)); @@ -832,7 +829,7 @@ function commonLogPrefix(connectionType: ConnectionType, reconnectionToken: stri } function connectLogPrefix(options: ISimpleConnectionOptions, connectionType: ConnectionType): string { - return `${commonLogPrefix(connectionType, options.reconnectionToken, !!options.reconnectionProtocol)}[${options.host}:${options.port}]`; + return `${commonLogPrefix(connectionType, options.reconnectionToken, !!options.reconnectionProtocol)}[${options.connectTo}]`; } function logElapsed(startTime: number): string { diff --git a/src/vs/platform/remote/common/remoteAuthorityResolver.ts b/src/vs/platform/remote/common/remoteAuthorityResolver.ts index d90895652e6..5f61474ff1d 100644 --- a/src/vs/platform/remote/common/remoteAuthorityResolver.ts +++ b/src/vs/platform/remote/common/remoteAuthorityResolver.ts @@ -10,10 +10,29 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export const IRemoteAuthorityResolverService = createDecorator('remoteAuthorityResolverService'); +export const enum MessagePassingType { + WebSocket, + Managed +} + +export interface ManagedMessagingPassing { + type: MessagePassingType.Managed; + id: number; +} + +export interface WebSocketMessagingPassing { + type: MessagePassingType.WebSocket; + host: string; + port: number; +} + +export type ResolvedAuthorityMessagePassing = WebSocketMessagingPassing | ManagedMessagingPassing; + +export type MessagePassingOfType = ResolvedAuthorityMessagePassing & { type: T }; + export interface ResolvedAuthority { readonly authority: string; - readonly host: string; - readonly port: number; + readonly messaging: ResolvedAuthorityMessagePassing; readonly connectionToken: string | undefined; } @@ -50,8 +69,7 @@ export interface ResolverResult { } export interface IRemoteConnectionData { - host: string; - port: number; + connectTo: ResolvedAuthorityMessagePassing; connectionToken: string | undefined; } diff --git a/src/vs/platform/remote/common/remoteSocketFactoryCollection.ts b/src/vs/platform/remote/common/remoteSocketFactoryCollection.ts new file mode 100644 index 00000000000..d002d289a49 --- /dev/null +++ b/src/vs/platform/remote/common/remoteSocketFactoryCollection.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { mapFind } from 'vs/base/common/arrays'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; +import { MessagePassingOfType, MessagePassingType, ResolvedAuthorityMessagePassing } from 'vs/platform/remote/common/remoteAuthorityResolver'; + +export const IRemoteSocketFactoryCollection = createDecorator('remoteSocketFactoryCollection'); + +export interface IRemoteSocketFactoryCollection { + readonly _serviceBrand: undefined; + + /** + * Register a socket factory for the given message passing type + * @param type passing type to register for + * @param factory function that returns the socket factory, or undefined if + * it can't handle the data. + */ + register( + type: T, + factory: (messagePassing: MessagePassingOfType) => ISocketFactory> | undefined + ): void; + + /** + * Gets a socket factory for the given message passing data. + */ + create(messagePassing: T): ISocketFactory | undefined; +} + +export class RemoteSocketFactoryCollection implements IRemoteSocketFactoryCollection { + declare readonly _serviceBrand: undefined; + + private readonly factories: { [T in MessagePassingType]?: ((messagePassing: MessagePassingOfType) => ISocketFactory> | undefined)[] } = {}; + + + public register( + type: T, + factory: (messagePassing: MessagePassingOfType) => ISocketFactory> | undefined + ): void { + this.factories[type] ??= []; + this.factories[type]!.push(factory); + } + + public create(messagePassing: T): ISocketFactory | undefined { + return mapFind( + (this.factories[messagePassing.type] || []) as ((messagePassing: T) => ISocketFactory | undefined)[], + factory => factory(messagePassing), + ); + } +} diff --git a/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts b/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts index f048d7153a9..f51ca6b3a13 100644 --- a/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts +++ b/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts @@ -3,41 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // +import { DeferredPromise } from 'vs/base/common/async'; import * as errors from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { RemoteAuthorities } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IRemoteAuthorityResolverService, IRemoteConnectionData, ResolvedAuthority, ResolvedOptions, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAuthorityResolverService, IRemoteConnectionData, MessagePassingType, ResolvedAuthority, ResolvedOptions, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts'; -class PendingPromise { - public readonly promise: Promise; - public readonly input: I; - public result: R | null; - private _resolve!: (value: R) => void; - private _reject!: (err: any) => void; - - constructor(request: I) { - this.input = request; - this.promise = new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; - }); - this.result = null; - } - - resolve(result: R): void { - this.result = result; - this._resolve(this.result); - } - - reject(err: any): void { - this._reject(err); - } -} - export class RemoteAuthorityResolverService extends Disposable implements IRemoteAuthorityResolverService { declare readonly _serviceBrand: undefined; @@ -45,16 +20,16 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot private readonly _onDidChangeConnectionData = this._register(new Emitter()); public readonly onDidChangeConnectionData = this._onDidChangeConnectionData.event; - private readonly _resolveAuthorityRequests: Map>; + private readonly _resolveAuthorityRequests: Map>; private readonly _connectionTokens: Map; - private readonly _canonicalURIRequests: Map>; + private readonly _canonicalURIRequests: Map>; private _canonicalURIProvider: ((uri: URI) => Promise) | null; constructor(@IProductService productService: IProductService) { super(); - this._resolveAuthorityRequests = new Map>(); + this._resolveAuthorityRequests = new Map>(); this._connectionTokens = new Map(); - this._canonicalURIRequests = new Map>(); + this._canonicalURIRequests = new Map>(); this._canonicalURIProvider = null; RemoteAuthorities.setServerRootPath(getRemoteServerRootPath(productService)); @@ -62,19 +37,19 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot resolveAuthority(authority: string): Promise { if (!this._resolveAuthorityRequests.has(authority)) { - this._resolveAuthorityRequests.set(authority, new PendingPromise(authority)); + this._resolveAuthorityRequests.set(authority, new DeferredPromise()); } - return this._resolveAuthorityRequests.get(authority)!.promise; + return this._resolveAuthorityRequests.get(authority)!.p; } async getCanonicalURI(uri: URI): Promise { const key = uri.toString(); if (!this._canonicalURIRequests.has(key)) { - const request = new PendingPromise(uri); - this._canonicalURIProvider?.(request.input).then((uri) => request.resolve(uri), (err) => request.reject(err)); + const request = new DeferredPromise(); + this._canonicalURIProvider?.(uri).then((uri) => request.complete(uri), (err) => request.error(err)); this._canonicalURIRequests.set(key, request); } - return this._canonicalURIRequests.get(key)!.promise; + return this._canonicalURIRequests.get(key)!.p; } getConnectionData(authority: string): IRemoteConnectionData | null { @@ -82,20 +57,19 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot return null; } const request = this._resolveAuthorityRequests.get(authority)!; - if (!request.result) { + if (!request.isResolved) { return null; } const connectionToken = this._connectionTokens.get(authority); return { - host: request.result.authority.host, - port: request.result.authority.port, + connectTo: request.value!.authority.messaging, connectionToken: connectionToken }; } _clearResolvedAuthority(authority: string): void { if (this._resolveAuthorityRequests.has(authority)) { - this._resolveAuthorityRequests.get(authority)!.reject(errors.canceled()); + this._resolveAuthorityRequests.get(authority)!.cancel(); this._resolveAuthorityRequests.delete(authority); } } @@ -103,11 +77,14 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot _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.messaging.type === MessagePassingType.WebSocket) { + // todo@connor4312 need to implement some kind of loopback for ext host based messaging + RemoteAuthorities.set(resolvedAuthority.authority, resolvedAuthority.messaging.host, resolvedAuthority.messaging.port); + } if (resolvedAuthority.connectionToken) { RemoteAuthorities.setConnectionToken(resolvedAuthority.authority, resolvedAuthority.connectionToken); } - request.resolve({ authority: resolvedAuthority, options }); + request.complete({ authority: resolvedAuthority, options }); this._onDidChangeConnectionData.fire(); } } @@ -116,7 +93,7 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot if (this._resolveAuthorityRequests.has(authority)) { const request = this._resolveAuthorityRequests.get(authority)!; // Avoid that this error makes it to telemetry - request.reject(errors.ErrorNoTelemetry.fromError(err)); + request.error(errors.ErrorNoTelemetry.fromError(err)); } } @@ -128,8 +105,8 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot _setCanonicalURIProvider(provider: (uri: URI) => Promise): void { this._canonicalURIProvider = provider; - this._canonicalURIRequests.forEach((value) => { - this._canonicalURIProvider!(value.input).then((uri) => value.resolve(uri), (err) => value.reject(err)); + this._canonicalURIRequests.forEach((value, key) => { + this._canonicalURIProvider!(URI.parse(key)).then((uri) => value.complete(uri), (err) => value.error(err)); }); } } diff --git a/src/vs/platform/remote/node/nodeSocketFactory.ts b/src/vs/platform/remote/node/nodeSocketFactory.ts index 8e859bcd2f6..06d2e52f684 100644 --- a/src/vs/platform/remote/node/nodeSocketFactory.ts +++ b/src/vs/platform/remote/node/nodeSocketFactory.ts @@ -5,29 +5,18 @@ import * as net from 'net'; import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { makeRawSocketHeaders } from 'vs/platform/remote/common/managedSocket'; import { IConnectCallback, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; +import { WebSocketMessagingPassing } from 'vs/platform/remote/common/remoteAuthorityResolver'; -export const nodeSocketFactory = new class implements ISocketFactory { - connect(host: string, port: number, path: string, query: string, debugLabel: string, callback: IConnectCallback): void { +export const nodeSocketFactory = new class implements ISocketFactory { + connect({ host, port }: WebSocketMessagingPassing, path: string, query: string, debugLabel: 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'); - - const headers = [ - `GET ws://${/:/.test(host) ? `[${host}]` : host}:${port}${path}?${query}&skipWebSocketFrames=true HTTP/1.1`, - `Connection: Upgrade`, - `Upgrade: websocket`, - `Sec-WebSocket-Key: ${nonce}` - ]; - socket.write(headers.join('\r\n') + '\r\n\r\n'); + socket.write(makeRawSocketHeaders(path, query, debugLabel)); const onData = (data: Buffer) => { const strData = data.toString(); diff --git a/src/vs/platform/tunnel/common/tunnel.ts b/src/vs/platform/tunnel/common/tunnel.ts index 1e66f39f215..f00d2c5b81d 100644 --- a/src/vs/platform/tunnel/common/tunnel.ts +++ b/src/vs/platform/tunnel/common/tunnel.ts @@ -110,7 +110,7 @@ export interface ITunnel { export interface ISharedTunnelsService { readonly _serviceBrand: undefined; - openTunnel(authority: string, addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localHost: string, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise | undefined; + openTunnel(authority: string, addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localHost: string, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise | undefined; } export interface ITunnelService { @@ -126,7 +126,7 @@ export interface ITunnelService { readonly onAddedTunnelProvider: Event; canTunnel(uri: URI): boolean; - openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localHost?: string, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise | undefined; + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localHost?: string, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise | undefined; getExistingTunnel(remoteHost: string, remotePort: number): Promise; setEnvironmentTunnel(remoteHost: string, remotePort: number, localAddress: string, privacy: string, protocol: string): void; closeTunnel(remoteHost: string, remotePort: number): Promise; diff --git a/src/vs/platform/tunnel/node/tunnelService.ts b/src/vs/platform/tunnel/node/tunnelService.ts index aa941b067e3..be0182819a6 100644 --- a/src/vs/platform/tunnel/node/tunnelService.ts +++ b/src/vs/platform/tunnel/node/tunnelService.ts @@ -7,17 +7,17 @@ import * as net from 'net'; import * as os from 'os'; import { BROWSER_RESTRICTED_PORTS, findFreePortFaster } from 'vs/base/node/ports'; import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; -import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; import { Barrier } from 'vs/base/common/async'; import { Disposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; -import { connectRemoteAgentTunnel, IAddressProvider, IConnectionOptions, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; +import { connectRemoteAgentTunnel, IAddressProvider, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection'; import { AbstractTunnelService, isAllInterfaces, ISharedTunnelsService as ISharedTunnelsService, isLocalhost, isPortPrivileged, ITunnelService, RemoteTunnel, TunnelPrivacyId } from 'vs/platform/tunnel/common/tunnel'; import { ISignService } from 'vs/platform/sign/common/sign'; import { OS } from 'vs/base/common/platform'; +import { IRemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection'; async function createRemoteTunnel(options: IConnectionOptions, defaultTunnelHost: string, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise { let readyTunnel: NodeRemoteTunnel | undefined; @@ -155,7 +155,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { export class BaseTunnelService extends AbstractTunnelService { public constructor( - private readonly socketFactory: ISocketFactory, + @IRemoteSocketFactoryCollection private readonly socketFactories: IRemoteSocketFactoryCollection, @ILogService logService: ILogService, @ISignService private readonly signService: ISignService, @IProductService private readonly productService: IProductService, @@ -179,32 +179,40 @@ export class BaseTunnelService extends AbstractTunnelService { return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol); } else { this.logService.trace(`ForwardedPorts: (TunnelService) Creating tunnel without provider ${remoteHost}:${remotePort} on local port ${localPort}.`); - const options: IConnectionOptions = { - commit: this.productService.commit, - quality: this.productService.quality, - socketFactory: this.socketFactory, - addressProvider, - signService: this.signService, - logService: this.logService, - ipcLogger: null - }; + return addressProvider.getAddress().then(address => { + const socketFactory = this.socketFactories.create(address.connectTo); + if (!socketFactory) { + throw new Error(`No socket factory found for ${address.connectTo}`); + } - const tunnel = createRemoteTunnel(options, localHost, remoteHost, remotePort, localPort); - this.logService.trace('ForwardedPorts: (TunnelService) Tunnel created without provider.'); - this.addTunnelToMap(remoteHost, remotePort, tunnel); - return tunnel; + const options: IConnectionOptions = { + commit: this.productService.commit, + quality: this.productService.quality, + socketFactory, + addressProvider, + signService: this.signService, + logService: this.logService, + ipcLogger: null + }; + + const tunnel = createRemoteTunnel(options, localHost, remoteHost, remotePort, localPort); + this.logService.trace('ForwardedPorts: (TunnelService) Tunnel created without provider.'); + this.addTunnelToMap(remoteHost, remotePort, tunnel); + return tunnel; + }); } } } export class TunnelService extends BaseTunnelService { public constructor( + @IRemoteSocketFactoryCollection socketFactories: IRemoteSocketFactoryCollection, @ILogService logService: ILogService, @ISignService signService: ISignService, @IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService ) { - super(nodeSocketFactory, logService, signService, productService, configurationService); + super(socketFactories, logService, signService, productService, configurationService); } } @@ -213,6 +221,7 @@ export class SharedTunnelsService extends Disposable implements ISharedTunnelsSe private readonly _tunnelServices: Map = new Map(); public constructor( + @IRemoteSocketFactoryCollection protected readonly socketFactories: IRemoteSocketFactoryCollection, @ILogService protected readonly logService: ILogService, @IProductService private readonly productService: IProductService, @ISignService private readonly signService: ISignService, @@ -224,7 +233,7 @@ export class SharedTunnelsService extends Disposable implements ISharedTunnelsSe async openTunnel(authority: string, addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localHost: string, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise { this.logService.trace(`ForwardedPorts: (SharedTunnelService) openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`); if (!this._tunnelServices.has(authority)) { - const tunnelService = new TunnelService(this.logService, this.signService, this.productService, this.configurationService); + const tunnelService = new TunnelService(this.socketFactories, this.logService, this.signService, this.productService, this.configurationService); this._register(tunnelService); this._tunnelServices.set(authority, tunnelService); tunnelService.onTunnelClosed(async () => { diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index d822db420e5..7e107299f50 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -16,7 +16,7 @@ import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensio import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteConnectionData, MessagePassingType } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ExtHostContext, ExtHostExtensionServiceShape, MainContext, MainThreadExtensionServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -25,7 +25,7 @@ import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/exten import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { IExtensionHostProxy, IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; import { ActivationKind, ExtensionActivationReason, IExtensionService, IInternalExtensionService, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions'; -import { extHostNamedCustomer, IExtHostContext, IInternalExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { extHostNamedCustomer, IExtHostContext, IInternalExtHostContext, IManagedSocketCallbacks } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; @@ -34,6 +34,7 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha private readonly _extensionHostKind: ExtensionHostKind; private readonly _internalExtensionService: IInternalExtensionService; + private readonly _managedSocketCallbacks: IManagedSocketCallbacks; constructor( extHostContext: IExtHostContext, @@ -50,6 +51,7 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha const internalExtHostContext = (extHostContext); this._internalExtensionService = internalExtHostContext.internalExtensionService; + this._managedSocketCallbacks = internalExtHostContext.managedSocketCallbacks; internalExtHostContext._setExtensionHostProxy( new ExtensionHostProxy(extHostContext.getProxy(ExtHostContext.ExtHostExtensionService)) ); @@ -59,6 +61,18 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha public dispose(): void { } + $onDidRemoteSocketHaveData(id: number, data: VSBuffer): void { + this._managedSocketCallbacks.onDidRemoteSocketHaveData(id, data); + } + + $onDidRemoteSocketClose(id: number, error: string | undefined): void { + this._managedSocketCallbacks.onDidRemoteSocketClose(id, error ? new Error(error) : undefined); + } + + $onDidRemoteSocketEnd(id: number): void { + this._managedSocketCallbacks.onDidRemoteSocketEnd(id); + } + $getExtension(extensionId: string) { return this._extensionService.getExtension(extensionId); } @@ -199,8 +213,15 @@ class ExtensionHostProxy implements IExtensionHostProxy { private readonly _actual: ExtHostExtensionServiceShape ) { } - resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise { - return this._actual.$resolveAuthority(remoteAuthority, resolveAttempt); + async resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise { + const resolved = await this._actual.$resolveAuthority(remoteAuthority, resolveAttempt); + if (resolved.type === 'ok') { + resolved.value.authority.toString = function () { + return this.messaging.type === MessagePassingType.Managed ? `ManagedSocket#${this.messaging.id}` : `${this.messaging.host}:${this.messaging.type}`; + }; + } + + return resolved; } async getCanonicalURI(remoteAuthority: string, uri: URI): Promise { const uriComponents = await this._actual.$getCanonicalURI(remoteAuthority, uri); @@ -236,4 +257,16 @@ class ExtensionHostProxy implements IExtensionHostProxy { test_down(size: number): Promise { return this._actual.$test_down(size); } + openRemoteSocket(factoryId: number): Promise { + return this._actual.$openRemoteSocket(factoryId); + } + remoteSocketWrite(socketId: number, buffer: VSBuffer): void { + return this._actual.$remoteSocketWrite(socketId, buffer); + } + remoteSocketEnd(socketId: number): void { + return this._actual.$remoteSocketEnd(socketId); + } + remoteSocketDrain(socketId: number): Promise { + return this._actual.$remoteSocketDrain(socketId); + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 6bb3af739e5..0e45d2f6d4f 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1420,6 +1420,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InlayHintKind: extHostTypes.InlayHintKind, RemoteAuthorityResolverError: extHostTypes.RemoteAuthorityResolverError, ResolvedAuthority: extHostTypes.ResolvedAuthority, + ManagedResolvedAuthority: extHostTypes.ManagedResolvedAuthority, SourceControlInputBoxValidationType: extHostTypes.SourceControlInputBoxValidationType, ExtensionRuntime: extHostTypes.ExtensionRuntime, TimelineItem: extHostTypes.TimelineItem, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 752767d7f0d..3005830d937 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1237,6 +1237,10 @@ export interface MainThreadExtensionServiceShape extends IDisposable { $onExtensionRuntimeError(extensionId: ExtensionIdentifier, error: SerializedError): void; $setPerformanceMarks(marks: performance.PerformanceMark[]): Promise; $asBrowserUri(uri: UriComponents): Promise; + + $onDidRemoteSocketHaveData(id: number, data: VSBuffer): void; + $onDidRemoteSocketClose(id: number, error: string | undefined): void; + $onDidRemoteSocketEnd(id: number): void; } export interface SCMProviderFeatures { @@ -1593,6 +1597,11 @@ export interface ExtHostExtensionServiceShape { $test_latency(n: number): Promise; $test_up(b: VSBuffer): Promise; $test_down(size: number): Promise; + + $openRemoteSocket(factoryId: number): Promise; + $remoteSocketWrite(socketId: number, buffer: VSBuffer): void; + $remoteSocketEnd(socketId: number): void; + $remoteSocketDrain(socketId: number): Promise; } export interface FileSystemEvents { diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index afe2939e5b8..ae134bad42b 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -8,7 +8,7 @@ import * as path from 'vs/base/common/path'; import * as performance from 'vs/base/common/performance'; import { originalFSPath, joinPath, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { asPromise, Barrier, IntervalTimer, timeout } from 'vs/base/common/async'; -import { dispose, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; @@ -25,8 +25,8 @@ import type * as vscode from 'vscode'; import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { VSBuffer } from 'vs/base/common/buffer'; import { ExtensionGlobalMemento, ExtensionMemento } from 'vs/workbench/api/common/extHostMemento'; -import { RemoteAuthorityResolverError, ExtensionKind, ExtensionMode, ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; -import { ResolvedAuthority, ResolvedOptions, RemoteAuthorityResolverErrorCode, IRemoteConnectionData, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { RemoteAuthorityResolverError, ExtensionKind, ExtensionMode, ExtensionRuntime, ResolvedAuthority as ExtHostResolvedAuthority } from 'vs/workbench/api/common/extHostTypes'; +import { ResolvedAuthority, ResolvedOptions, RemoteAuthorityResolverErrorCode, IRemoteConnectionData, getRemoteAuthorityPrefix, TunnelInformation, MessagePassingType } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; @@ -79,6 +79,8 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme readonly _serviceBrand: undefined; + private static remoteSocketIdCounter = 0; + abstract readonly extensionRuntime: ExtensionRuntime; private readonly _onDidChangeRemoteConnectionData = this._register(new Emitter()); @@ -118,6 +120,11 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme private _started: boolean; private _isTerminating: boolean = false; private _remoteConnectionData: IRemoteConnectionData | null; + private readonly _managedSocketFactories: Map Thenable>; + private readonly _managedRemoteSockets: Map; constructor( @IInstantiationService instaService: IInstantiationService, @@ -191,6 +198,8 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._resolvers = Object.create(null); this._started = false; this._remoteConnectionData = this._initData.remote.connectionData; + this._managedSocketFactories = new Map(); + this._managedRemoteSockets = new Map(); } public getRemoteConnectionData(): IRemoteConnectionData | null { @@ -822,30 +831,44 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const result = await resolver.resolve(remoteAuthority, { resolveAttempt }); performance.mark(`code/extHost/didResolveAuthorityOK/${authorityPrefix}`); intervalLogger.dispose(); - logInfo(`returned ${result.host}:${result.port}`); + + const tunnelInformation: TunnelInformation = { + environmentTunnels: result.environmentTunnels, + features: result.tunnelFeatures + }; // Split merged API result into separate authority/options - const authority: ResolvedAuthority = { - authority: remoteAuthority, - host: result.host, - port: result.port, - connectionToken: result.connectionToken - }; const options: ResolvedOptions = { extensionHostEnv: result.extensionHostEnv, isTrusted: result.isTrusted, authenticationSession: result.authenticationSessionForInitializingExtensions ? { id: result.authenticationSessionForInitializingExtensions.id, providerId: result.authenticationSessionForInitializingExtensions.providerId } : undefined }; + logInfo(`returned ${result instanceof ExtHostResolvedAuthority ? `${result.host}:${result.port}` : 'managed authority'}`); + + let authority: ResolvedAuthority; + if (result instanceof ExtHostResolvedAuthority) { + authority = { + authority: remoteAuthority, + messaging: { type: MessagePassingType.WebSocket, host: result.host, port: result.port }, + connectionToken: result.connectionToken + }; + } else { + const factoryId = AbstractExtHostExtensionService.remoteSocketIdCounter++; + this._managedSocketFactories.set(factoryId, result.makeConnection); + authority = { + authority: remoteAuthority, + messaging: { type: MessagePassingType.Managed, id: factoryId }, + connectionToken: result.connectionToken + }; + } + return { type: 'ok', value: { authority, options, - tunnelInformation: { - environmentTunnels: result.environmentTunnels, - features: result.tunnelFeatures - } + tunnelInformation, } }; } catch (err) { @@ -866,6 +889,47 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } } + public async $openRemoteSocket(factoryId: number): Promise { + const factory = this._managedSocketFactories.get(factoryId); + if (!factory) { + throw new Error(`No socket factory with id ${factoryId}`); + } + + const id = AbstractExtHostExtensionService.remoteSocketIdCounter++; + const socket = await factory(); + const disposable = new DisposableStore(); + + this._managedRemoteSockets.set(id, { object: socket, disposer: disposable }); + disposable.add(toDisposable(() => this._managedRemoteSockets.delete(id))); + disposable.add(socket.onDidEnd(() => { + this._mainThreadExtensionsProxy.$onDidRemoteSocketEnd(id); + disposable.dispose(); + })); + disposable.add(socket.onDidClose(e => { + this._mainThreadExtensionsProxy.$onDidRemoteSocketClose(id, e?.stack ?? e?.message); + disposable.dispose(); + })); + disposable.add(socket.onDidReceiveMessage(e => this._mainThreadExtensionsProxy.$onDidRemoteSocketHaveData(id, VSBuffer.wrap(e)))); + + return id; + } + + public $remoteSocketDrain(id: number): Promise { + return this._managedRemoteSockets.get(id)?.object.drainHandler?.() ?? Promise.resolve(); + } + + public $remoteSocketEnd(id: number): void { + const socket = this._managedRemoteSockets.get(id); + if (socket) { + socket.object.endHandler(); + socket.disposer.dispose(); + } + } + + public $remoteSocketWrite(id: number, buffer: VSBuffer): void { + this._managedRemoteSockets.get(id)?.object.dataHandler(buffer.buffer); + } + public async $getCanonicalURI(remoteAuthority: string, uriComponents: UriComponents): Promise { this._logService.info(`$getCanonicalURI invoked for authority (${getRemoteAuthorityPrefix(remoteAuthority)})`); diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index dbe449375e9..d5c91148172 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -479,6 +479,13 @@ export class Selection extends Range { } } +const validateConnectionToken = (connectionToken: string) => { + if (typeof connectionToken !== 'string' || connectionToken.length === 0 || !/^[0-9A-Za-z_\-]+$/.test(connectionToken)) { + throw illegalArgument('connectionToken'); + } +}; + + export class ResolvedAuthority { readonly host: string; readonly port: number; @@ -492,9 +499,7 @@ export class ResolvedAuthority { throw illegalArgument('port'); } if (typeof connectionToken !== 'undefined') { - if (typeof connectionToken !== 'string' || connectionToken.length === 0 || !/^[0-9A-Za-z_\-]+$/.test(connectionToken)) { - throw illegalArgument('connectionToken'); - } + validateConnectionToken(connectionToken); } this.host = host; this.port = Math.round(port); @@ -502,6 +507,14 @@ export class ResolvedAuthority { } } +export class ManagedResolvedAuthority { + constructor(public readonly makeConnection: () => Thenable, public readonly connectionToken?: string) { + if (typeof connectionToken !== 'undefined') { + validateConnectionToken(connectionToken); + } + } +} + export class RemoteAuthorityResolverError extends Error { static NotAvailable(message?: string, handled?: boolean): RemoteAuthorityResolverError { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 5625fb56d3b..13e8d1d26bc 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -18,7 +18,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import product from 'vs/platform/product/common/product'; import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteAgentService'; import { RemoteAuthorityResolverService } from 'vs/platform/remote/browser/remoteAuthorityResolverService'; -import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAuthorityResolverService, MessagePassingType } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -84,6 +84,8 @@ import { BrowserUserDataProfilesService } from 'vs/platform/userDataProfile/brow import { timeout } from 'vs/base/common/async'; import { windowLogId } from 'vs/workbench/services/log/common/logConstants'; import { LogService } from 'vs/platform/log/common/logService'; +import { IRemoteSocketFactoryCollection, RemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection'; +import { BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; export class BrowserMain extends Disposable { @@ -253,7 +255,8 @@ export class BrowserMain extends Disposable { // Remote const connectionToken = environmentService.options.connectionToken || getCookieValue(connectionTokenCookieName); - const remoteAuthorityResolverService = new RemoteAuthorityResolverService(connectionToken, this.configuration.resourceUriProvider, productService, logService); + const expectResolverExtension = !!environmentService.remoteAuthority?.includes('+') && !environmentService.options.webSocketFactory; + const remoteAuthorityResolverService = new RemoteAuthorityResolverService(!expectResolverExtension, connectionToken, this.configuration.resourceUriProvider, productService, logService); serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService); // Signing @@ -292,7 +295,10 @@ export class BrowserMain extends Disposable { serviceCollection.set(IUserDataProfileService, userDataProfileService); // Remote Agent - const remoteAgentService = this._register(new RemoteAgentService(this.configuration.webSocketFactory, userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService)); + const socketFactories = new RemoteSocketFactoryCollection(); + socketFactories.register(MessagePassingType.WebSocket, () => new BrowserSocketFactory(this.configuration.webSocketFactory)); + serviceCollection.set(IRemoteSocketFactoryCollection, socketFactories); + const remoteAgentService = this._register(new RemoteAgentService(socketFactories, userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService)); serviceCollection.set(IRemoteAgentService, remoteAgentService); await this.registerFileSystemProviders(environmentService, fileService, remoteAgentService, bufferLogger, logService, loggerService, logsPath); diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index ee19ad8de81..71e755072b1 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -782,7 +782,10 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD private async localLocalhost(id: string, origin: string) { const authority = this._environmentService.remoteAuthority; const resolveAuthority = authority ? await this._remoteAuthorityResolverService.resolveAuthority(authority) : undefined; - const redirect = resolveAuthority ? await this._portMappingManager.getRedirect(resolveAuthority.authority, origin) : undefined; + const redirect = resolveAuthority ? await this._portMappingManager.getRedirect({ + connectionToken: resolveAuthority.authority.connectionToken, + connectTo: resolveAuthority.authority.messaging, + }, origin) : undefined; return this._send('did-load-localhost', { id, origin, diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index eb348f8b08a..bba65653a0d 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -25,7 +25,7 @@ import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService'; import { SharedProcessService } from 'vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService'; import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-sandbox/remoteAuthorityResolverService'; -import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAuthorityResolverService, MessagePassingType } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -55,6 +55,8 @@ import { PolicyChannelClient } from 'vs/platform/policy/common/policyIpc'; import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy'; import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; +import { RemoteSocketFactoryCollection, IRemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection'; export class DesktopMain extends Disposable { @@ -236,7 +238,10 @@ export class DesktopMain extends Disposable { serviceCollection.set(IUserDataProfileService, userDataProfileService); // Remote Agent - const remoteAgentService = this._register(new RemoteAgentService(userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService)); + const socketFactories = new RemoteSocketFactoryCollection(); + socketFactories.register(MessagePassingType.WebSocket, () => new BrowserSocketFactory(null)); + serviceCollection.set(IRemoteSocketFactoryCollection, socketFactories); + const remoteAgentService = this._register(new RemoteAgentService(socketFactories, userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService)); serviceCollection.set(IRemoteAgentService, remoteAgentService); // Remote Files diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index cf6af9debb4..8432e212e74 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -842,7 +842,8 @@ export class NativeWindow extends Disposable { const remoteAuthority = this.environmentService.remoteAuthority; const addressProvider: IAddressProvider | undefined = remoteAuthority ? { getAddress: async (): Promise => { - return (await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority)).authority; + const { authority } = await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority); + return { connectTo: authority.messaging, connectionToken: authority.connectionToken }; } } : undefined; let tunnel = await this.tunnelService.getExistingTunnel(portMappingRequest.address, portMappingRequest.port); diff --git a/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts index 3574a026ce5..058f5a99770 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts @@ -113,7 +113,7 @@ suite('ConfigurationEditing', () => { const uriIdentityService = new UriIdentityService(fileService); const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService)); userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService); - const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService, null)); + const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService)); disposables.add(fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, logService)))); instantiationService.stub(IFileService, fileService); instantiationService.stub(IRemoteAgentService, remoteAgentService); diff --git a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index ab13f504d76..43db57ad590 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -51,6 +51,7 @@ import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { TasksSchemaProperties } from 'vs/workbench/contrib/tasks/common/tasks'; +import { RemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection'; function convertToWorkspacePayload(folder: URI): ISingleFolderWorkspaceIdentifier { return { @@ -89,7 +90,7 @@ suite('WorkspaceContextService - Folder', () => { const uriIdentityService = new UriIdentityService(fileService); const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService); const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService); - testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(null, userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryCollection(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); }); @@ -132,7 +133,7 @@ suite('WorkspaceContextService - Folder', () => { const uriIdentityService = new UriIdentityService(fileService); const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService); const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService); - const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(null, userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); + const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryCollection(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); const actual = testObject.getWorkspaceFolder(joinPath(folder, 'a')); @@ -155,7 +156,7 @@ suite('WorkspaceContextService - Folder', () => { const uriIdentityService = new UriIdentityService(fileService); const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService); const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService); - const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(null, userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); + const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryCollection(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); @@ -199,7 +200,7 @@ suite('WorkspaceContextService - Workspace', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); const environmentService = TestEnvironmentService; - const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService, null)); + const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService)); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const uriIdentityService = new UriIdentityService(fileService); @@ -259,7 +260,7 @@ suite('WorkspaceContextService - Workspace Editing', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); const environmentService = TestEnvironmentService; - const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); + const remoteAgentService = instantiationService.createInstance(RemoteAgentService); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const uriIdentityService = new UriIdentityService(fileService); @@ -505,7 +506,7 @@ suite('WorkspaceService - Initialization', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); environmentService = TestEnvironmentService; - const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); + const remoteAgentService = instantiationService.createInstance(RemoteAgentService); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const uriIdentityService = new UriIdentityService(fileService); @@ -766,7 +767,7 @@ suite('WorkspaceConfigurationService - Folder', () => { instantiationService = workbenchInstantiationService(undefined, disposables); environmentService = TestEnvironmentService; environmentService.policyFile = joinPath(folder, 'policies.json'); - const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); + const remoteAgentService = instantiationService.createInstance(RemoteAgentService); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const uriIdentityService = new UriIdentityService(fileService); @@ -1562,7 +1563,7 @@ suite('WorkspaceConfigurationService - Profiles', () => { instantiationService = workbenchInstantiationService(undefined, disposables); environmentService = TestEnvironmentService; environmentService.policyFile = joinPath(folder, 'policies.json'); - const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); + const remoteAgentService = instantiationService.createInstance(RemoteAgentService); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const uriIdentityService = new UriIdentityService(fileService); @@ -1801,7 +1802,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); environmentService = TestEnvironmentService; - const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); + const remoteAgentService = instantiationService.createInstance(RemoteAgentService); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const uriIdentityService = new UriIdentityService(fileService); diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 91a8c0fb57b..e01650924e9 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -170,7 +170,7 @@ class BrowserExtensionHostFactory implements IExtensionHostFactory { case ExtensionHostKind.Remote: { const remoteAgentConnection = this._remoteAgentService.getConnection(); if (remoteAgentConnection) { - return this._instantiationService.createInstance(RemoteExtensionHost, runningLocation, this._createRemoteExtensionHostDataProvider(runningLocations, remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory); + return this._instantiationService.createInstance(RemoteExtensionHost, runningLocation, this._createRemoteExtensionHostDataProvider(runningLocations, remoteAgentConnection.remoteAuthority)); } return null; } diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index b7db6b7c552..f97648c1aac 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -938,7 +938,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx }, _onExtensionRuntimeError: (extensionId: ExtensionIdentifier, err: Error): void => { return this._onExtensionRuntimeError(extensionId, err); - } + }, }; } diff --git a/src/vs/workbench/services/extensions/common/extHostCustomers.ts b/src/vs/workbench/services/extensions/common/extHostCustomers.ts index 0d3104fcded..34123e72b15 100644 --- a/src/vs/workbench/services/extensions/common/extHostCustomers.ts +++ b/src/vs/workbench/services/extensions/common/extHostCustomers.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { VSBuffer } from 'vs/base/common/buffer'; import { IDisposable } from 'vs/base/common/lifecycle'; import { BrandedService, IConstructorSignature } from 'vs/platform/instantiation/common/instantiation'; import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensionHostKind'; @@ -15,8 +16,15 @@ export interface IExtHostContext extends IRPCProtocol { readonly extensionHostKind: ExtensionHostKind; } +export interface IManagedSocketCallbacks { + onDidRemoteSocketHaveData(id: number, data: VSBuffer): void; + onDidRemoteSocketEnd(id: number): void; + onDidRemoteSocketClose(id: number, error: Error | undefined): void; +} + export interface IInternalExtHostContext extends IExtHostContext { readonly internalExtensionService: IInternalExtensionService; + readonly managedSocketCallbacks: IManagedSocketCallbacks; _setExtensionHostProxy(extensionHostProxy: IExtensionHostProxy): void; _setAllMainProxyIdentifiers(mainProxyIdentifiers: ProxyIdentifier[]): void; } diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 7a1d76d77cc..deb26526677 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -11,13 +11,15 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI } from 'vs/base/common/uri'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; +import { SocketCloseEvent, SocketCloseEventType } from 'vs/base/parts/ipc/common/ipc.net'; import * as nls from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { RemoteAuthorityResolverErrorCode, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { ManagedMessagingPassing, MessagePassingType, RemoteAuthorityResolverErrorCode, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -29,6 +31,7 @@ import { ExtensionRunningLocation } from 'vs/workbench/services/extensions/commo import { ActivationKind, ExtensionActivationReason, ExtensionHostExtensions, ExtensionHostStartup, IExtensionHost, IInternalExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { Proxied, ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { IRPCProtocolLogger, RPCProtocol, RequestInitiator, ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol'; +import { ManagedSocket } from 'vs/workbench/services/remote/common/managedSocket'; // Enable to see detailed message communication between window and extension host const LOG_EXTENSION_HOST_COMMUNICATION = false; @@ -85,6 +88,13 @@ type ExtensionHostStartupEvent = { errorStack?: string; }; + +interface RemoteSocketHalf { + onData: Emitter; + onClose: Emitter; + onEnd: Emitter; +} + class ExtensionHostManager extends Disposable implements IExtensionHostManager { public readonly onDidExit: Event<[number, string | null]>; @@ -102,6 +112,7 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { private readonly _extensionHost: IExtensionHost; private _proxy: Promise | null; private _hasStarted = false; + private readonly _remoteSockets = new Map(); public get kind(): ExtensionHostKind { return this._extensionHost.runningLocation.kind; @@ -115,6 +126,7 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { extensionHost: IExtensionHost, initialActivationEvents: string[], private readonly _internalExtensionService: IInternalExtensionService, + @IRemoteSocketFactoryCollection private readonly _remoteSocketFactoryCollection: IRemoteSocketFactoryCollection, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @@ -287,6 +299,23 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { //#region internal internalExtensionService: this._internalExtensionService, + managedSocketCallbacks: { + onDidRemoteSocketHaveData: (id, data) => { + this._remoteSockets.get(id)?.onData.fire(data); + }, + onDidRemoteSocketEnd: id => { + this._remoteSockets.get(id)?.onEnd.fire(); + this._remoteSockets.delete(id); + }, + onDidRemoteSocketClose: (id, error) => { + this._remoteSockets.get(id)?.onClose.fire({ + type: SocketCloseEventType.NodeSocketCloseEvent, + error, + hadError: !!error + }); + this._remoteSockets.delete(id); + }, + }, _setExtensionHostProxy: (value: IExtensionHostProxy): void => { extensionHostProxy = value; }, @@ -409,7 +438,10 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { const resolverResult = await proxy.resolveAuthority(remoteAuthority, resolveAttempt); intervalLogger.dispose(); if (resolverResult.type === 'ok') { - logInfo(`returned ${resolverResult.value.authority.host}:${resolverResult.value.authority.port}`); + logInfo(`returned ${resolverResult.value.authority}`); + if (resolverResult.value.authority.messaging.type === MessagePassingType.Managed) { + this.registerManagedSocketFactory(resolverResult.value.authority.messaging, proxy); + } } else { logError(`returned an error`, resolverResult.error); } @@ -428,6 +460,38 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { } } + private registerManagedSocketFactory(messaging: ManagedMessagingPassing, proxy: IExtensionHostProxy) { + this._remoteSocketFactoryCollection.register(MessagePassingType.Managed, resolved => { + if (resolved.id !== messaging.id) { + return undefined; + } + + return { + connect: ({ id: factoryId }, path, query, debugLabel, callback) => { + proxy.openRemoteSocket(factoryId).then(socketId => { + const half: RemoteSocketHalf = { + onClose: new Emitter(), + onData: new Emitter(), + onEnd: new Emitter(), + }; + this._remoteSockets.set(socketId, half); + + ManagedSocket.connect(socketId, proxy, path, query, debugLabel, half) + .then( + socket => { + socket.onDidDispose(() => this._remoteSockets.delete(socketId)); + callback(undefined, socket); + }, + err => { + this._remoteSockets.delete(socketId); + callback(err, undefined); + }); + }).catch(err => callback(err, undefined)); + }, + }; + }); + } + public async getCanonicalURI(remoteAuthority: string, uri: URI): Promise { const proxy = await this._proxy; if (!proxy) { diff --git a/src/vs/workbench/services/extensions/common/extensionHostProxy.ts b/src/vs/workbench/services/extensions/common/extensionHostProxy.ts index e1c962a8b40..b6c8a8d24cc 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProxy.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProxy.ts @@ -42,4 +42,9 @@ export interface IExtensionHostProxy { test_latency(n: number): Promise; test_up(b: VSBuffer): Promise; test_down(size: number): Promise; + + openRemoteSocket(factoryId: number): Promise; + remoteSocketWrite(socketId: number, buffer: VSBuffer): void; + remoteSocketEnd(socketId: number): void; + remoteSocketDrain(socketId: number): Promise; } diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index 3d929ec2b21..9a13531cd01 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -16,8 +16,9 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService, ILoggerService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IConnectionOptions, IRemoteExtensionHostStartParams, ISocketFactory, connectRemoteAgentExtensionHost } from 'vs/platform/remote/common/remoteAgentConnection'; +import { IConnectionOptions, IRemoteExtensionHostStartParams, connectRemoteAgentExtensionHost } from 'vs/platform/remote/common/remoteAgentConnection'; import { IRemoteAuthorityResolverService, IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection'; import { ISignService } from 'vs/platform/sign/common/sign'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isLoggingOnly } from 'vs/platform/telemetry/common/telemetryUtils'; @@ -61,7 +62,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { constructor( public readonly runningLocation: RemoteRunningLocation, private readonly _initDataProvider: IRemoteExtensionHostDataProvider, - private readonly _socketFactory: ISocketFactory, + @IRemoteSocketFactoryCollection private readonly socketFactories: IRemoteSocketFactoryCollection, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @@ -84,21 +85,26 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { } public start(): Promise { - const options: IConnectionOptions = { - commit: this._productService.commit, - quality: this._productService.quality, - socketFactory: this._socketFactory, - addressProvider: { - getAddress: async () => { - const { authority } = await this.remoteAuthorityResolverService.resolveAuthority(this._initDataProvider.remoteAuthority); - return { host: authority.host, port: authority.port, connectionToken: authority.connectionToken }; - } - }, - signService: this._signService, - logService: this._logService, - ipcLogger: null - }; return this.remoteAuthorityResolverService.resolveAuthority(this._initDataProvider.remoteAuthority).then((resolverResult) => { + const socketFactory = this.socketFactories.create(resolverResult.authority.messaging); + if (!socketFactory) { + throw new Error('No socket factory found for remote authority'); + } + + const options: IConnectionOptions = { + commit: this._productService.commit, + quality: this._productService.quality, + socketFactory, + addressProvider: { + getAddress: async () => { + const { authority } = await this.remoteAuthorityResolverService.resolveAuthority(this._initDataProvider.remoteAuthority); + return { connectTo: authority.messaging, connectionToken: authority.connectionToken }; + } + }, + signService: this._signService, + logService: this._logService, + ipcLogger: null + }; const startParams: IRemoteExtensionHostStartParams = { language: platform.language, diff --git a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts index c801603d6de..f6d4912de61 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts @@ -29,7 +29,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; -import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode, ResolverResult, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAuthorityResolverService, MessagePassingType, RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode, ResolverResult, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; import { getRemoteName, parseAuthorityWithPort } from 'vs/platform/remote/common/remoteHosts'; import { updateProxyConfigurationsScope } from 'vs/platform/request/common/request'; @@ -277,15 +277,22 @@ export class NativeExtensionService extends AbstractExtensionService implements const authorityPlusIndex = remoteAuthority.indexOf('+'); if (authorityPlusIndex === -1) { // This authority does not need to be resolved, simply parse the port number - const { host, port } = parseAuthorityWithPort(remoteAuthority); - return { - authority: { - authority: remoteAuthority, - host, - port, - connectionToken: undefined - } - }; + try { + const { host, port } = parseAuthorityWithPort(remoteAuthority); + return { + authority: { + authority: remoteAuthority, + messaging: { + type: MessagePassingType.WebSocket, + host, + port + }, + connectionToken: undefined + } + }; + } catch { + // continue + } } const localProcessExtensionHosts = this._getExtensionHostManagers(ExtensionHostKind.LocalProcess); @@ -391,7 +398,7 @@ export class NativeExtensionService extends AbstractExtensionService implements performance.mark(`code/willResolveAuthority/${authorityPrefix}`); const result = await this._resolveAuthority(remoteAuthority); performance.mark(`code/didResolveAuthorityOK/${authorityPrefix}`); - this._logService.info(`resolveAuthority(${authorityPrefix}) returned '${result.authority.host}:${result.authority.port}' after ${sw.elapsed()} ms`); + this._logService.info(`resolveAuthority(${authorityPrefix}) returned '${result.authority}' after ${sw.elapsed()} ms`); return result; } catch (err) { performance.mark(`code/didResolveAuthorityError/${authorityPrefix}`); @@ -631,7 +638,7 @@ class NativeExtensionHostFactory implements IExtensionHostFactory { case ExtensionHostKind.Remote: { const remoteAgentConnection = this._remoteAgentService.getConnection(); if (remoteAgentConnection) { - return this._instantiationService.createInstance(RemoteExtensionHost, runningLocation, this._createRemoteExtensionHostDataProvider(runningLocations, remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory); + return this._instantiationService.createInstance(RemoteExtensionHost, runningLocation, this._createRemoteExtensionHostDataProvider(runningLocations, remoteAgentConnection.remoteAuthority)); } return null; } diff --git a/src/vs/workbench/services/remote/browser/remoteAgentService.ts b/src/vs/workbench/services/remote/browser/remoteAgentService.ts index 3cc54d49eea..8e4e4ec1b45 100644 --- a/src/vs/workbench/services/remote/browser/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/browser/remoteAgentService.ts @@ -9,7 +9,6 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { AbstractRemoteAgentService } from 'vs/workbench/services/remote/common/abstractRemoteAgentService'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IWebSocketFactory, BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; import { ISignService } from 'vs/platform/sign/common/sign'; import { ILogService } from 'vs/platform/log/common/log'; import { Severity } from 'vs/platform/notification/common/notification'; @@ -19,11 +18,12 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } f import { IHostService } from 'vs/workbench/services/host/browser/host'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IRemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection'; export class RemoteAgentService extends AbstractRemoteAgentService implements IRemoteAgentService { constructor( - webSocketFactory: IWebSocketFactory | null | undefined, + @IRemoteSocketFactoryCollection socketFactories: IRemoteSocketFactoryCollection, @IUserDataProfileService userDataProfileService: IUserDataProfileService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IProductService productService: IProductService, @@ -31,7 +31,7 @@ export class RemoteAgentService extends AbstractRemoteAgentService implements IR @ISignService signService: ISignService, @ILogService logService: ILogService ) { - super(new BrowserSocketFactory(webSocketFactory), userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService); + super(socketFactories, userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService); } } diff --git a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts index 2b0ecd843a1..9a80b63ff7e 100644 --- a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts @@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IChannel, IServerChannel, getDelayedChannel, IPCLogger } from 'vs/base/parts/ipc/common/ipc'; import { Client } from 'vs/base/parts/ipc/common/ipc.net'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { connectRemoteAgentManagement, IConnectionOptions, ISocketFactory, ManagementPersistentConnection, PersistentConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection'; +import { connectRemoteAgentManagement, IConnectionOptions, ManagementPersistentConnection, PersistentConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection'; import { IExtensionHostExitInfo, IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { RemoteAgentConnectionContext, IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; @@ -19,17 +19,17 @@ import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { IProductService } from 'vs/platform/product/common/productService'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IRemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection'; export abstract class AbstractRemoteAgentService extends Disposable implements IRemoteAgentService { declare readonly _serviceBrand: undefined; - public readonly socketFactory: ISocketFactory; private readonly _connection: IRemoteAgentConnection | null; private _environment: Promise | null; constructor( - socketFactory: ISocketFactory, + @IRemoteSocketFactoryCollection private readonly socketFactories: IRemoteSocketFactoryCollection, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IWorkbenchEnvironmentService protected readonly _environmentService: IWorkbenchEnvironmentService, @IProductService productService: IProductService, @@ -38,9 +38,8 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I @ILogService logService: ILogService ) { super(); - this.socketFactory = socketFactory; if (this._environmentService.remoteAuthority) { - this._connection = this._register(new RemoteAgentConnection(this._environmentService.remoteAuthority, productService.commit, productService.quality, this.socketFactory, this._remoteAuthorityResolverService, signService, logService)); + this._connection = this._register(new RemoteAgentConnection(this._environmentService.remoteAuthority, productService.commit, productService.quality, this.socketFactories, this._remoteAuthorityResolverService, signService, logService)); } else { this._connection = null; } @@ -150,7 +149,7 @@ class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection remoteAuthority: string, private readonly _commit: string | undefined, private readonly _quality: string | undefined, - private readonly _socketFactory: ISocketFactory, + private readonly _socketFactories: IRemoteSocketFactoryCollection, private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, private readonly _signService: ISignService, private readonly _logService: ILogService @@ -193,28 +192,35 @@ class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection private async _createConnection(): Promise> { let firstCall = true; - const options: IConnectionOptions = { - commit: this._commit, - quality: this._quality, - socketFactory: this._socketFactory, - addressProvider: { - getAddress: async () => { - if (firstCall) { - firstCall = false; - } else { - this._onReconnecting.fire(undefined); - } - const { authority } = await this._remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority); - return { host: authority.host, port: authority.port, connectionToken: authority.connectionToken }; - } - }, - signService: this._signService, - logService: this._logService, - ipcLogger: false ? new IPCLogger(`Local \u2192 Remote`, `Remote \u2192 Local`) : null - }; let connection: ManagementPersistentConnection; const start = Date.now(); try { + const { authority } = await this._remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority); + const socketFactory = this._socketFactories.create(authority.messaging); + if (!socketFactory) { + throw new Error(`No socket factory found for ${authority}`); + } + + const options: IConnectionOptions = { + commit: this._commit, + quality: this._quality, + socketFactory, + addressProvider: { + getAddress: async () => { + if (firstCall) { + firstCall = false; + } else { + this._onReconnecting.fire(undefined); + } + const { authority } = await this._remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority); + return { connectTo: authority.messaging, connectionToken: authority.connectionToken }; + } + }, + signService: this._signService, + logService: this._logService, + ipcLogger: false ? new IPCLogger(`Local \u2192 Remote`, `Remote \u2192 Local`) : null + }; + connection = this._register(await connectRemoteAgentManagement(options, this.remoteAuthority, `renderer`)); } finally { this._initialConnectionMs = Date.now() - start; diff --git a/src/vs/workbench/services/remote/common/managedSocket.ts b/src/vs/workbench/services/remote/common/managedSocket.ts new file mode 100644 index 00000000000..4ff860a8f44 --- /dev/null +++ b/src/vs/workbench/services/remote/common/managedSocket.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ISocket, SocketCloseEvent, SocketDiagnostics, SocketDiagnosticsEventType } from 'vs/base/parts/ipc/common/ipc.net'; +import { makeRawSocketHeaders, socketRawEndHeaderSequence } from 'vs/platform/remote/common/managedSocket'; +import { IExtensionHostProxy } from 'vs/workbench/services/extensions/common/extensionHostProxy'; + +export class ManagedSocket extends Disposable implements ISocket { + public static connect( + socketId: number, + proxy: IExtensionHostProxy, + path: string, query: string, debugLabel: string, + + half: { + onClose: Emitter; + onData: Emitter; + onEnd: Emitter; + } + ): Promise { + const socket = new ManagedSocket(socketId, proxy, debugLabel, half.onClose, half.onData, half.onEnd); + + socket.write(VSBuffer.fromString(makeRawSocketHeaders(path, query, debugLabel))); + + const d = new DisposableStore(); + return new Promise((resolve, reject) => { + d.add(socket.onData(d => { + if (d.indexOf(socketRawEndHeaderSequence) !== -1) { + resolve(socket); + } + })); + + d.add(socket.onClose(err => reject(err ?? new Error('socket closed')))); + d.add(socket.onEnd(() => reject(new Error('socket ended')))); + }).finally(() => d.dispose()); + } + + public onData: Event; + public onClose: Event; + public onEnd: Event; + + private readonly didDisposeEmitter = this._register(new Emitter()); + public onDidDispose = this.didDisposeEmitter.event; + + private ended = false; + + private constructor( + private readonly socketId: number, + private readonly proxy: IExtensionHostProxy, + private readonly debugLabel: string, + onCloseEmitter: Emitter, + onDataEmitter: Emitter, + onEndEmitter: Emitter, + ) { + super(); + this.onClose = this._register(onCloseEmitter).event; + this.onData = this._register(onDataEmitter).event; + this.onEnd = this._register(onEndEmitter).event; + } + + write(buffer: VSBuffer): void { + this.proxy.remoteSocketWrite(this.socketId, buffer); + } + + end(): void { + this.ended = true; + this.proxy.remoteSocketEnd(this.socketId); + } + + drain(): Promise { + return this.proxy.remoteSocketDrain(this.socketId); + } + + traceSocketEvent(type: SocketDiagnosticsEventType, data?: any): void { + SocketDiagnostics.traceSocketEvent(this, this.debugLabel, type, data); + } + + override dispose(): void { + if (!this.ended) { + this.proxy.remoteSocketEnd(this.socketId); + } + + this.didDisposeEmitter.fire(); + super.dispose(); + } +} + diff --git a/src/vs/workbench/services/remote/common/remoteAgentService.ts b/src/vs/workbench/services/remote/common/remoteAgentService.ts index ab505eb11ce..147d9728c80 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentService.ts @@ -8,7 +8,7 @@ import { RemoteAgentConnectionContext, IRemoteAgentEnvironment } from 'vs/platfo import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { Event } from 'vs/base/common/event'; -import { PersistentConnectionEvent, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; +import { PersistentConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection'; import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; export const IRemoteAgentService = createDecorator('remoteAgentService'); @@ -16,8 +16,6 @@ export const IRemoteAgentService = createDecorator('remoteA export interface IRemoteAgentService { readonly _serviceBrand: undefined; - readonly socketFactory: ISocketFactory; - getConnection(): IRemoteAgentConnection | null; /** * Get the remote environment. In case of an error, returns `null`. diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 7ce33fccd67..e3d3388c15b 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -621,7 +621,10 @@ export class TunnelModel extends Disposable { if (!existingTunnel) { const authority = this.environmentService.remoteAuthority; const addressProvider: IAddressProvider | undefined = authority ? { - getAddress: async () => { return (await this.remoteAuthorityResolverService.resolveAuthority(authority)).authority; } + getAddress: async () => { + const r = await this.remoteAuthorityResolverService.resolveAuthority(authority); + return { connectTo: r.authority.messaging, connectionToken: r.authority.connectionToken }; + } } : undefined; const key = makeAddress(tunnelProperties.remote.host, tunnelProperties.remote.port); diff --git a/src/vs/workbench/services/remote/electron-sandbox/remoteAgentService.ts b/src/vs/workbench/services/remote/electron-sandbox/remoteAgentService.ts index 85a638d5937..ded93640ace 100644 --- a/src/vs/workbench/services/remote/electron-sandbox/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/electron-sandbox/remoteAgentService.ts @@ -5,9 +5,8 @@ import * as nls from 'vs/nls'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAuthorityResolverService, MessagePassingType, RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IProductService } from 'vs/platform/product/common/productService'; -import { BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; import { AbstractRemoteAgentService } from 'vs/workbench/services/remote/common/abstractRemoteAgentService'; import { ISignService } from 'vs/platform/sign/common/sign'; import { ILogService } from 'vs/platform/log/common/log'; @@ -21,9 +20,11 @@ import { INativeHostService } from 'vs/platform/native/common/native'; import { URI } from 'vs/base/common/uri'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IRemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection'; export class RemoteAgentService extends AbstractRemoteAgentService implements IRemoteAgentService { constructor( + @IRemoteSocketFactoryCollection socketFactoryCollection: IRemoteSocketFactoryCollection, @IUserDataProfileService userDataProfileService: IUserDataProfileService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IProductService productService: IProductService, @@ -31,7 +32,7 @@ export class RemoteAgentService extends AbstractRemoteAgentService implements IR @ISignService signService: ISignService, @ILogService logService: ILogService, ) { - super(new BrowserSocketFactory(null), userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService); + super(socketFactoryCollection, userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService); } } @@ -79,12 +80,12 @@ class RemoteConnectionFailureNotificationContribution implements IWorkbenchContr return null; } const connectionData = this._remoteAuthorityResolverService.getConnectionData(remoteAgentConnection.remoteAuthority); - if (!connectionData) { + if (!connectionData || connectionData.connectTo.type !== MessagePassingType.WebSocket) { return null; } return URI.from({ scheme: 'http', - authority: `${connectionData.host}:${connectionData.port}`, + authority: `${connectionData.connectTo.host}:${connectionData.connectTo.port}`, path: `/version` }); } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index efcae100a18..34a841eaed9 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -167,6 +167,7 @@ import { InstallVSIXOptions, ILocalExtension, IGalleryExtension, InstallOptions, import { Codicon } from 'vs/base/common/codicons'; import { IHoverOptions, IHoverService, IHoverWidget } from 'vs/workbench/services/hover/browser/hover'; import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; +import { IRemoteSocketFactoryCollection, RemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined); @@ -325,6 +326,7 @@ export function workbenchInstantiationService( instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); instantiationService.stub(ITerminalInstanceService, new TestTerminalInstanceService()); instantiationService.stub(IElevatedFileService, new BrowserElevatedFileService()); + instantiationService.stub(IRemoteSocketFactoryCollection, new RemoteSocketFactoryCollection()); return instantiationService; } @@ -1937,7 +1939,7 @@ export class TestRemoteAgentService implements IRemoteAgentService { declare readonly _serviceBrand: undefined; - socketFactory: ISocketFactory = { + socketFactory: ISocketFactory = { connect() { } }; diff --git a/src/vscode-dts/vscode.proposed.resolvers.d.ts b/src/vscode-dts/vscode.proposed.resolvers.d.ts index c1c413bc31f..f35962c273d 100644 --- a/src/vscode-dts/vscode.proposed.resolvers.d.ts +++ b/src/vscode-dts/vscode.proposed.resolvers.d.ts @@ -26,6 +26,23 @@ declare module 'vscode' { constructor(host: string, port: number, connectionToken?: string); } + export interface ManagedMessagePassing { + onDidReceiveMessage: Event; + onDidClose: Event; + onDidEnd: Event; + + dataHandler: (data: Uint8Array) => void; + endHandler: () => void; + drainHandler?: () => void; + } + + export class ManagedResolvedAuthority { + readonly makeConnection: () => Thenable; + readonly connectionToken: string | undefined; + + constructor(makeConnection: () => Thenable, connectionToken?: string); + } + export interface ResolvedOptions { extensionHostEnv?: { [key: string]: string | null }; @@ -109,7 +126,7 @@ declare module 'vscode' { Output = 2 } - export type ResolverResult = ResolvedAuthority & ResolvedOptions & TunnelInformation; + export type ResolverResult = (ResolvedAuthority | ManagedResolvedAuthority) & ResolvedOptions & TunnelInformation; export class RemoteAuthorityResolverError extends Error { static NotAvailable(message?: string, handled?: boolean): RemoteAuthorityResolverError;