diff --git a/src/vs/platform/remote/common/remoteSocketFactoryService.ts b/src/vs/platform/remote/common/remoteSocketFactoryService.ts index 78160b31938..6d6eacfc854 100644 --- a/src/vs/platform/remote/common/remoteSocketFactoryService.ts +++ b/src/vs/platform/remote/common/remoteSocketFactoryService.ts @@ -3,6 +3,7 @@ * 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'; @@ -18,7 +19,7 @@ export interface IRemoteSocketFactoryService { * @param factory function that returns the socket factory, or undefined if * it can't handle the data. */ - register(type: T, factory: ISocketFactory): void; + register(type: T, factory: ISocketFactory): IDisposable; connect(connectTo: RemoteConnection, path: string, query: string, debugLabel: string): Promise; } @@ -33,9 +34,15 @@ export class RemoteSocketFactoryService implements IRemoteSocketFactoryService { private readonly factories: { [T in RemoteConnectionType]?: ISocketFactory[] } = {}; - public register(type: T, factory: ISocketFactory): void { + 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 { 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 d06644993d4..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, RemoteConnectionType } 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'; @@ -25,7 +25,8 @@ import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/exten import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { IExtensionHostProxy, IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; import { ActivationKind, ExtensionActivationReason, IExtensionService, IInternalExtensionService, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions'; -import { extHostNamedCustomer, IExtHostContext, IInternalExtHostContext, IManagedSocketCallbacks } from 'vs/workbench/services/extensions/common/extHostCustomers'; +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'; @@ -34,7 +35,6 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha private readonly _extensionHostKind: ExtensionHostKind; private readonly _internalExtensionService: IInternalExtensionService; - private readonly _managedSocketCallbacks: IManagedSocketCallbacks; constructor( extHostContext: IExtHostContext, @@ -51,7 +51,6 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha const internalExtHostContext = (extHostContext); this._internalExtensionService = internalExtHostContext.internalExtensionService; - this._managedSocketCallbacks = internalExtHostContext.managedSocketCallbacks; internalExtHostContext._setExtensionHostProxy( new ExtensionHostProxy(extHostContext.getProxy(ExtHostContext.ExtHostExtensionService)) ); @@ -61,18 +60,6 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha public dispose(): void { } - $onDidRemoteSocketHaveData(id: number, data: VSBuffer): void { - this._managedSocketCallbacks.onDidRemoteSocketHaveData(id, data); - } - - $onDidRemoteSocketClose(id: number, error: string | undefined): void { - this._managedSocketCallbacks.onDidRemoteSocketClose(id, error ? new Error(error) : undefined); - } - - $onDidRemoteSocketEnd(id: number): void { - this._managedSocketCallbacks.onDidRemoteSocketEnd(id); - } - $getExtension(extensionId: string) { return this._extensionService.getExtension(extensionId); } @@ -214,13 +201,7 @@ class ExtensionHostProxy implements IExtensionHostProxy { ) { } async resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise { - const resolved = await this._actual.$resolveAuthority(remoteAuthority, resolveAttempt); - if (resolved.type === 'ok') { - resolved.value.authority.toString = function () { - return this.connectTo.type === RemoteConnectionType.Managed ? `ManagedSocket#${this.connectTo.id}` : `${this.connectTo.host}:${this.connectTo.type}`; - }; - } - + const resolved = reviveResolveAuthorityResult(await this._actual.$resolveAuthority(remoteAuthority, resolveAttempt)); return resolved; } async getCanonicalURI(remoteAuthority: string, uri: URI): Promise { @@ -257,16 +238,32 @@ class ExtensionHostProxy implements IExtensionHostProxy { test_down(size: number): Promise { return this._actual.$test_down(size); } - openRemoteSocket(factoryId: number): Promise { - return this._actual.$openRemoteSocket(factoryId); - } - remoteSocketWrite(socketId: number, buffer: VSBuffer): void { - return this._actual.$remoteSocketWrite(socketId, buffer); - } - remoteSocketEnd(socketId: number): void { - return this._actual.$remoteSocketEnd(socketId); - } - remoteSocketDrain(socketId: number): Promise { - return this._actual.$remoteSocketDrain(socketId); +} + +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..ef532391c60 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadManagedSockets.ts @@ -0,0 +1,178 @@ +/*--------------------------------------------------------------------------------------------- + * 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 } 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 scoketFactory = 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, scoketFactory)); + + } + + 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(); + this._remoteSockets.delete(socketId); + } +} + +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: { + onClose: Emitter; + onData: Emitter; + onEnd: Emitter; + } + ): Promise { + const socket = new ManagedSocket(socketId, proxy, debugLabel, half.onClose, half.onData, half.onEnd); + + socket.write(VSBuffer.fromString(makeRawSocketHeaders(path, query, debugLabel))); + + const d = new DisposableStore(); + return new Promise((resolve, reject) => { + d.add(socket.onData(d => { + if (d.indexOf(socketRawEndHeaderSequence) !== -1) { + resolve(socket); + } + })); + + d.add(socket.onClose(err => reject(err ?? new Error('socket closed')))); + d.add(socket.onEnd(() => reject(new Error('socket ended')))); + }).finally(() => d.dispose()); + } + + public onData: Event; + public onClose: Event; + public onEnd: Event; + + private readonly didDisposeEmitter = this._register(new Emitter()); + public onDidDispose = this.didDisposeEmitter.event; + + private ended = false; + + private constructor( + private readonly socketId: number, + private readonly proxy: ExtHostManagedSocketsShape, + private readonly debugLabel: string, + onCloseEmitter: Emitter, + onDataEmitter: Emitter, + onEndEmitter: Emitter, + ) { + super(); + this.onClose = this._register(onCloseEmitter).event; + this.onData = this._register(onDataEmitter).event; + this.onEnd = this._register(onEndEmitter).event; + } + + write(buffer: VSBuffer): void { + this.proxy.$remoteSocketWrite(this.socketId, buffer); + } + + end(): void { + this.ended = true; + this.proxy.$remoteSocketEnd(this.socketId); + } + + drain(): Promise { + return this.proxy.$remoteSocketDrain(this.socketId); + } + + traceSocketEvent(type: SocketDiagnosticsEventType, data?: any): void { + SocketDiagnostics.traceSocketEvent(this, this.debugLabel, type, data); + } + + override dispose(): void { + if (!this.ended) { + this.proxy.$remoteSocketEnd(this.socketId); + } + + this.didDisposeEmitter.fire(); + super.dispose(); + } +} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 22f6ed8d346..3222ad9483f 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)); 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 a6eaf0bf1a7..09f8c4677bf 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -952,6 +952,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, @@ -1246,10 +1261,6 @@ export interface MainThreadExtensionServiceShape extends IDisposable { $onExtensionRuntimeError(extensionId: ExtensionIdentifier, error: SerializedError): void; $setPerformanceMarks(marks: performance.PerformanceMark[]): Promise; $asBrowserUri(uri: UriComponents): Promise; - - $onDidRemoteSocketHaveData(id: number, data: VSBuffer): void; - $onDidRemoteSocketClose(id: number, error: string | undefined): void; - $onDidRemoteSocketEnd(id: number): void; } export interface SCMProviderFeatures { @@ -1589,7 +1600,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. */ @@ -1606,11 +1617,6 @@ export interface ExtHostExtensionServiceShape { $test_latency(n: number): Promise; $test_up(b: VSBuffer): Promise; $test_down(size: number): Promise; - - $openRemoteSocket(factoryId: number): Promise; - $remoteSocketWrite(socketId: number, buffer: VSBuffer): void; - $remoteSocketEnd(socketId: number): void; - $remoteSocketDrain(socketId: number): Promise; } export interface FileSystemEvents { @@ -2512,6 +2518,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'), @@ -2573,6 +2580,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 16548fdb328..75db96a50ca 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -8,7 +8,7 @@ import * as path from 'vs/base/common/path'; import * as performance from 'vs/base/common/performance'; import { originalFSPath, joinPath, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { asPromise, Barrier, IntervalTimer, timeout } from 'vs/base/common/async'; -import { dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { dispose, toDisposable, Disposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; @@ -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` */ @@ -79,8 +81,6 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme readonly _serviceBrand: undefined; - private static remoteSocketIdCounter = 0; - abstract readonly extensionRuntime: ExtensionRuntime; private readonly _onDidChangeRemoteConnectionData = this._register(new Emitter()); @@ -120,11 +120,6 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme private _started: boolean; private _isTerminating: boolean = false; private _remoteConnectionData: IRemoteConnectionData | null; - private readonly _managedSocketFactories: Map Thenable>; - private readonly _managedRemoteSockets: Map; constructor( @IInstantiationService instaService: IInstantiationService, @@ -137,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; @@ -198,8 +194,6 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._resolvers = Object.create(null); this._started = false; this._remoteConnectionData = this._initData.remote.connectionData; - this._managedSocketFactories = new Map(); - this._managedRemoteSockets = new Map(); } public getRemoteConnectionData(): IRemoteConnectionData | null { @@ -800,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}`); @@ -854,11 +848,16 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme connectionToken: result.connectionToken }; } else { - const factoryId = AbstractExtHostExtensionService.remoteSocketIdCounter++; - this._managedSocketFactories.set(factoryId, result.makeConnection); + // 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(factoryId), + connectTo: new ManagedRemoteConnection(socketFactoryId), connectionToken: result.connectionToken }; } @@ -866,7 +865,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return { type: 'ok', value: { - authority, + authority: authority as Dto, options, tunnelInformation, } @@ -889,47 +888,6 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } } - public async $openRemoteSocket(factoryId: number): Promise { - const factory = this._managedSocketFactories.get(factoryId); - if (!factory) { - throw new Error(`No socket factory with id ${factoryId}`); - } - - const id = AbstractExtHostExtensionService.remoteSocketIdCounter++; - const socket = await factory(); - const disposable = new DisposableStore(); - - this._managedRemoteSockets.set(id, { object: socket, disposer: disposable }); - disposable.add(toDisposable(() => this._managedRemoteSockets.delete(id))); - disposable.add(socket.onDidEnd(() => { - this._mainThreadExtensionsProxy.$onDidRemoteSocketEnd(id); - disposable.dispose(); - })); - disposable.add(socket.onDidClose(e => { - this._mainThreadExtensionsProxy.$onDidRemoteSocketClose(id, e?.stack ?? e?.message); - disposable.dispose(); - })); - disposable.add(socket.onDidReceiveMessage(e => this._mainThreadExtensionsProxy.$onDidRemoteSocketHaveData(id, VSBuffer.wrap(e)))); - - return id; - } - - public $remoteSocketDrain(id: number): Promise { - return this._managedRemoteSockets.get(id)?.object.drainHandler?.() ?? Promise.resolve(); - } - - public $remoteSocketEnd(id: number): void { - const socket = this._managedRemoteSockets.get(id); - if (socket) { - socket.object.endHandler(); - socket.disposer.dispose(); - } - } - - public $remoteSocketWrite(id: number, buffer: VSBuffer): void { - this._managedRemoteSockets.get(id)?.object.dataHandler(buffer.buffer); - } - public async $getCanonicalURI(remoteAuthority: string, uriComponents: UriComponents): Promise { this._logService.info(`$getCanonicalURI invoked for authority (${getRemoteAuthorityPrefix(remoteAuthority)})`); diff --git a/src/vs/workbench/api/common/extHostManagedSockets.ts b/src/vs/workbench/api/common/extHostManagedSockets.ts new file mode 100644 index 00000000000..ccf2c7be634 --- /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.dataHandler(buffer.buffer); + } + + $remoteSocketEnd(socketId: number): void { + const socket = this._managedRemoteSockets.get(socketId); + if (socket) { + socket.actual.endHandler(); + socket.dispose(); + } + } + + $remoteSocketDrain(socketId: number): Promise { + return this._managedRemoteSockets.get(socketId)?.actual.drainHandler?.() ?? Promise.resolve(); + } +} + +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/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index b5ff879733a..9d789d8f995 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -255,7 +255,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten performance.mark(`code/willResolveAuthority/${authorityPrefix}`); const result = await this._resolveAuthority(remoteAuthority); performance.mark(`code/didResolveAuthorityOK/${authorityPrefix}`); - this._logService.info(`resolveAuthority(${authorityPrefix}) returned '${result.authority}' after ${sw.elapsed()} ms`); + this._logService.info(`resolveAuthority(${authorityPrefix}) returned '${result.authority.connectTo}' after ${sw.elapsed()} ms`); return result; } catch (err) { performance.mark(`code/didResolveAuthorityError/${authorityPrefix}`); diff --git a/src/vs/workbench/services/extensions/common/extHostCustomers.ts b/src/vs/workbench/services/extensions/common/extHostCustomers.ts index 34123e72b15..0d3104fcded 100644 --- a/src/vs/workbench/services/extensions/common/extHostCustomers.ts +++ b/src/vs/workbench/services/extensions/common/extHostCustomers.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSBuffer } from 'vs/base/common/buffer'; import { IDisposable } from 'vs/base/common/lifecycle'; import { BrandedService, IConstructorSignature } from 'vs/platform/instantiation/common/instantiation'; import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensionHostKind'; @@ -16,15 +15,8 @@ export interface IExtHostContext extends IRPCProtocol { readonly extensionHostKind: ExtensionHostKind; } -export interface IManagedSocketCallbacks { - onDidRemoteSocketHaveData(id: number, data: VSBuffer): void; - onDidRemoteSocketEnd(id: number): void; - onDidRemoteSocketClose(id: number, error: Error | undefined): void; -} - export interface IInternalExtHostContext extends IExtHostContext { readonly internalExtensionService: IInternalExtensionService; - readonly managedSocketCallbacks: IManagedSocketCallbacks; _setExtensionHostProxy(extensionHostProxy: IExtensionHostProxy): void; _setAllMainProxyIdentifiers(mainProxyIdentifiers: ProxyIdentifier[]): void; } diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 401e5d3e2ec..d4db167e5e5 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -11,15 +11,13 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI } from 'vs/base/common/uri'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { ISocket, SocketCloseEvent, SocketCloseEventType } from 'vs/base/parts/ipc/common/ipc.net'; import * as nls from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { ManagedRemoteConnection, RemoteConnectionType, RemoteAuthorityResolverErrorCode, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { IRemoteSocketFactoryService, ISocketFactory } from 'vs/platform/remote/common/remoteSocketFactoryService'; +import { RemoteAuthorityResolverErrorCode, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -31,7 +29,6 @@ import { ExtensionRunningLocation } from 'vs/workbench/services/extensions/commo import { ActivationKind, ExtensionActivationReason, ExtensionHostExtensions, ExtensionHostStartup, IExtensionHost, IInternalExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { Proxied, ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { IRPCProtocolLogger, RPCProtocol, RequestInitiator, ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol'; -import { ManagedSocket } from 'vs/workbench/services/remote/common/managedSocket'; // Enable to see detailed message communication between window and extension host const LOG_EXTENSION_HOST_COMMUNICATION = false; @@ -88,13 +85,6 @@ type ExtensionHostStartupEvent = { errorStack?: string; }; - -interface RemoteSocketHalf { - onData: Emitter; - onClose: Emitter; - onEnd: Emitter; -} - class ExtensionHostManager extends Disposable implements IExtensionHostManager { public readonly onDidExit: Event<[number, string | null]>; @@ -112,7 +102,6 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { private readonly _extensionHost: IExtensionHost; private _proxy: Promise | null; private _hasStarted = false; - private readonly _remoteSockets = new Map(); public get kind(): ExtensionHostKind { return this._extensionHost.runningLocation.kind; @@ -126,7 +115,6 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { extensionHost: IExtensionHost, initialActivationEvents: string[], private readonly _internalExtensionService: IInternalExtensionService, - @IRemoteSocketFactoryService private readonly _remoteSocketFactoryService: IRemoteSocketFactoryService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @@ -299,23 +287,6 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { //#region internal internalExtensionService: this._internalExtensionService, - managedSocketCallbacks: { - onDidRemoteSocketHaveData: (id, data) => { - this._remoteSockets.get(id)?.onData.fire(data); - }, - onDidRemoteSocketEnd: id => { - this._remoteSockets.get(id)?.onEnd.fire(); - this._remoteSockets.delete(id); - }, - onDidRemoteSocketClose: (id, error) => { - this._remoteSockets.get(id)?.onClose.fire({ - type: SocketCloseEventType.NodeSocketCloseEvent, - error, - hadError: !!error - }); - this._remoteSockets.delete(id); - }, - }, _setExtensionHostProxy: (value: IExtensionHostProxy): void => { extensionHostProxy = value; }, @@ -438,10 +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}`); - if (resolverResult.value.authority.connectTo.type === RemoteConnectionType.Managed) { - this.registerManagedSocketFactory(resolverResult.value.authority.connectTo, proxy); - } + logInfo(`returned ${resolverResult.value.authority.connectTo}`); } else { logError(`returned an error`, resolverResult.error); } @@ -460,45 +428,6 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { } } - private registerManagedSocketFactory(messaging: ManagedRemoteConnection, proxy: IExtensionHostProxy) { - const that = this; - this._remoteSocketFactoryService.register(RemoteConnectionType.Managed, new class implements ISocketFactory { - - supports(connectTo: ManagedRemoteConnection): boolean { - return (connectTo.id === messaging.id); - } - - connect(connectTo: ManagedRemoteConnection, path: string, query: string, debugLabel: string): Promise { - return new Promise((resolve, reject) => { - if (connectTo.id !== messaging.id) { - return reject(new Error('Invalid connectTo')); - } - - const factoryId = connectTo.id; - 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, proxy, path, query, debugLabel, half) - .then( - socket => { - socket.onDidDispose(() => that._remoteSockets.delete(socketId)); - resolve(socket); - }, - err => { - that._remoteSockets.delete(socketId); - reject(err); - }); - }).catch(reject); - }); - } - }); - } - public async getCanonicalURI(remoteAuthority: string, uri: URI): Promise { const proxy = await this._proxy; if (!proxy) { diff --git a/src/vs/workbench/services/extensions/common/extensionHostProxy.ts b/src/vs/workbench/services/extensions/common/extensionHostProxy.ts index b6c8a8d24cc..e1c962a8b40 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProxy.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProxy.ts @@ -42,9 +42,4 @@ export interface IExtensionHostProxy { test_latency(n: number): Promise; test_up(b: VSBuffer): Promise; test_down(size: number): Promise; - - openRemoteSocket(factoryId: number): Promise; - remoteSocketWrite(socketId: number, buffer: VSBuffer): void; - remoteSocketEnd(socketId: number): void; - remoteSocketDrain(socketId: number): Promise; } diff --git a/src/vs/workbench/services/remote/common/managedSocket.ts b/src/vs/workbench/services/remote/common/managedSocket.ts deleted file mode 100644 index 4ff860a8f44..00000000000 --- a/src/vs/workbench/services/remote/common/managedSocket.ts +++ /dev/null @@ -1,91 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { VSBuffer } from 'vs/base/common/buffer'; -import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ISocket, SocketCloseEvent, SocketDiagnostics, SocketDiagnosticsEventType } from 'vs/base/parts/ipc/common/ipc.net'; -import { makeRawSocketHeaders, socketRawEndHeaderSequence } from 'vs/platform/remote/common/managedSocket'; -import { IExtensionHostProxy } from 'vs/workbench/services/extensions/common/extensionHostProxy'; - -export class ManagedSocket extends Disposable implements ISocket { - public static connect( - socketId: number, - proxy: IExtensionHostProxy, - path: string, query: string, debugLabel: string, - - half: { - onClose: Emitter; - onData: Emitter; - onEnd: Emitter; - } - ): Promise { - const socket = new ManagedSocket(socketId, proxy, debugLabel, half.onClose, half.onData, half.onEnd); - - socket.write(VSBuffer.fromString(makeRawSocketHeaders(path, query, debugLabel))); - - const d = new DisposableStore(); - return new Promise((resolve, reject) => { - d.add(socket.onData(d => { - if (d.indexOf(socketRawEndHeaderSequence) !== -1) { - resolve(socket); - } - })); - - d.add(socket.onClose(err => reject(err ?? new Error('socket closed')))); - d.add(socket.onEnd(() => reject(new Error('socket ended')))); - }).finally(() => d.dispose()); - } - - public onData: Event; - public onClose: Event; - public onEnd: Event; - - private readonly didDisposeEmitter = this._register(new Emitter()); - public onDidDispose = this.didDisposeEmitter.event; - - private ended = false; - - private constructor( - private readonly socketId: number, - private readonly proxy: IExtensionHostProxy, - private readonly debugLabel: string, - onCloseEmitter: Emitter, - onDataEmitter: Emitter, - onEndEmitter: Emitter, - ) { - super(); - this.onClose = this._register(onCloseEmitter).event; - this.onData = this._register(onDataEmitter).event; - this.onEnd = this._register(onEndEmitter).event; - } - - write(buffer: VSBuffer): void { - this.proxy.remoteSocketWrite(this.socketId, buffer); - } - - end(): void { - this.ended = true; - this.proxy.remoteSocketEnd(this.socketId); - } - - drain(): Promise { - return this.proxy.remoteSocketDrain(this.socketId); - } - - traceSocketEvent(type: SocketDiagnosticsEventType, data?: any): void { - SocketDiagnostics.traceSocketEvent(this, this.debugLabel, type, data); - } - - override dispose(): void { - if (!this.ended) { - this.proxy.remoteSocketEnd(this.socketId); - } - - this.didDisposeEmitter.fire(); - super.dispose(); - } -} -