From f5427eed53bec4db53f95bb02802c87ebcefc665 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 18 Apr 2023 14:51:14 -0700 Subject: [PATCH] remote: first cut at 'inline' remote resolvers For web, it seems the most feasible direction for resolvers as we make existing remote extensions 'web enabled' is to allow them to run in the extension host. However, in no case will there just be a simple websocket we can connect to ordinarily. This PR implements a first cut at 'inline' resolvers where messaging is done in the extension host. I have not yet tested them on web, where I think some more wiring is needed to mirror desktop. Also, resolution of URLs is not in yet. I think for this we'd want to do some service-worker -based 'loopback' approach to run requests inline in the remote connection, similar to what I did for tunnels... Resolvers are not yet run in a dedicated extension host, but I think that should happen, at least on web where resolvers will always(?) be 'inline'. Most of the actual changes are genericizing places where we specified the "host" and "port" previously into an enum. Additionally, instead of having a single ISocketFactory, there's now a collection of them, which the extension host manager registers into when a managed resolution happens. --- extensions/vscode-test-resolver/package.json | 5 + .../vscode-test-resolver/src/extension.ts | 89 ++++++++++++------ src/vs/base/common/async.ts | 28 +++--- src/vs/base/common/buffer.ts | 48 ++++++++++ src/vs/base/test/common/buffer.test.ts | 16 ++++ .../remote/browser/browserSocketFactory.ts | 6 +- .../browser/remoteAuthorityResolverService.ts | 53 ++++++++--- .../platform/remote/common/managedSocket.ts | 26 ++++++ .../remote/common/remoteAgentConnection.ts | 49 +++++----- .../remote/common/remoteAuthorityResolver.ts | 26 +++++- .../common/remoteSocketFactoryCollection.ts | 53 +++++++++++ .../remoteAuthorityResolverService.ts | 67 +++++--------- .../platform/remote/node/nodeSocketFactory.ts | 21 +---- src/vs/platform/tunnel/common/tunnel.ts | 4 +- src/vs/platform/tunnel/node/tunnelService.ts | 45 +++++---- .../api/browser/mainThreadExtensionService.ts | 41 ++++++++- .../workbench/api/common/extHost.api.impl.ts | 1 + .../workbench/api/common/extHost.protocol.ts | 9 ++ .../api/common/extHostExtensionService.ts | 92 ++++++++++++++++--- src/vs/workbench/api/common/extHostTypes.ts | 19 +++- src/vs/workbench/browser/web.main.ts | 12 ++- .../contrib/webview/browser/webviewElement.ts | 5 +- .../electron-sandbox/desktop.main.ts | 9 +- src/vs/workbench/electron-sandbox/window.ts | 3 +- .../test/browser/configurationEditing.test.ts | 2 +- .../test/browser/configurationService.test.ts | 19 ++-- .../extensions/browser/extensionService.ts | 2 +- .../common/abstractExtensionService.ts | 2 +- .../extensions/common/extHostCustomers.ts | 8 ++ .../extensions/common/extensionHostManager.ts | 68 +++++++++++++- .../extensions/common/extensionHostProxy.ts | 5 + .../extensions/common/remoteExtensionHost.ts | 38 ++++---- .../nativeExtensionService.ts | 31 ++++--- .../remote/browser/remoteAgentService.ts | 6 +- .../common/abstractRemoteAgentService.ts | 56 ++++++----- .../services/remote/common/managedSocket.ts | 91 ++++++++++++++++++ .../remote/common/remoteAgentService.ts | 4 +- .../remote/common/remoteExplorerService.ts | 5 +- .../electron-sandbox/remoteAgentService.ts | 11 ++- .../test/browser/workbenchTestServices.ts | 4 +- src/vscode-dts/vscode.proposed.resolvers.d.ts | 19 +++- 41 files changed, 826 insertions(+), 272 deletions(-) create mode 100644 src/vs/platform/remote/common/managedSocket.ts create mode 100644 src/vs/platform/remote/common/remoteSocketFactoryCollection.ts create mode 100644 src/vs/workbench/services/remote/common/managedSocket.ts 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;