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..4178c2b823e 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, + send: d => remoteSocket.write(d), + end: () => 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/common/event.ts b/src/vs/base/common/event.ts index 81981c52906..24547947c56 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -1171,6 +1171,10 @@ export class PauseableEmitter extends Emitter { protected _eventQueue = new LinkedList(); private _mergeFn?: (input: T[]) => T; + public get isPaused(): boolean { + return this._isPaused !== 0; + } + constructor(options?: EmitterOptions & { merge?: (input: T[]) => T }) { super(options); this._mergeFn = options?.merge; 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/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 4e427d56b8f..cfa0e3296f0 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -341,7 +341,12 @@ export interface IExtension { */ export class ExtensionIdentifier { public readonly value: string; - private readonly _lower: string; + + /** + * Do not use directly. This is public to avoid mangling and thus + * allow compatibility between running from source and a built version. + */ + readonly _lower: string; constructor(value: string) { this.value = value; diff --git a/src/vs/platform/remote/browser/browserSocketFactory.ts b/src/vs/platform/remote/browser/browserSocketFactory.ts index 0998b898ea9..765b2f3fa6f 100644 --- a/src/vs/platform/remote/browser/browserSocketFactory.ts +++ b/src/vs/platform/remote/browser/browserSocketFactory.ts @@ -9,8 +9,8 @@ import { VSBuffer } from 'vs/base/common/buffer'; 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 { ISocketFactory } from 'vs/platform/remote/common/remoteSocketFactoryService'; +import { RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode, RemoteConnectionType, WebSocketRemoteConnection } from 'vs/platform/remote/common/remoteAuthorityResolver'; export interface IWebSocketFactory { create(url: string, debugLabel: string): IWebSocket; @@ -265,23 +265,27 @@ 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 { - 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)); - socket.onOpen(() => { - errorListener.dispose(); - callback(undefined, new BrowserSocket(socket, debugLabel)); + supports(connectTo: WebSocketRemoteConnection): boolean { + return true; + } + + connect({ host, port }: WebSocketRemoteConnection, path: string, query: string, debugLabel: string): Promise { + return new Promise((resolve, reject) => { + 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(reject); + socket.onOpen(() => { + errorListener.dispose(); + resolve(new BrowserSocket(socket, debugLabel)); + }); }); } } - - - diff --git a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts index 183790bd295..2e956666f15 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, RemoteConnectionType, ResolvedAuthority, ResolvedOptions, ResolverResult, WebSocketRemoteConnection, 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.connectTo, 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, connectTo: new WebSocketRemoteConnection(host, 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.connectTo.type === RemoteConnectionType.WebSocket) { + // todo@connor4312 need to implement some kind of loopback for ext host based messaging + RemoteAuthorities.set(resolvedAuthority.authority, resolvedAuthority.connectTo.host, resolvedAuthority.connectTo.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..6b24b148aa8 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -16,8 +16,9 @@ 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, RemoteConnection } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts'; +import { IRemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; import { ISignService } from 'vs/platform/sign/common/sign'; const RECONNECT_TIMEOUT = 30 * 1000 /* 30s */; @@ -71,27 +72,18 @@ 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; + remoteSocketFactoryService: IRemoteSocketFactoryService; signService: ISignService; logService: ILogService; } -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; -} - function createTimeoutCancellation(millis: number): CancellationToken { const source = new CancellationTokenSource(); setTimeout(() => source.cancel(), millis); @@ -192,31 +184,29 @@ 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, remoteSocketFactoryService: IRemoteSocketFactoryService, 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) => { + + remoteSocketFactoryService.connect(connectTo, path, query, debugLabel).then((socket) => { 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.`); - if (err) { - logService.error(err); - } socket?.dispose(); } else { - if (err || !socket) { - performance.mark(`code/didCreateSocketError/${debugConnectionType}`); - logService.info(`Creating a socket (${debugLabel}) returned an error after ${sw.elapsed()} ms.`); - result.reject(err); - } else { - performance.mark(`code/didCreateSocketOK/${debugConnectionType}`); - logService.info(`Creating a socket (${debugLabel}) was successful after ${sw.elapsed()} ms.`); - result.resolve(socket); - } + performance.mark(`code/didCreateSocketOK/${debugConnectionType}`); + logService.info(`Creating a socket (${debugLabel}) was successful after ${sw.elapsed()} ms.`); + result.resolve(socket); } + }, (err) => { + performance.mark(`code/didCreateSocketError/${debugConnectionType}`); + logService.info(`Creating a socket (${debugLabel}) returned an error after ${sw.elapsed()} ms.`); + logService.error(err); + result.reject(err); }); + return result.promise; } @@ -237,14 +227,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.remoteSocketFactoryService, 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,40 +379,38 @@ async function doConnectRemoteAgentTunnel(options: ISimpleConnectionOptions, sta return protocol; } -export interface IConnectionOptions { +export interface IConnectionOptions { commit: string | undefined; quality: string | undefined; - socketFactory: ISocketFactory; - addressProvider: IAddressProvider; + addressProvider: IAddressProvider; + remoteSocketFactoryService: IRemoteSocketFactoryService; 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, - socketFactory: options.socketFactory, + remoteSocketFactoryService: options.remoteSocketFactoryService, signService: options.signService, logService: options.logService }; } -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 +436,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 +679,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 +820,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..c67541b0bc6 100644 --- a/src/vs/platform/remote/common/remoteAuthorityResolver.ts +++ b/src/vs/platform/remote/common/remoteAuthorityResolver.ts @@ -10,10 +10,43 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export const IRemoteAuthorityResolverService = createDecorator('remoteAuthorityResolverService'); +export const enum RemoteConnectionType { + WebSocket, + Managed +} + +export class ManagedRemoteConnection { + public readonly type = RemoteConnectionType.Managed; + + constructor( + public readonly id: number + ) { } + + public toString(): string { + return `Managed(${this.id})`; + } +} + +export class WebSocketRemoteConnection { + public readonly type = RemoteConnectionType.WebSocket; + + constructor( + public readonly host: string, + public readonly port: number, + ) { } + + public toString(): string { + return `WebSocket(${this.host}:${this.port})`; + } +} + +export type RemoteConnection = WebSocketRemoteConnection | ManagedRemoteConnection; + +export type RemoteConnectionOfType = RemoteConnection & { type: T }; + export interface ResolvedAuthority { readonly authority: string; - readonly host: string; - readonly port: number; + readonly connectTo: RemoteConnection; readonly connectionToken: string | undefined; } @@ -50,8 +83,7 @@ export interface ResolverResult { } export interface IRemoteConnectionData { - host: string; - port: number; + connectTo: RemoteConnection; connectionToken: string | undefined; } diff --git a/src/vs/platform/remote/common/remoteSocketFactoryService.ts b/src/vs/platform/remote/common/remoteSocketFactoryService.ts new file mode 100644 index 00000000000..6d6eacfc854 --- /dev/null +++ b/src/vs/platform/remote/common/remoteSocketFactoryService.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ISocket } from 'vs/base/parts/ipc/common/ipc.net'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { RemoteConnectionOfType, RemoteConnectionType, RemoteConnection } from 'vs/platform/remote/common/remoteAuthorityResolver'; + +export const IRemoteSocketFactoryService = createDecorator('remoteSocketFactoryService'); + +export interface IRemoteSocketFactoryService { + 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: ISocketFactory): IDisposable; + + connect(connectTo: RemoteConnection, path: string, query: string, debugLabel: string): Promise; +} + +export interface ISocketFactory { + supports(connectTo: RemoteConnectionOfType): boolean; + connect(connectTo: RemoteConnectionOfType, path: string, query: string, debugLabel: string): Promise; +} + +export class RemoteSocketFactoryService implements IRemoteSocketFactoryService { + declare readonly _serviceBrand: undefined; + + private readonly factories: { [T in RemoteConnectionType]?: ISocketFactory[] } = {}; + + public register(type: T, factory: ISocketFactory): IDisposable { + this.factories[type] ??= []; + this.factories[type]!.push(factory); + return toDisposable(() => { + const idx = this.factories[type]?.indexOf(factory); + if (typeof idx === 'number' && idx >= 0) { + this.factories[type]?.splice(idx, 1); + } + }); + } + + private getSocketFactory(messagePassing: RemoteConnectionOfType): ISocketFactory | undefined { + const factories = (this.factories[messagePassing.type] || []) as ISocketFactory[]; + return factories.find(factory => factory.supports(messagePassing)); + } + + public connect(connectTo: RemoteConnection, path: string, query: string, debugLabel: string): Promise { + const socketFactory = this.getSocketFactory(connectTo); + if (!socketFactory) { + throw new Error(`No socket factory found for ${connectTo}`); + } + return socketFactory.connect(connectTo, path, query, debugLabel); + } +} diff --git a/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts b/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts index f048d7153a9..701257da09b 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, RemoteConnectionType, 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,22 @@ 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)); - this._canonicalURIRequests.set(key, request); + const existing = this._canonicalURIRequests.get(key); + if (existing) { + return existing.result.p; } - return this._canonicalURIRequests.get(key)!.promise; + + const result = new DeferredPromise(); + this._canonicalURIProvider?.(uri).then((uri) => result.complete(uri), (err) => result.error(err)); + this._canonicalURIRequests.set(key, { input: uri, result }); + return result.p; } getConnectionData(authority: string): IRemoteConnectionData | null { @@ -82,20 +60,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.connectTo, 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 +80,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.connectTo.type === RemoteConnectionType.WebSocket) { + // todo@connor4312 need to implement some kind of loopback for ext host based messaging + RemoteAuthorities.set(resolvedAuthority.authority, resolvedAuthority.connectTo.host, resolvedAuthority.connectTo.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 +96,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 +108,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(({ result, input }) => { + this._canonicalURIProvider!(input).then((uri) => result.complete(uri), (err) => result.error(err)); }); } } diff --git a/src/vs/platform/remote/node/nodeSocketFactory.ts b/src/vs/platform/remote/node/nodeSocketFactory.ts index 8e859bcd2f6..4641791946a 100644 --- a/src/vs/platform/remote/node/nodeSocketFactory.ts +++ b/src/vs/platform/remote/node/nodeSocketFactory.ts @@ -4,43 +4,38 @@ *--------------------------------------------------------------------------------------------*/ import * as net from 'net'; +import { ISocket } from 'vs/base/parts/ipc/common/ipc.net'; import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; -import { IConnectCallback, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; +import { makeRawSocketHeaders } from 'vs/platform/remote/common/managedSocket'; +import { RemoteConnectionType, WebSocketRemoteConnection } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { ISocketFactory } from 'vs/platform/remote/common/remoteSocketFactoryService'; -export const nodeSocketFactory = new class implements ISocketFactory { - connect(host: string, port: number, path: string, query: string, debugLabel: string, callback: IConnectCallback): void { - const errorListener = (err: any) => callback(err, undefined); +export const nodeSocketFactory = new class implements ISocketFactory { - const socket = net.createConnection({ host: host, port: port }, () => { - socket.removeListener('error', errorListener); + supports(connectTo: WebSocketRemoteConnection): boolean { + return true; + } - // 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'); + connect({ host, port }: WebSocketRemoteConnection, path: string, query: string, debugLabel: string): Promise { + return new Promise((resolve, reject) => { + const socket = net.createConnection({ host: host, port: port }, () => { + socket.removeListener('error', reject); - 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(); - if (strData.indexOf('\r\n\r\n') >= 0) { - // headers received OK - socket.off('data', onData); - callback(undefined, new NodeSocket(socket, debugLabel)); - } - }; - socket.on('data', onData); + const onData = (data: Buffer) => { + const strData = data.toString(); + if (strData.indexOf('\r\n\r\n') >= 0) { + // headers received OK + socket.off('data', onData); + resolve(new NodeSocket(socket, debugLabel)); + } + }; + socket.on('data', onData); + }); + // Disable Nagle's algorithm. + socket.setNoDelay(true); + socket.once('error', reject); }); - // Disable Nagle's algorithm. - socket.setNoDelay(true); - socket.once('error', errorListener); } }; diff --git a/src/vs/platform/tunnel/node/tunnelService.ts b/src/vs/platform/tunnel/node/tunnelService.ts index aa941b067e3..31f153b651a 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 { IRemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; 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, + @IRemoteSocketFactoryService private readonly remoteSocketFactoryService: IRemoteSocketFactoryService, @ILogService logService: ILogService, @ISignService private readonly signService: ISignService, @IProductService private readonly productService: IProductService, @@ -182,8 +182,8 @@ export class BaseTunnelService extends AbstractTunnelService { const options: IConnectionOptions = { commit: this.productService.commit, quality: this.productService.quality, - socketFactory: this.socketFactory, addressProvider, + remoteSocketFactoryService: this.remoteSocketFactoryService, signService: this.signService, logService: this.logService, ipcLogger: null @@ -199,12 +199,13 @@ export class BaseTunnelService extends AbstractTunnelService { export class TunnelService extends BaseTunnelService { public constructor( + @IRemoteSocketFactoryService remoteSocketFactoryService: IRemoteSocketFactoryService, @ILogService logService: ILogService, @ISignService signService: ISignService, @IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService ) { - super(nodeSocketFactory, logService, signService, productService, configurationService); + super(remoteSocketFactoryService, logService, signService, productService, configurationService); } } @@ -213,6 +214,7 @@ export class SharedTunnelsService extends Disposable implements ISharedTunnelsSe private readonly _tunnelServices: Map = new Map(); public constructor( + @IRemoteSocketFactoryService protected readonly remoteSocketFactoryService: IRemoteSocketFactoryService, @ILogService protected readonly logService: ILogService, @IProductService private readonly productService: IProductService, @ISignService private readonly signService: ISignService, @@ -224,7 +226,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.remoteSocketFactoryService, 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/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 65aeecacbf5..cc5768ce225 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -43,6 +43,7 @@ import './mainThreadLanguageFeatures'; import './mainThreadLanguages'; import './mainThreadLogService'; import './mainThreadMessageService'; +import './mainThreadManagedSockets'; import './mainThreadOutputService'; import './mainThreadProgress'; import './mainThreadQuickDiff'; diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index d822db420e5..cbddc0c233b 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, ManagedRemoteConnection, RemoteConnection, RemoteConnectionType, ResolvedAuthority, WebSocketRemoteConnection } 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'; @@ -26,6 +26,7 @@ import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/com 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 { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; @@ -199,8 +200,9 @@ 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 = reviveResolveAuthorityResult(await this._actual.$resolveAuthority(remoteAuthority, resolveAttempt)); + return resolved; } async getCanonicalURI(remoteAuthority: string, uri: URI): Promise { const uriComponents = await this._actual.$getCanonicalURI(remoteAuthority, uri); @@ -237,3 +239,31 @@ class ExtensionHostProxy implements IExtensionHostProxy { return this._actual.$test_down(size); } } + +function reviveResolveAuthorityResult(result: Dto): IResolveAuthorityResult { + if (result.type === 'ok') { + return { + type: 'ok', + value: { + ...result.value, + authority: reviveResolvedAuthority(result.value.authority), + } + }; + } else { + return result; + } +} + +function reviveResolvedAuthority(resolvedAuthority: Dto): ResolvedAuthority { + return { + ...resolvedAuthority, + connectTo: reviveConnection(resolvedAuthority.connectTo), + }; +} + +function reviveConnection(connection: Dto): RemoteConnection { + if (connection.type === RemoteConnectionType.WebSocket) { + return new WebSocketRemoteConnection(connection.host, connection.port); + } + return new ManagedRemoteConnection(connection.id); +} diff --git a/src/vs/workbench/api/browser/mainThreadManagedSockets.ts b/src/vs/workbench/api/browser/mainThreadManagedSockets.ts new file mode 100644 index 00000000000..dbd5c34ac3e --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadManagedSockets.ts @@ -0,0 +1,207 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MainContext, ExtHostContext, MainThreadManagedSocketsShape, ExtHostManagedSocketsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { ManagedRemoteConnection, RemoteConnectionType } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { IRemoteSocketFactoryService, ISocketFactory } from 'vs/platform/remote/common/remoteSocketFactoryService'; +import { ISocket, SocketCloseEvent, SocketCloseEventType, SocketDiagnostics, SocketDiagnosticsEventType } from 'vs/base/parts/ipc/common/ipc.net'; +import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event'; +import { makeRawSocketHeaders, socketRawEndHeaderSequence } from 'vs/platform/remote/common/managedSocket'; + +@extHostNamedCustomer(MainContext.MainThreadManagedSockets) +export class MainThreadManagedSockets extends Disposable implements MainThreadManagedSocketsShape { + + private readonly _proxy: ExtHostManagedSocketsShape; + private readonly _registrations = new Map(); + private readonly _remoteSockets = new Map(); + + constructor( + extHostContext: IExtHostContext, + @IRemoteSocketFactoryService private readonly _remoteSocketFactoryService: IRemoteSocketFactoryService, + ) { + super(); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostManagedSockets); + } + + async $registerSocketFactory(socketFactoryId: number): Promise { + const that = this; + const socketFactory = new class implements ISocketFactory { + + supports(connectTo: ManagedRemoteConnection): boolean { + return (connectTo.id === socketFactoryId); + } + + connect(connectTo: ManagedRemoteConnection, path: string, query: string, debugLabel: string): Promise { + return new Promise((resolve, reject) => { + if (connectTo.id !== socketFactoryId) { + return reject(new Error('Invalid connectTo')); + } + + const factoryId = connectTo.id; + that._proxy.$openRemoteSocket(factoryId).then(socketId => { + const half: RemoteSocketHalf = { + onClose: new Emitter(), + onData: new Emitter(), + onEnd: new Emitter(), + }; + that._remoteSockets.set(socketId, half); + + ManagedSocket.connect(socketId, that._proxy, path, query, debugLabel, half) + .then( + socket => { + socket.onDidDispose(() => that._remoteSockets.delete(socketId)); + resolve(socket); + }, + err => { + that._remoteSockets.delete(socketId); + reject(err); + }); + }).catch(reject); + }); + } + }; + this._registrations.set(socketFactoryId, this._remoteSocketFactoryService.register(RemoteConnectionType.Managed, socketFactory)); + + } + + async $unregisterSocketFactory(socketFactoryId: number): Promise { + this._registrations.get(socketFactoryId)?.dispose(); + } + + $onDidManagedSocketHaveData(socketId: number, data: VSBuffer): void { + this._remoteSockets.get(socketId)?.onData.fire(data); + } + + $onDidManagedSocketClose(socketId: number, error: string | undefined): void { + this._remoteSockets.get(socketId)?.onClose.fire({ + type: SocketCloseEventType.NodeSocketCloseEvent, + error: error ? new Error(error) : undefined, + hadError: !!error + }); + this._remoteSockets.delete(socketId); + } + + $onDidManagedSocketEnd(socketId: number): void { + this._remoteSockets.get(socketId)?.onEnd.fire(); + } +} + +export interface RemoteSocketHalf { + onData: Emitter; + onClose: Emitter; + onEnd: Emitter; +} + +export class ManagedSocket extends Disposable implements ISocket { + public static connect( + socketId: number, + proxy: ExtHostManagedSocketsShape, + path: string, query: string, debugLabel: string, + + half: RemoteSocketHalf + ): 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) => { + let dataSoFar: VSBuffer | undefined; + d.add(socket.onData(d => { + if (!dataSoFar) { + dataSoFar = d; + } else { + dataSoFar = VSBuffer.concat([dataSoFar, d], dataSoFar.byteLength + d.byteLength); + } + + const index = dataSoFar.indexOf(socketRawEndHeaderSequence); + if (index === -1) { + return; + } + + resolve(socket); + // pause data events until the socket consumer is hooked up. We may + // immediately emit remaining data, but if not there may still be + // microtasks queued which would fire data into the abyss. + socket.pauseData(); + + const rest = dataSoFar.slice(index + socketRawEndHeaderSequence.byteLength); + if (rest.byteLength) { + half.onData.fire(rest); + } + })); + + d.add(socket.onClose(err => reject(err ?? new Error('socket closed')))); + d.add(socket.onEnd(() => reject(new Error('socket ended')))); + }).finally(() => d.dispose()); + } + + private readonly pausableDataEmitter = this._register(new PauseableEmitter()); + + public onData: Event = (...args) => { + if (this.pausableDataEmitter.isPaused) { + queueMicrotask(() => this.pausableDataEmitter.resume()); + } + return this.pausableDataEmitter.event(...args); + }; + 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: ExtHostManagedSocketsShape, + private readonly debugLabel: string, + onCloseEmitter: Emitter, + onDataEmitter: Emitter, + onEndEmitter: Emitter, + ) { + super(); + + this._register(onDataEmitter); + this._register(onDataEmitter.event(data => this.pausableDataEmitter.fire(data))); + + this.onClose = this._register(onCloseEmitter).event; + this.onEnd = this._register(onEndEmitter).event; + } + + /** Pauses data events until a new listener comes in onData() */ + pauseData() { + this.pausableDataEmitter.pause(); + } + + 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/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 4bc0613aaea..1536555f0e3 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -102,6 +102,7 @@ import { ExtHostInteractiveEditor } from 'vs/workbench/api/common/extHostInterac import { ExtHostNotebookDocumentSaveParticipant } from 'vs/workbench/api/common/extHostNotebookDocumentSaveParticipant'; import { ExtHostSemanticSimilarity } from 'vs/workbench/api/common/extHostSemanticSimilarity'; import { ExtHostIssueReporter } from 'vs/workbench/api/common/extHostIssueReporter'; +import { IExtHostManagedSockets } from 'vs/workbench/api/common/extHostManagedSockets'; export interface IExtensionRegistries { mine: ExtensionDescriptionRegistry; @@ -136,6 +137,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostWindow = accessor.get(IExtHostWindow); const extHostSecretState = accessor.get(IExtHostSecretState); const extHostEditorTabs = accessor.get(IExtHostEditorTabs); + const extHostManagedSockets = accessor.get(IExtHostManagedSockets); // register addressable instances rpcProtocol.set(ExtHostContext.ExtHostFileSystemInfo, extHostFileSystemInfo); @@ -149,6 +151,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.set(ExtHostContext.ExtHostSecretState, extHostSecretState); rpcProtocol.set(ExtHostContext.ExtHostTelemetry, extHostTelemetry); rpcProtocol.set(ExtHostContext.ExtHostEditorTabs, extHostEditorTabs); + rpcProtocol.set(ExtHostContext.ExtHostManagedSockets, extHostManagedSockets); // automatically create and register addressable instances const extHostDecorations = rpcProtocol.set(ExtHostContext.ExtHostDecorations, accessor.get(IExtHostDecorations)); @@ -1444,6 +1447,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.common.services.ts b/src/vs/workbench/api/common/extHost.common.services.ts index d451acc438f..faf45b596a7 100644 --- a/src/vs/workbench/api/common/extHost.common.services.ts +++ b/src/vs/workbench/api/common/extHost.common.services.ts @@ -27,6 +27,7 @@ import { ExtHostLoggerService } from 'vs/workbench/api/common/extHostLoggerServi import { ILoggerService } from 'vs/platform/log/common/log'; import { ExtHostVariableResolverProviderService, IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService'; import { ExtHostLocalizationService, IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService'; +import { ExtHostManagedSockets, IExtHostManagedSockets } from 'vs/workbench/api/common/extHostManagedSockets'; registerSingleton(IExtHostLocalizationService, ExtHostLocalizationService, InstantiationType.Delayed); registerSingleton(ILoggerService, ExtHostLoggerService, InstantiationType.Delayed); @@ -37,6 +38,7 @@ registerSingleton(IExtHostConsumerFileSystem, ExtHostConsumerFileSystem, Instant registerSingleton(IExtHostDebugService, WorkerExtHostDebugService, InstantiationType.Eager); registerSingleton(IExtHostDecorations, ExtHostDecorations, InstantiationType.Eager); registerSingleton(IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors, InstantiationType.Eager); +registerSingleton(IExtHostManagedSockets, ExtHostManagedSockets, InstantiationType.Eager); registerSingleton(IExtHostFileSystemInfo, ExtHostFileSystemInfo, InstantiationType.Eager); registerSingleton(IExtHostOutputService, ExtHostOutputService, InstantiationType.Delayed); registerSingleton(IExtHostSearch, ExtHostSearch, InstantiationType.Eager); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f0221c117c3..5e20100ae03 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -957,6 +957,21 @@ export interface ExtHostWebviewViewsShape { $disposeWebviewView(webviewHandle: WebviewHandle): void; } +export interface MainThreadManagedSocketsShape extends IDisposable { + $registerSocketFactory(socketFactoryId: number): Promise; + $unregisterSocketFactory(socketFactoryId: number): Promise; + $onDidManagedSocketHaveData(socketId: number, data: VSBuffer): void; + $onDidManagedSocketClose(socketId: number, error: string | undefined): void; + $onDidManagedSocketEnd(socketId: number): void; +} + +export interface ExtHostManagedSocketsShape { + $openRemoteSocket(socketFactoryId: number): Promise; + $remoteSocketWrite(socketId: number, buffer: VSBuffer): void; + $remoteSocketEnd(socketId: number): void; + $remoteSocketDrain(socketId: number): Promise; +} + export enum CellOutputKind { Text = 1, Error = 2, @@ -1590,7 +1605,7 @@ export interface ExtHostSearchShape { } export interface ExtHostExtensionServiceShape { - $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise; + $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise>; /** * Returns `null` if no resolver for `remoteAuthority` is found. */ @@ -2518,6 +2533,7 @@ export const MainContext = { MainThreadInteractiveEditor: createProxyIdentifier('MainThreadInteractiveEditor'), MainThreadTheming: createProxyIdentifier('MainThreadTheming'), MainThreadTunnelService: createProxyIdentifier('MainThreadTunnelService'), + MainThreadManagedSockets: createProxyIdentifier('MainThreadManagedSockets'), MainThreadTimeline: createProxyIdentifier('MainThreadTimeline'), MainThreadTesting: createProxyIdentifier('MainThreadTesting'), MainThreadLocalization: createProxyIdentifier('MainThreadLocalizationShape'), @@ -2579,6 +2595,7 @@ export const ExtHostContext = { ExtHostSemanticSimilarity: createProxyIdentifier('ExtHostSemanticSimilarity'), ExtHostTheming: createProxyIdentifier('ExtHostTheming'), ExtHostTunnelService: createProxyIdentifier('ExtHostTunnelService'), + ExtHostManagedSockets: createProxyIdentifier('ExtHostManagedSockets'), ExtHostAuthentication: createProxyIdentifier('ExtHostAuthentication'), ExtHostTimeline: createProxyIdentifier('ExtHostTimeline'), ExtHostTesting: createProxyIdentifier('ExtHostTesting'), diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index afe2939e5b8..75db96a50ca 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -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, ManagedRemoteConnection, WebSocketRemoteConnection } 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'; @@ -43,6 +43,8 @@ import { IResolveAuthorityResult } from 'vs/workbench/services/extensions/common import { IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService'; import { StopWatch } from 'vs/base/common/stopwatch'; import { setTimeout0 } from 'vs/base/common/platform'; +import { IExtHostManagedSockets } from 'vs/workbench/api/common/extHostManagedSockets'; +import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; interface ITestRunner { /** Old test runner API, as exported from `vscode/lib/testrunner` */ @@ -130,7 +132,8 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme @IExtensionStoragePaths storagePath: IExtensionStoragePaths, @IExtHostTunnelService extHostTunnelService: IExtHostTunnelService, @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService, - @IExtHostLocalizationService extHostLocalizationService: IExtHostLocalizationService + @IExtHostLocalizationService extHostLocalizationService: IExtHostLocalizationService, + @IExtHostManagedSockets private readonly _extHostManagedSockets: IExtHostManagedSockets, ) { super(); this._hostUtils = hostUtils; @@ -791,7 +794,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return { authorityPrefix, resolver: this._resolvers[authorityPrefix] }; } - public async $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise { + public async $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise> { const sw = StopWatch.create(false); const prefix = () => `[resolveAuthority(${getRemoteAuthorityPrefix(remoteAuthority)},${resolveAttempt})][${sw.elapsed()}ms] `; const logInfo = (msg: string) => this._logService.info(`${prefix()}${msg}`); @@ -822,30 +825,49 @@ 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, + connectTo: new WebSocketRemoteConnection(result.host, result.port), + connectionToken: result.connectionToken + }; + } else { + // The socket factory is identified by the `resolveAttempt`, since that is a number which + // always increments and is unique over all resolve() calls in a workbench session. + const socketFactoryId = resolveAttempt; + + // There is only on managed socket factory at a time, so we can just overwrite the old one. + this._extHostManagedSockets.setFactory(socketFactoryId, result.makeConnection); + + authority = { + authority: remoteAuthority, + connectTo: new ManagedRemoteConnection(socketFactoryId), + connectionToken: result.connectionToken + }; + } + return { type: 'ok', value: { - authority, + authority: authority as Dto, options, - tunnelInformation: { - environmentTunnels: result.environmentTunnels, - features: result.tunnelFeatures - } + tunnelInformation, } }; } catch (err) { diff --git a/src/vs/workbench/api/common/extHostManagedSockets.ts b/src/vs/workbench/api/common/extHostManagedSockets.ts new file mode 100644 index 00000000000..d2f028df19e --- /dev/null +++ b/src/vs/workbench/api/common/extHostManagedSockets.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtHostManagedSocketsShape, MainContext, MainThreadManagedSocketsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import * as vscode from 'vscode'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { VSBuffer } from 'vs/base/common/buffer'; + +export interface IExtHostManagedSockets extends ExtHostManagedSocketsShape { + setFactory(socketFactoryId: number, makeConnection: () => Thenable): void; + readonly _serviceBrand: undefined; +} + +export const IExtHostManagedSockets = createDecorator('IExtHostManagedSockets'); + +export class ExtHostManagedSockets implements IExtHostManagedSockets { + declare readonly _serviceBrand: undefined; + + private readonly _proxy: MainThreadManagedSocketsShape; + private _remoteSocketIdCounter = 0; + private _factory: ManagedSocketFactory | null = null; + private readonly _managedRemoteSockets: Map = new Map(); + + constructor( + @IExtHostRpcService extHostRpc: IExtHostRpcService, + ) { + this._proxy = extHostRpc.getProxy(MainContext.MainThreadManagedSockets); + } + + setFactory(socketFactoryId: number, makeConnection: () => Thenable): void { + // Terminate all previous sockets + for (const socket of this._managedRemoteSockets.values()) { + // calling dispose() will lead to it removing itself from the map + socket.dispose(); + } + // Unregister previous factory + if (this._factory) { + this._proxy.$unregisterSocketFactory(this._factory.socketFactoryId); + } + + this._factory = new ManagedSocketFactory(socketFactoryId, makeConnection); + this._proxy.$registerSocketFactory(this._factory.socketFactoryId); + } + + async $openRemoteSocket(socketFactoryId: number): Promise { + if (!this._factory || this._factory.socketFactoryId !== socketFactoryId) { + throw new Error(`No socket factory with id ${socketFactoryId}`); + } + + const id = (++this._remoteSocketIdCounter); + const socket = await this._factory.makeConnection(); + const disposable = new DisposableStore(); + this._managedRemoteSockets.set(id, new ManagedSocket(id, socket, disposable)); + + disposable.add(toDisposable(() => this._managedRemoteSockets.delete(id))); + disposable.add(socket.onDidEnd(() => { + this._proxy.$onDidManagedSocketEnd(id); + disposable.dispose(); + })); + disposable.add(socket.onDidClose(e => { + this._proxy.$onDidManagedSocketClose(id, e?.stack ?? e?.message); + disposable.dispose(); + })); + disposable.add(socket.onDidReceiveMessage(e => this._proxy.$onDidManagedSocketHaveData(id, VSBuffer.wrap(e)))); + + return id; + } + + $remoteSocketWrite(socketId: number, buffer: VSBuffer): void { + this._managedRemoteSockets.get(socketId)?.actual.send(buffer.buffer); + } + + $remoteSocketEnd(socketId: number): void { + const socket = this._managedRemoteSockets.get(socketId); + if (socket) { + socket.actual.end(); + socket.dispose(); + } + } + + async $remoteSocketDrain(socketId: number): Promise { + await this._managedRemoteSockets.get(socketId)?.actual.drain?.(); + } +} + +class ManagedSocketFactory { + constructor( + public readonly socketFactoryId: number, + public readonly makeConnection: () => Thenable, + ) { } +} + +class ManagedSocket extends Disposable { + constructor( + public readonly socketId: number, + public readonly actual: vscode.ManagedMessagePassing, + disposer: DisposableStore, + ) { + super(); + this._register(disposer); + } +} diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 4fd6e2deb1a..0eb50c6f095 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/api/test/browser/mainThreadManagedSockets.test.ts b/src/vs/workbench/api/test/browser/mainThreadManagedSockets.test.ts new file mode 100644 index 00000000000..611ec9727c0 --- /dev/null +++ b/src/vs/workbench/api/test/browser/mainThreadManagedSockets.test.ts @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { disposableTimeout, timeout } from 'vs/base/common/async'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { Emitter } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { SocketCloseEvent } from 'vs/base/parts/ipc/common/ipc.net'; +import { mock } from 'vs/base/test/common/mock'; +import { ManagedSocket, RemoteSocketHalf } from 'vs/workbench/api/browser/mainThreadManagedSockets'; +import { ExtHostManagedSocketsShape } from 'vs/workbench/api/common/extHost.protocol'; + +suite('MainThreadManagedSockets', () => { + + suite('ManagedSocket', () => { + let extHost: ExtHostMock; + let half: RemoteSocketHalf; + + class ExtHostMock extends mock() { + private onDidFire = new Emitter(); + public readonly events: any[] = []; + + override $remoteSocketWrite(socketId: number, buffer: VSBuffer): void { + this.events.push({ socketId, data: buffer.toString() }); + this.onDidFire.fire(); + } + + override $remoteSocketDrain(socketId: number) { + this.events.push({ socketId, event: 'drain' }); + this.onDidFire.fire(); + return Promise.resolve(); + } + + override $remoteSocketEnd(socketId: number) { + this.events.push({ socketId, event: 'end' }); + this.onDidFire.fire(); + } + + expectEvent(test: (evt: any) => void, message: string) { + if (this.events.some(test)) { + return; + } + + const d = new DisposableStore(); + return new Promise(resolve => { + d.add(this.onDidFire.event(() => { + if (this.events.some(test)) { + return; + } + })); + d.add(disposableTimeout(() => { + throw new Error(`Expected ${message} but only had ${JSON.stringify(this.events, null, 2)}`); + }, 1000)); + }).finally(() => d.dispose()); + } + } + + setup(() => { + extHost = new ExtHostMock(); + half = { + onClose: new Emitter(), + onData: new Emitter(), + onEnd: new Emitter(), + }; + }); + + async function doConnect() { + const socket = ManagedSocket.connect(1, extHost, '/hello', 'world=true', '', half); + await extHost.expectEvent(evt => evt.data && evt.data.startsWith('GET ws://localhost/hello?world=true&skipWebSocketFrames=true HTTP/1.1\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key:'), 'websocket open event'); + half.onData.fire(VSBuffer.fromString('Opened successfully ;)\r\n\r\n')); + return await socket; + } + + test('connects', async () => { + await doConnect(); + }); + + test('includes trailing connection data', async () => { + const socketProm = ManagedSocket.connect(1, extHost, '/hello', 'world=true', '', half); + await extHost.expectEvent(evt => evt.data && evt.data.includes('GET ws://localhost'), 'websocket open event'); + half.onData.fire(VSBuffer.fromString('Opened successfully ;)\r\n\r\nSome trailing data')); + const socket = await socketProm; + + const data: string[] = []; + socket.onData(d => data.push(d.toString())); + await timeout(1); // allow microtasks to flush + assert.deepStrictEqual(data, ['Some trailing data']); + }); + + test('round trips data', async () => { + const socket = await doConnect(); + const data: string[] = []; + socket.onData(d => data.push(d.toString())); + + socket.write(VSBuffer.fromString('ping')); + await extHost.expectEvent(evt => evt.data === 'ping', 'expected ping'); + half.onData.fire(VSBuffer.fromString("pong")); + assert.deepStrictEqual(data, ['pong']); + }); + }); +}); diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index d5e709382ee..81999cd37be 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, RemoteConnectionType } 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 { IRemoteSocketFactoryService, RemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; +import { BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; import { VSBuffer } from 'vs/base/common/buffer'; import { IStoredWorkspace } from 'vs/platform/workspaces/common/workspaces'; import { UserDataProfileInitializer } from 'vs/workbench/services/userDataProfile/browser/userDataProfileInit'; @@ -257,7 +259,7 @@ export class BrowserMain extends Disposable { // Remote const connectionToken = environmentService.options.connectionToken || getCookieValue(connectionTokenCookieName); - const remoteAuthorityResolverService = new RemoteAuthorityResolverService(connectionToken, this.configuration.resourceUriProvider, productService, logService); + const remoteAuthorityResolverService = new RemoteAuthorityResolverService(!environmentService.expectsResolverExtension, connectionToken, this.configuration.resourceUriProvider, productService, logService); serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService); // Signing @@ -300,7 +302,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 remoteSocketFactoryService = new RemoteSocketFactoryService(); + remoteSocketFactoryService.register(RemoteConnectionType.WebSocket, new BrowserSocketFactory(this.configuration.webSocketFactory)); + serviceCollection.set(IRemoteSocketFactoryService, remoteSocketFactoryService); + const remoteAgentService = this._register(new RemoteAgentService(remoteSocketFactoryService, userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService)); serviceCollection.set(IRemoteAgentService, remoteAgentService); this._register(RemoteFileSystemProviderClient.register(remoteAgentService, fileService, logService)); @@ -514,7 +519,7 @@ export class BrowserMain extends Disposable { } } - private async createWorkspaceService(workspace: IAnyWorkspaceIdentifier, environmentService: IWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService, userDataProfilesService: IUserDataProfilesService, fileService: FileService, remoteAgentService: IRemoteAgentService, uriIdentityService: IUriIdentityService, logService: ILogService): Promise { + private async createWorkspaceService(workspace: IAnyWorkspaceIdentifier, environmentService: IBrowserWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService, userDataProfilesService: IUserDataProfilesService, fileService: FileService, remoteAgentService: IRemoteAgentService, uriIdentityService: IUriIdentityService, logService: ILogService): Promise { // Temporary workspaces do not exist on startup because they are // just in memory. As such, detect this case and eagerly create diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index eb348f8b08a..b4514fd71b1 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, RemoteConnectionType } 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 { RemoteSocketFactoryService, IRemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; 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 remoteSocketFactoryService = new RemoteSocketFactoryService(); + remoteSocketFactoryService.register(RemoteConnectionType.WebSocket, new BrowserSocketFactory(null)); + serviceCollection.set(IRemoteSocketFactoryService, remoteSocketFactoryService); + const remoteAgentService = this._register(new RemoteAgentService(remoteSocketFactoryService, userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService)); serviceCollection.set(IRemoteAgentService, remoteAgentService); // Remote Files diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index bc3a08c3cbd..17579d81bf2 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -45,6 +45,7 @@ import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userData import { updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import { VSBuffer } from 'vs/base/common/buffer'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; +import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; function getLocalUserConfigurationScopes(userDataProfile: IUserDataProfile, hasRemote: boolean): ConfigurationScope[] | undefined { return userDataProfile.isDefault @@ -105,7 +106,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat constructor( { remoteAuthority, configurationCache }: { remoteAuthority?: string; configurationCache: IConfigurationCache }, - environmentService: IWorkbenchEnvironmentService, + environmentService: IBrowserWorkbenchEnvironmentService, private readonly userDataProfileService: IUserDataProfileService, private readonly userDataProfilesService: IUserDataProfilesService, private readonly fileService: IFileService, 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..6c8d92f9a0f 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts @@ -40,12 +40,12 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteAgentService'; import { getSingleFolderWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { hash } from 'vs/base/common/hash'; import { FilePolicyService } from 'vs/platform/policy/common/filePolicyService'; 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 { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); @@ -60,7 +60,7 @@ suite('ConfigurationEditing', () => { let instantiationService: TestInstantiationService; let userDataProfileService: IUserDataProfileService; - let environmentService: IWorkbenchEnvironmentService; + let environmentService: IBrowserWorkbenchEnvironmentService; let fileService: IFileService; let workspaceService: WorkspaceService; let testObject: ConfigurationEditing; @@ -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..d15a25b1bb1 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -39,7 +39,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; -import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; +import { BrowserWorkbenchEnvironmentService, IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteAgentService'; import { RemoteAuthorityResolverService } from 'vs/platform/remote/browser/remoteAuthorityResolverService'; import { hash } from 'vs/base/common/hash'; @@ -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 { RemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; 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 RemoteSocketFactoryService(), 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 RemoteSocketFactoryService(), 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 RemoteSocketFactoryService(), 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); @@ -695,7 +696,7 @@ suite('WorkspaceService - Initialization', () => { suite('WorkspaceConfigurationService - Folder', () => { - let testObject: WorkspaceService, workspaceService: WorkspaceService, fileService: IFileService, environmentService: IWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService, instantiationService: TestInstantiationService; + let testObject: WorkspaceService, workspaceService: WorkspaceService, fileService: IFileService, environmentService: IBrowserWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService, instantiationService: TestInstantiationService; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); const disposables: DisposableStore = new DisposableStore(); @@ -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); @@ -1519,7 +1520,7 @@ suite('WorkspaceConfigurationService - Folder', () => { suite('WorkspaceConfigurationService - Profiles', () => { - let testObject: WorkspaceService, workspaceService: WorkspaceService, fileService: IFileService, environmentService: IWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService, instantiationService: TestInstantiationService; + let testObject: WorkspaceService, workspaceService: WorkspaceService, fileService: IFileService, environmentService: IBrowserWorkbenchEnvironmentService, userDataProfileService: IUserDataProfileService, instantiationService: TestInstantiationService; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); const disposables: DisposableStore = new DisposableStore(); @@ -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/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 553cbdeb5a5..4a8a93ff8bf 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -32,6 +32,11 @@ export interface IBrowserWorkbenchEnvironmentService extends IWorkbenchEnvironme * Options used to configure the workbench. */ readonly options?: IWorkbenchConstructionOptions; + + /** + * Gets whether a resolver extension is expected for the environment. + */ + readonly expectsResolverExtension: boolean; } export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvironmentService { @@ -41,6 +46,11 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi @memoize get remoteAuthority(): string | undefined { return this.options.remoteAuthority; } + @memoize + get expectsResolverExtension(): boolean { + return !!this.options.remoteAuthority?.includes('+') && !this.options.webSocketFactory; + } + @memoize get isBuilt(): boolean { return !!this.productService.commit; } diff --git a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts index 77580ee035b..810a38b3866 100644 --- a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts @@ -62,6 +62,9 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment @memoize get remoteAuthority() { return this.configuration.remoteAuthority; } + @memoize + get expectsResolverExtension() { return !!this.configuration.remoteAuthority?.includes('+'); } + @memoize get execPath() { return this.configuration.execPath; } diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 4fd780f4ff6..fa581f401e6 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -14,25 +14,28 @@ import { IAutomatedWindow, getLogs } from 'vs/platform/log/browser/log'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; +import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IWebWorkerExtensionHostDataProvider, IWebWorkerExtensionHostInitData, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; import { FetchFileSystemProvider } from 'vs/workbench/services/extensions/browser/webWorkerFileSystemProvider'; -import { AbstractExtensionService, IExtensionHostFactory, ResolvedExtensions } from 'vs/workbench/services/extensions/common/abstractExtensionService'; +import { AbstractExtensionService, IExtensionHostFactory, ResolvedExtensions, checkEnabledAndProposedAPI } from 'vs/workbench/services/extensions/common/abstractExtensionService'; import { ExtensionHostKind, ExtensionRunningPreference, IExtensionHostKindPicker, extensionHostKindToString, extensionRunningPreferenceToString } from 'vs/workbench/services/extensions/common/extensionHostKind'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { ExtensionRunningLocation } from 'vs/workbench/services/extensions/common/extensionRunningLocation'; -import { ExtensionRunningLocationTracker } from 'vs/workbench/services/extensions/common/extensionRunningLocationTracker'; +import { ExtensionRunningLocationTracker, filterExtensionDescriptions } from 'vs/workbench/services/extensions/common/extensionRunningLocationTracker'; import { ExtensionHostStartup, IExtensionHost, IExtensionService, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionsProposedApi } from 'vs/workbench/services/extensions/common/extensionsProposedApi'; import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { IRemoteExtensionHostDataProvider, RemoteExtensionHost } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IUserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; @@ -41,7 +44,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten constructor( @IInstantiationService instantiationService: IInstantiationService, @INotificationService notificationService: INotificationService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IBrowserWorkbenchEnvironmentService private readonly _browserEnvironmentService: IBrowserWorkbenchEnvironmentService, @ITelemetryService telemetryService: ITelemetryService, @IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService, @IFileService fileService: IFileService, @@ -58,13 +61,18 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, @IUserDataInitializationService private readonly _userDataInitializationService: IUserDataInitializationService, @IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService, + @IWorkspaceTrustManagementService private readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IRemoteExplorerService private readonly _remoteExplorerService: IRemoteExplorerService, ) { const extensionsProposedApi = instantiationService.createInstance(ExtensionsProposedApi); const extensionHostFactory = new BrowserExtensionHostFactory( + extensionsProposedApi, + () => this._scanWebExtensions(), () => this._getExtensions(), instantiationService, remoteAgentService, - remoteAuthorityResolverService + remoteAuthorityResolverService, + extensionEnablementService ); super( extensionsProposedApi, @@ -72,7 +80,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten new BrowserExtensionHostKindPicker(logService), instantiationService, notificationService, - environmentService, + _browserEnvironmentService, telemetryService, extensionEnablementService, fileService, @@ -84,7 +92,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten logService, remoteAgentService, remoteExtensionsScannerService, - lifecycleService + lifecycleService, + remoteAuthorityResolverService ); // Initialize installed extensions first and do it only after workbench is ready @@ -129,8 +138,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten return dedupExtensions(system, user, development, this._logService); } - protected async _resolveExtensions(): Promise { - // fetch the remote environment + protected async _resolveExtensionsDefault() { const [localExtensions, remoteExtensions] = await Promise.all([ this._scanWebExtensions(), this._remoteExtensionsScannerService.scanExtensions() @@ -139,6 +147,50 @@ export class ExtensionService extends AbstractExtensionService implements IExten return new ResolvedExtensions(localExtensions, remoteExtensions, /*hasLocalProcess*/false, /*allowRemoteExtensionsInLocalWebWorker*/true); } + protected async _resolveExtensions(): Promise { + if (!this._browserEnvironmentService.expectsResolverExtension) { + return this._resolveExtensionsDefault(); + } + + const remoteAuthority = this._environmentService.remoteAuthority!; + + // Now that the canonical URI provider has been registered, we need to wait for the trust state to be + // calculated. The trust state will be used while resolving the authority, however the resolver can + // override the trust state through the resolver result. + await this._workspaceTrustManagementService.workspaceResolved; + + + let resolverResult: ResolverResult; + try { + resolverResult = await this._resolveAuthorityInitial(remoteAuthority); + } catch (err) { + if (RemoteAuthorityResolverError.isHandled(err)) { + console.log(`Error handled: Not showing a notification for the error`); + } + this._remoteAuthorityResolverService._setResolvedAuthorityError(remoteAuthority, err); + + // Proceed with the local extension host + return this._resolveExtensionsDefault(); + } + + // set the resolved authority + this._remoteAuthorityResolverService._setResolvedAuthority(resolverResult.authority, resolverResult.options); + this._remoteExplorerService.setTunnelInformation(resolverResult.tunnelInformation); + + // monitor for breakage + const connection = this._remoteAgentService.getConnection(); + if (connection) { + connection.onDidStateChange(async (e) => { + if (e.type === PersistentConnectionEventType.ConnectionLost) { + this._remoteAuthorityResolverService._clearResolvedAuthority(remoteAuthority); + } + }); + connection.onReconnecting(() => this._resolveAuthorityAgain()); + } + + return this._resolveExtensionsDefault(); + } + protected async _onExtensionHostExit(code: number): Promise { // Dispose everything associated with the extension host this._doStopExtensionHosts(); @@ -149,15 +201,22 @@ export class ExtensionService extends AbstractExtensionService implements IExten automatedWindow.codeAutomationExit(code, await getLogs(this._fileService, this._environmentService)); } } + + protected async _resolveAuthority(remoteAuthority: string): Promise { + return this._resolveAuthorityOnExtensionHosts(ExtensionHostKind.LocalWebWorker, remoteAuthority); + } } class BrowserExtensionHostFactory implements IExtensionHostFactory { constructor( + private readonly _extensionsProposedApi: ExtensionsProposedApi, + private readonly _scanWebExtensions: () => Promise, private readonly _getExtensions: () => Promise, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, + @IWorkbenchExtensionEnablementService private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService, ) { } createExtensionHost(runningLocations: ExtensionRunningLocationTracker, runningLocation: ExtensionRunningLocation, isInitialStart: boolean): IExtensionHost | null { @@ -166,27 +225,39 @@ class BrowserExtensionHostFactory implements IExtensionHostFactory { return null; } case ExtensionHostKind.LocalWebWorker: { - return this._instantiationService.createInstance(WebWorkerExtensionHost, runningLocation, ExtensionHostStartup.EagerAutoStart, this._createLocalExtensionHostDataProvider(runningLocations, runningLocation)); + return this._instantiationService.createInstance(WebWorkerExtensionHost, runningLocation, ExtensionHostStartup.EagerAutoStart, this._createLocalExtensionHostDataProvider(runningLocations, runningLocation, isInitialStart)); } 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; } } } - private _createLocalExtensionHostDataProvider(runningLocations: ExtensionRunningLocationTracker, desiredRunningLocation: ExtensionRunningLocation): IWebWorkerExtensionHostDataProvider { + private _createLocalExtensionHostDataProvider(runningLocations: ExtensionRunningLocationTracker, desiredRunningLocation: ExtensionRunningLocation, isInitialStart: boolean): IWebWorkerExtensionHostDataProvider { return { getInitData: async (): Promise => { - const allExtensions = await this._getExtensions(); - const localWebWorkerExtensions = runningLocations.filterByRunningLocation(allExtensions, desiredRunningLocation); - return { - allExtensions: allExtensions, - myExtensions: localWebWorkerExtensions.map(extension => extension.identifier) - }; + if (isInitialStart) { + // Here we load even extensions that would be disabled by workspace trust + const localExtensions = checkEnabledAndProposedAPI(this._extensionEnablementService, this._extensionsProposedApi, await this._scanWebExtensions(), /* ignore workspace trust */true); + const runningLocation = runningLocations.computeRunningLocation(localExtensions, [], false); + const myExtensions = filterExtensionDescriptions(localExtensions, runningLocation, extRunningLocation => desiredRunningLocation.equals(extRunningLocation)); + return { + allExtensions: localExtensions, + myExtensions: myExtensions.map(extension => extension.identifier) + }; + } else { + // restart case + const allExtensions = await this._getExtensions(); + const myExtensions = runningLocations.filterByRunningLocation(allExtensions, desiredRunningLocation); + return { + allExtensions: allExtensions, + myExtensions: myExtensions.map(extension => extension.identifier) + }; + } } }; } diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index cd829cd14f5..33a46e24b6d 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -9,6 +9,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as perf from 'vs/base/common/performance'; import { isEqualOrParent } from 'vs/base/common/resources'; +import { StopWatch } from 'vs/base/common/stopwatch'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -21,6 +22,7 @@ import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IProductService } from 'vs/platform/product/common/productService'; +import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode, ResolverResult, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -30,6 +32,7 @@ import { ExtensionDescriptionRegistryLock, IActivationEventsReader, LockableExte import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { ExtensionHostKind, ExtensionRunningPreference, IExtensionHostKindPicker, extensionHostKindToString } from 'vs/workbench/services/extensions/common/extensionHostKind'; import { IExtensionHostManager, createExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager'; +import { IResolveAuthorityErrorResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { ExtensionRunningLocation, LocalProcessRunningLocation, LocalWebWorkerRunningLocation, RemoteRunningLocation } from 'vs/workbench/services/extensions/common/extensionRunningLocation'; import { ExtensionRunningLocationTracker, filterExtensionIdentifiers } from 'vs/workbench/services/extensions/common/extensionRunningLocationTracker'; @@ -79,6 +82,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx private _extensionHostManagers: IExtensionHostManager[] = []; + private _resolveAuthorityAttempt: number = 0; + constructor( private readonly _extensionsProposedApi: ExtensionsProposedApi, private readonly _extensionHostFactory: IExtensionHostFactory, @@ -97,7 +102,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx @ILogService protected readonly _logService: ILogService, @IRemoteAgentService protected readonly _remoteAgentService: IRemoteAgentService, @IRemoteExtensionsScannerService protected readonly _remoteExtensionsScannerService: IRemoteExtensionsScannerService, - @ILifecycleService private readonly _lifecycleService: ILifecycleService + @ILifecycleService private readonly _lifecycleService: ILifecycleService, + @IRemoteAuthorityResolverService protected readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, ) { super(); @@ -521,6 +527,98 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._onDidChangeExtensionsStatus.fire(this._registry.getAllExtensionDescriptions().map(e => e.identifier)); } + //#region remote authority resolving + + protected async _resolveAuthorityInitial(remoteAuthority: string): Promise { + const MAX_ATTEMPTS = 5; + + for (let attempt = 1; ; attempt++) { + try { + return this._resolveAuthorityWithLogging(remoteAuthority); + } catch (err) { + if (RemoteAuthorityResolverError.isNoResolverFound(err)) { + // There is no point in retrying if there is no resolver found + throw err; + } + + if (RemoteAuthorityResolverError.isNotAvailable(err)) { + // The resolver is not available and asked us to not retry + throw err; + } + + if (attempt >= MAX_ATTEMPTS) { + // Too many failed attempts, give up + throw err; + } + } + } + } + + protected async _resolveAuthorityAgain(): Promise { + const remoteAuthority = this._environmentService.remoteAuthority; + if (!remoteAuthority) { + return; + } + + this._remoteAuthorityResolverService._clearResolvedAuthority(remoteAuthority); + try { + const result = await this._resolveAuthorityWithLogging(remoteAuthority); + this._remoteAuthorityResolverService._setResolvedAuthority(result.authority, result.options); + } catch (err) { + this._remoteAuthorityResolverService._setResolvedAuthorityError(remoteAuthority, err); + } + } + + private async _resolveAuthorityWithLogging(remoteAuthority: string): Promise { + const authorityPrefix = getRemoteAuthorityPrefix(remoteAuthority); + const sw = StopWatch.create(false); + this._logService.info(`Invoking resolveAuthority(${authorityPrefix})...`); + try { + perf.mark(`code/willResolveAuthority/${authorityPrefix}`); + const result = await this._resolveAuthority(remoteAuthority); + perf.mark(`code/didResolveAuthorityOK/${authorityPrefix}`); + this._logService.info(`resolveAuthority(${authorityPrefix}) returned '${result.authority.connectTo}' after ${sw.elapsed()} ms`); + return result; + } catch (err) { + perf.mark(`code/didResolveAuthorityError/${authorityPrefix}`); + this._logService.error(`resolveAuthority(${authorityPrefix}) returned an error after ${sw.elapsed()} ms`, err); + throw err; + } + } + + protected async _resolveAuthorityOnExtensionHosts(kind: ExtensionHostKind, remoteAuthority: string): Promise { + + const extensionHosts = this._getExtensionHostManagers(kind); + if (extensionHosts.length === 0) { + // no local process extension hosts + throw new Error(`Cannot resolve authority`); + } + + this._resolveAuthorityAttempt++; + const results = await Promise.all(extensionHosts.map(extHost => extHost.resolveAuthority(remoteAuthority, this._resolveAuthorityAttempt))); + + let bestErrorResult: IResolveAuthorityErrorResult | null = null; + for (const result of results) { + if (result.type === 'ok') { + return result.value; + } + if (!bestErrorResult) { + bestErrorResult = result; + continue; + } + const bestErrorIsUnknown = (bestErrorResult.error.code === RemoteAuthorityResolverErrorCode.Unknown); + const errorIsUnknown = (result.error.code === RemoteAuthorityResolverErrorCode.Unknown); + if (bestErrorIsUnknown && !errorIsUnknown) { + bestErrorResult = result; + } + } + + // we can only reach this if there is an error + throw new RemoteAuthorityResolverError(bestErrorResult!.error.message, bestErrorResult!.error.code, bestErrorResult!.error.detail); + } + + //#endregion + //#region Stopping / Starting / Restarting public stopExtensionHosts(): Promise; @@ -1019,6 +1117,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx protected abstract _resolveExtensions(): Promise; protected abstract _scanSingleExtension(extension: IExtension): Promise; protected abstract _onExtensionHostExit(code: number): void; + protected abstract _resolveAuthority(remoteAuthority: string): Promise; } export class ResolvedExtensions { diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 7a1d76d77cc..d4db167e5e5 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -409,7 +409,7 @@ 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.connectTo}`); } else { logError(`returned an error`, resolverResult.error); } diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index 3d929ec2b21..70aa7236fb2 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 { IRemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; 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, + @IRemoteSocketFactoryService private readonly remoteSocketFactoryService: IRemoteSocketFactoryService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @@ -87,13 +88,13 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { 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 }; + return { connectTo: authority.connectTo, connectionToken: authority.connectionToken }; } }, + remoteSocketFactoryService: this.remoteSocketFactoryService, signService: this._signService, logService: this._logService, ipcLogger: null diff --git a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts index f93db1e4a9a..e46774df584 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts @@ -8,7 +8,6 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Schemas } from 'vs/base/common/network'; import * as performance from 'vs/base/common/performance'; import { isCI } from 'vs/base/common/platform'; -import { StopWatch } from 'vs/base/common/stopwatch'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; @@ -29,7 +28,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, RemoteConnectionType, RemoteAuthorityResolverError, 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'; @@ -44,7 +43,6 @@ import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/commo import { ExtensionHostKind, ExtensionRunningPreference, IExtensionHostKindPicker, extensionHostKindToString, extensionRunningPreferenceToString } from 'vs/workbench/services/extensions/common/extensionHostKind'; import { IExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager'; import { ExtensionHostExitCode } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; -import { IResolveAuthorityErrorResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { ExtensionRunningLocation, LocalProcessRunningLocation, LocalWebWorkerRunningLocation } from 'vs/workbench/services/extensions/common/extensionRunningLocation'; import { ExtensionRunningLocationTracker, filterExtensionDescriptions } from 'vs/workbench/services/extensions/common/extensionRunningLocationTracker'; @@ -62,7 +60,6 @@ export class NativeExtensionService extends AbstractExtensionService implements private readonly _extensionScanner: CachedExtensionScanner; private readonly _localCrashTracker = new ExtensionHostCrashTracker(); - private _resolveAuthorityAttempt: number = 0; constructor( @IInstantiationService instantiationService: IInstantiationService, @@ -80,7 +77,7 @@ export class NativeExtensionService extends AbstractExtensionService implements @IRemoteAgentService remoteAgentService: IRemoteAgentService, @IRemoteExtensionsScannerService remoteExtensionsScannerService: IRemoteExtensionsScannerService, @ILifecycleService lifecycleService: ILifecycleService, - @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, + @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, @INativeHostService private readonly _nativeHostService: INativeHostService, @IHostService private readonly _hostService: IHostService, @IRemoteExplorerService private readonly _remoteExplorerService: IRemoteExplorerService, @@ -98,7 +95,7 @@ export class NativeExtensionService extends AbstractExtensionService implements extensionEnablementService, configurationService, remoteAgentService, - _remoteAuthorityResolverService + remoteAuthorityResolverService ); super( extensionsProposedApi, @@ -118,7 +115,8 @@ export class NativeExtensionService extends AbstractExtensionService implements logService, remoteAgentService, remoteExtensionsScannerService, - lifecycleService + lifecycleService, + remoteAuthorityResolverService ); this._extensionScanner = extensionScanner; @@ -272,7 +270,7 @@ export class NativeExtensionService extends AbstractExtensionService implements // --- impl - private async _resolveAuthority(remoteAuthority: string): Promise { + protected async _resolveAuthority(remoteAuthority: string): Promise { const authorityPlusIndex = remoteAuthority.indexOf('+'); if (authorityPlusIndex === -1) { @@ -281,40 +279,17 @@ export class NativeExtensionService extends AbstractExtensionService implements return { authority: { authority: remoteAuthority, - host, - port, + connectTo: { + type: RemoteConnectionType.WebSocket, + host, + port + }, connectionToken: undefined } }; } - const localProcessExtensionHosts = this._getExtensionHostManagers(ExtensionHostKind.LocalProcess); - if (localProcessExtensionHosts.length === 0) { - // no local process extension hosts - throw new Error(`Cannot resolve authority`); - } - - this._resolveAuthorityAttempt++; - const results = await Promise.all(localProcessExtensionHosts.map(extHost => extHost.resolveAuthority(remoteAuthority, this._resolveAuthorityAttempt))); - - let bestErrorResult: IResolveAuthorityErrorResult | null = null; - for (const result of results) { - if (result.type === 'ok') { - return result.value; - } - if (!bestErrorResult) { - bestErrorResult = result; - continue; - } - const bestErrorIsUnknown = (bestErrorResult.error.code === RemoteAuthorityResolverErrorCode.Unknown); - const errorIsUnknown = (result.error.code === RemoteAuthorityResolverErrorCode.Unknown); - if (bestErrorIsUnknown && !errorIsUnknown) { - bestErrorResult = result; - } - } - - // we can only reach this if there is an error - throw new RemoteAuthorityResolverError(bestErrorResult!.error.message, bestErrorResult!.error.code, bestErrorResult!.error.detail); + return this._resolveAuthorityOnExtensionHosts(ExtensionHostKind.LocalProcess, remoteAuthority); } private async _getCanonicalURI(remoteAuthority: string, uri: URI): Promise { @@ -343,63 +318,6 @@ export class NativeExtensionService extends AbstractExtensionService implements throw new Error(`Cannot get canonical URI because no extension is installed to resolve ${getRemoteAuthorityPrefix(remoteAuthority)}`); } - private async _resolveAuthorityInitial(remoteAuthority: string): Promise { - const MAX_ATTEMPTS = 5; - - for (let attempt = 1; ; attempt++) { - try { - return this._resolveAuthorityWithLogging(remoteAuthority); - } catch (err) { - if (RemoteAuthorityResolverError.isNoResolverFound(err)) { - // There is no point in retrying if there is no resolver found - throw err; - } - - if (RemoteAuthorityResolverError.isNotAvailable(err)) { - // The resolver is not available and asked us to not retry - throw err; - } - - if (attempt >= MAX_ATTEMPTS) { - // Too many failed attempts, give up - throw err; - } - } - } - } - - private async _resolveAuthorityAgain(): Promise { - const remoteAuthority = this._environmentService.remoteAuthority; - if (!remoteAuthority) { - return; - } - - this._remoteAuthorityResolverService._clearResolvedAuthority(remoteAuthority); - try { - const result = await this._resolveAuthorityWithLogging(remoteAuthority); - this._remoteAuthorityResolverService._setResolvedAuthority(result.authority, result.options); - } catch (err) { - this._remoteAuthorityResolverService._setResolvedAuthorityError(remoteAuthority, err); - } - } - - private async _resolveAuthorityWithLogging(remoteAuthority: string): Promise { - const authorityPrefix = getRemoteAuthorityPrefix(remoteAuthority); - const sw = StopWatch.create(false); - this._logService.info(`Invoking resolveAuthority(${authorityPrefix})...`); - try { - 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`); - return result; - } catch (err) { - performance.mark(`code/didResolveAuthorityError/${authorityPrefix}`); - this._logService.error(`resolveAuthority(${authorityPrefix}) returned an error after ${sw.elapsed()} ms`, err); - throw err; - } - } - protected async _resolveExtensions(): Promise { this._extensionScanner.startScanningExtensions(); @@ -631,7 +549,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/extensions/test/browser/extensionService.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts index 7e833bfaafd..c6833a93971 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts @@ -19,6 +19,8 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import product from 'vs/platform/product/common/product'; import { IProductService } from 'vs/platform/product/common/productService'; +import { RemoteAuthorityResolverService } from 'vs/platform/remote/browser/remoteAuthorityResolverService'; +import { IRemoteAuthorityResolverService, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; @@ -145,6 +147,7 @@ suite('ExtensionService', () => { @IRemoteAgentService remoteAgentService: IRemoteAgentService, @IRemoteExtensionsScannerService remoteExtensionsScannerService: IRemoteExtensionsScannerService, @ILifecycleService lifecycleService: ILifecycleService, + @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, ) { const extensionsProposedApi = instantiationService.createInstance(ExtensionsProposedApi); const extensionHostFactory = new class implements IExtensionHostFactory { @@ -172,7 +175,8 @@ suite('ExtensionService', () => { logService, remoteAgentService, remoteExtensionsScannerService, - lifecycleService + lifecycleService, + remoteAuthorityResolverService ); } @@ -205,6 +209,9 @@ suite('ExtensionService', () => { protected _onExtensionHostExit(code: number): void { throw new Error('Method not implemented.'); } + protected _resolveAuthority(remoteAuthority: string): Promise { + throw new Error('Method not implemented.'); + } } let disposables: DisposableStore; @@ -236,6 +243,7 @@ suite('ExtensionService', () => { [IUserDataProfileService, TestUserDataProfileService], [IUriIdentityService, UriIdentityService], [IRemoteExtensionsScannerService, TestRemoteExtensionsScannerService], + [IRemoteAuthorityResolverService, RemoteAuthorityResolverService] ]); extService = instantiationService.get(IExtensionService); }); diff --git a/src/vs/workbench/services/remote/browser/remoteAgentService.ts b/src/vs/workbench/services/remote/browser/remoteAgentService.ts index 3cc54d49eea..0170a861289 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 { IRemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; export class RemoteAgentService extends AbstractRemoteAgentService implements IRemoteAgentService { constructor( - webSocketFactory: IWebSocketFactory | null | undefined, + @IRemoteSocketFactoryService remoteSocketFactoryService: IRemoteSocketFactoryService, @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(remoteSocketFactoryService, 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..9550c6a76d6 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 { IRemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; 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, + @IRemoteSocketFactoryService private readonly remoteSocketFactoryService: IRemoteSocketFactoryService, @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.remoteSocketFactoryService, 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 _remoteSocketFactoryService: IRemoteSocketFactoryService, private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, private readonly _signService: ISignService, private readonly _logService: ILogService @@ -196,7 +195,6 @@ class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection const options: IConnectionOptions = { commit: this._commit, quality: this._quality, - socketFactory: this._socketFactory, addressProvider: { getAddress: async () => { if (firstCall) { @@ -205,9 +203,10 @@ class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection this._onReconnecting.fire(undefined); } const { authority } = await this._remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority); - return { host: authority.host, port: authority.port, connectionToken: authority.connectionToken }; + return { connectTo: authority.connectTo, connectionToken: authority.connectionToken }; } }, + remoteSocketFactoryService: this._remoteSocketFactoryService, signService: this._signService, logService: this._logService, ipcLogger: false ? new IPCLogger(`Local \u2192 Remote`, `Remote \u2192 Local`) : null 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/electron-sandbox/remoteAgentService.ts b/src/vs/workbench/services/remote/electron-sandbox/remoteAgentService.ts index 85a638d5937..550b988c939 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, RemoteConnectionType, 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 { IRemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; export class RemoteAgentService extends AbstractRemoteAgentService implements IRemoteAgentService { constructor( + @IRemoteSocketFactoryService remoteSocketFactoryService: IRemoteSocketFactoryService, @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(remoteSocketFactoryService, 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 !== RemoteConnectionType.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 4823dc4fe3d..b1d0d2edb5e 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -156,7 +156,6 @@ import { IExtensionHostExitInfo, IRemoteAgentConnection, IRemoteAgentService } f import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { ExtensionType, IExtension, IExtensionDescription, IRelaxedExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; -import { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { ILayoutOffsetInfo } from 'vs/platform/layout/browser/layoutService'; import { IUserDataProfile, IUserDataProfilesService, toUserDataProfile, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; @@ -167,6 +166,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 { IRemoteSocketFactoryService, RemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined); @@ -325,6 +325,7 @@ export function workbenchInstantiationService( instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); instantiationService.stub(ITerminalInstanceService, new TestTerminalInstanceService()); instantiationService.stub(IElevatedFileService, new BrowserElevatedFileService()); + instantiationService.stub(IRemoteSocketFactoryService, new RemoteSocketFactoryService()); return instantiationService; } @@ -1938,10 +1939,6 @@ export class TestRemoteAgentService implements IRemoteAgentService { declare readonly _serviceBrand: undefined; - socketFactory: ISocketFactory = { - connect() { } - }; - getConnection(): IRemoteAgentConnection | null { return null; } async getEnvironment(): Promise { return null; } async getRawEnvironment(): Promise { return null; } diff --git a/src/vscode-dts/vscode.proposed.resolvers.d.ts b/src/vscode-dts/vscode.proposed.resolvers.d.ts index c145c600667..d7529dd871f 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; + + send: (data: Uint8Array) => void; + end: () => void; + drain?: () => Thenable; + } + + 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;