diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 744bfea95308e..08d0791de7350 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -47,6 +47,9 @@ import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemPro import { Schemas } from 'vs/base/common/network'; import { IFileService } from 'vs/platform/files/common/files'; import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { ITunnelService } from 'vs/platform/remote/common/tunnel'; +import { TunnelService } from 'vs/platform/remote/node/tunnelService'; +import { IProductService } from 'vs/platform/product/common/productService'; class ExpectedError extends Error { readonly isExpected = true; @@ -162,6 +165,8 @@ class CodeMain { services.set(IThemeMainService, new SyncDescriptor(ThemeMainService)); services.set(ISignService, new SyncDescriptor(SignService)); services.set(IStorageKeysSyncRegistryService, new SyncDescriptor(StorageKeysSyncRegistryService)); + services.set(IProductService, { _serviceBrand: undefined, ...product }); + services.set(ITunnelService, new SyncDescriptor(TunnelService)); return [new InstantiationService(services, true), instanceEnvironment, environmentService]; } diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index 3f6356d41dee0..30eec0fca6ce3 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { URI } from 'vs/base/common/uri'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IAddress } from 'vs/platform/remote/common/remoteAgentConnection'; export const ITunnelService = createDecorator('tunnelService'); @@ -35,7 +37,7 @@ export interface ITunnelService { readonly onTunnelOpened: Event; readonly onTunnelClosed: Event<{ host: string, port: number }>; - openTunnel(remoteHost: string | undefined, remotePort: number, localPort?: number): Promise | undefined; + openTunnel(resolveAuthority: IAddress | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise | undefined; closeTunnel(remoteHost: string, remotePort: number): Promise; setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable; } @@ -53,3 +55,144 @@ export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: port: +localhostMatch[2], }; } + + + +export abstract class AbstractTunnelService implements ITunnelService { + declare readonly _serviceBrand: undefined; + + private _onTunnelOpened: Emitter = new Emitter(); + public onTunnelOpened: Event = this._onTunnelOpened.event; + private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter(); + public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event; + protected readonly _tunnels = new Map }>>(); + protected _tunnelProvider: ITunnelProvider | undefined; + + public constructor( + @ILogService protected readonly logService: ILogService + ) { } + + setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { + if (!provider) { + return { + dispose: () => { } + }; + } + this._tunnelProvider = provider; + return { + dispose: () => { + this._tunnelProvider = undefined; + } + }; + } + + public get tunnels(): Promise { + const promises: Promise[] = []; + Array.from(this._tunnels.values()).forEach(portMap => Array.from(portMap.values()).forEach(x => promises.push(x.value))); + return Promise.all(promises); + } + + dispose(): void { + for (const portMap of this._tunnels.values()) { + for (const { value } of portMap.values()) { + value.then(tunnel => tunnel.dispose()); + } + portMap.clear(); + } + this._tunnels.clear(); + } + + openTunnel(resolvedAuthority: IAddress | undefined, remoteHost: string | undefined, remotePort: number, localPort: number): Promise | undefined { + if (!resolvedAuthority) { + return undefined; + } + + if (!remoteHost || (remoteHost === '127.0.0.1')) { + remoteHost = 'localhost'; + } + + const resolvedTunnel = this.retainOrCreateTunnel(resolvedAuthority, remoteHost, remotePort, localPort); + if (!resolvedTunnel) { + return resolvedTunnel; + } + + return resolvedTunnel.then(tunnel => { + const newTunnel = this.makeTunnel(tunnel); + if (tunnel.tunnelRemoteHost !== remoteHost || tunnel.tunnelRemotePort !== remotePort) { + this.logService.warn('Created tunnel does not match requirements of requested tunnel. Host or port mismatch.'); + } + this._onTunnelOpened.fire(newTunnel); + return newTunnel; + }); + } + + private makeTunnel(tunnel: RemoteTunnel): RemoteTunnel { + return { + tunnelRemotePort: tunnel.tunnelRemotePort, + tunnelRemoteHost: tunnel.tunnelRemoteHost, + tunnelLocalPort: tunnel.tunnelLocalPort, + localAddress: tunnel.localAddress, + dispose: () => { + const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost); + if (existingHost) { + const existing = existingHost.get(tunnel.tunnelRemotePort); + if (existing) { + existing.refcount--; + this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing); + } + } + } + }; + } + + private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise }): Promise { + if (tunnel.refcount <= 0) { + const disposePromise: Promise = tunnel.value.then(tunnel => { + tunnel.dispose(true); + this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); + }); + if (this._tunnels.has(remoteHost)) { + this._tunnels.get(remoteHost)!.delete(remotePort); + } + return disposePromise; + } + } + + async closeTunnel(remoteHost: string, remotePort: number): Promise { + const portMap = this._tunnels.get(remoteHost); + if (portMap && portMap.has(remotePort)) { + const value = portMap.get(remotePort)!; + value.refcount = 0; + await this.tryDisposeTunnel(remoteHost, remotePort, value); + } + } + + protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise) { + if (!this._tunnels.has(remoteHost)) { + this._tunnels.set(remoteHost, new Map()); + } + this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel }); + } + + protected abstract retainOrCreateTunnel(resolveRemoteAuthority: IAddress, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined; +} + +export class TunnelService extends AbstractTunnelService { + protected retainOrCreateTunnel(_resolveRemoteAuthority: IAddress, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise | undefined { + const portMap = this._tunnels.get(remoteHost); + const existing = portMap ? portMap.get(remotePort) : undefined; + if (existing) { + ++existing.refcount; + return existing.value; + } + + if (this._tunnelProvider) { + const tunnel = this._tunnelProvider.forwardPort({ remoteAddress: { host: remoteHost, port: remotePort } }); + if (tunnel) { + this.addTunnelToMap(remoteHost, remotePort, tunnel); + } + return tunnel; + } + return undefined; + } +} diff --git a/src/vs/workbench/services/remote/node/tunnelService.ts b/src/vs/platform/remote/node/tunnelService.ts similarity index 83% rename from src/vs/workbench/services/remote/node/tunnelService.ts rename to src/vs/platform/remote/node/tunnelService.ts index 6829f0ccbd517..637c50d3aa706 100644 --- a/src/vs/workbench/services/remote/node/tunnelService.ts +++ b/src/vs/platform/remote/node/tunnelService.ts @@ -6,18 +6,14 @@ import * as net from 'net'; import { Barrier } from 'vs/base/common/async'; import { Disposable } from 'vs/base/common/lifecycle'; +import { findFreePortFaster } from 'vs/base/node/ports'; import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; -import { connectRemoteAgentTunnel, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection'; -import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { connectRemoteAgentTunnel, IAddress, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection'; +import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; import { ISignService } from 'vs/platform/sign/common/sign'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ILogService } from 'vs/platform/log/common/log'; -import { findFreePortFaster } from 'vs/base/node/ports'; -import { AbstractTunnelService } from 'vs/workbench/services/remote/common/tunnelService'; async function createRemoteTunnel(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise { const tunnel = new NodeRemoteTunnel(options, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort); @@ -128,16 +124,14 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { export class TunnelService extends AbstractTunnelService { public constructor( - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @ILogService logService: ILogService, - @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService, @ISignService private readonly signService: ISignService, @IProductService private readonly productService: IProductService ) { - super(environmentService, logService); + super(logService); } - protected retainOrCreateTunnel(remoteAuthority: string, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined { + protected retainOrCreateTunnel(resolveRemoteAuthority: IAddress, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined { const portMap = this._tunnels.get(remoteHost); const existing = portMap ? portMap.get(remotePort) : undefined; if (existing) { @@ -157,8 +151,7 @@ export class TunnelService extends AbstractTunnelService { socketFactory: nodeSocketFactory, addressProvider: { getAddress: async () => { - const { authority } = await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority); - return { host: authority.host, port: authority.port }; + return resolveRemoteAuthority; } }, signService: this.signService, @@ -171,5 +164,3 @@ export class TunnelService extends AbstractTunnelService { } } } - -registerSingleton(ITunnelService, TunnelService, true); diff --git a/src/vs/platform/webview/common/resourceLoader.ts b/src/vs/platform/webview/common/resourceLoader.ts index a5679566df4a8..e0809809ee4fe 100644 --- a/src/vs/platform/webview/common/resourceLoader.ts +++ b/src/vs/platform/webview/common/resourceLoader.ts @@ -15,6 +15,10 @@ import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { IRequestService } from 'vs/platform/request/common/request'; import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes'; + +export const webviewPartitionId = 'webview'; + + export namespace WebviewResourceResponse { export enum Type { Success, Failed, AccessDenied } diff --git a/src/vs/platform/webview/common/webviewManagerService.ts b/src/vs/platform/webview/common/webviewManagerService.ts index 2be1cd5951211..5f063b325d35a 100644 --- a/src/vs/platform/webview/common/webviewManagerService.ts +++ b/src/vs/platform/webview/common/webviewManagerService.ts @@ -6,13 +6,14 @@ import { UriComponents } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IWebviewPortMapping } from 'vs/platform/webview/common/webviewPortMapping'; export const IWebviewManagerService = createDecorator('webviewManagerService'); export interface IWebviewManagerService { _serviceBrand: unknown; - registerWebview(id: string, metadata: RegisterWebviewMetadata): Promise; + registerWebview(id: string, webContentsId: number, metadata: RegisterWebviewMetadata): Promise; unregisterWebview(id: string): Promise; updateWebviewMetadata(id: string, metadataDelta: Partial): Promise; @@ -23,4 +24,5 @@ export interface RegisterWebviewMetadata { readonly extensionLocation: UriComponents | undefined; readonly localResourceRoots: readonly UriComponents[]; readonly remoteConnectionData: IRemoteConnectionData | null; + readonly portMappings: readonly IWebviewPortMapping[]; } diff --git a/src/vs/workbench/contrib/webview/common/portMapping.ts b/src/vs/platform/webview/common/webviewPortMapping.ts similarity index 60% rename from src/vs/workbench/contrib/webview/common/portMapping.ts rename to src/vs/platform/webview/common/webviewPortMapping.ts index fe5a6d21962ad..16fe1f4ac22e6 100644 --- a/src/vs/workbench/contrib/webview/common/portMapping.ts +++ b/src/vs/platform/webview/common/webviewPortMapping.ts @@ -3,36 +3,42 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import * as modes from 'vs/editor/common/modes'; +import { IAddress } from 'vs/platform/remote/common/remoteAgentConnection'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; -import { ITunnelService, RemoteTunnel, extractLocalHostUriMetaDataForPortMapping } from 'vs/platform/remote/common/tunnel'; +import { extractLocalHostUriMetaDataForPortMapping, ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; -export class WebviewPortMappingManager extends Disposable { +export interface IWebviewPortMapping { + webviewPort: number; + extensionHostPort: number; +} + +/** + * Manages port mappings for a single webview. + */ +export class WebviewPortMappingManager implements IDisposable { private readonly _tunnels = new Map>(); constructor( - private readonly getExtensionLocation: () => URI | undefined, - private readonly mappings: () => ReadonlyArray, + private readonly _getExtensionLocation: () => URI | undefined, + private readonly _getMappings: () => readonly IWebviewPortMapping[], private readonly tunnelService: ITunnelService - ) { - super(); - } + ) { } - public async getRedirect(url: string): Promise { + public async getRedirect(resolveAuthority: IAddress, url: string): Promise { const uri = URI.parse(url); const requestLocalHostInfo = extractLocalHostUriMetaDataForPortMapping(uri); if (!requestLocalHostInfo) { return undefined; } - for (const mapping of this.mappings()) { + for (const mapping of this._getMappings()) { if (mapping.webviewPort === requestLocalHostInfo.port) { - const extensionLocation = this.getExtensionLocation(); + const extensionLocation = this._getExtensionLocation(); if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) { - const tunnel = await this.getOrCreateTunnel(mapping.extensionHostPort); + const tunnel = await this.getOrCreateTunnel(resolveAuthority, mapping.extensionHostPort); if (tunnel) { if (tunnel.tunnelLocalPort === mapping.webviewPort) { return undefined; @@ -55,20 +61,18 @@ export class WebviewPortMappingManager extends Disposable { } dispose() { - super.dispose(); - for (const tunnel of this._tunnels.values()) { tunnel.then(tunnel => tunnel.dispose()); } this._tunnels.clear(); } - private getOrCreateTunnel(remotePort: number): Promise | undefined { + private getOrCreateTunnel(remoteAuthority: IAddress, remotePort: number): Promise | undefined { const existing = this._tunnels.get(remotePort); if (existing) { return existing; } - const tunnel = this.tunnelService.openTunnel(undefined, remotePort); + const tunnel = this.tunnelService.openTunnel(remoteAuthority, undefined, remotePort); if (tunnel) { this._tunnels.set(remotePort, tunnel); } diff --git a/src/vs/platform/webview/electron-main/webviewMainService.ts b/src/vs/platform/webview/electron-main/webviewMainService.ts index a52f551b9d9e6..71dd736975533 100644 --- a/src/vs/platform/webview/electron-main/webviewMainService.ts +++ b/src/vs/platform/webview/electron-main/webviewMainService.ts @@ -4,42 +4,65 @@ *--------------------------------------------------------------------------------------------*/ import { webContents } from 'electron'; +import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; +import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { IRequestService } from 'vs/platform/request/common/request'; import { IWebviewManagerService, RegisterWebviewMetadata } from 'vs/platform/webview/common/webviewManagerService'; +import { WebviewPortMappingProvider } from 'vs/platform/webview/electron-main/webviewPortMappingProvider'; import { WebviewProtocolProvider } from 'vs/platform/webview/electron-main/webviewProtocolProvider'; -export class WebviewMainService implements IWebviewManagerService { +export class WebviewMainService extends Disposable implements IWebviewManagerService { declare readonly _serviceBrand: undefined; - private protocolProvider: WebviewProtocolProvider; + private readonly protocolProvider: WebviewProtocolProvider; + private readonly portMappingProvider: WebviewPortMappingProvider; constructor( @IFileService fileService: IFileService, @IRequestService requestService: IRequestService, + @ITunnelService tunnelService: ITunnelService, ) { - this.protocolProvider = new WebviewProtocolProvider(fileService, requestService); + super(); + this.protocolProvider = this._register(new WebviewProtocolProvider(fileService, requestService)); + this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService)); } - public async registerWebview(id: string, metadata: RegisterWebviewMetadata): Promise { + public async registerWebview(id: string, webContentsId: number, metadata: RegisterWebviewMetadata): Promise { + const extensionLocation = metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined; + this.protocolProvider.registerWebview(id, { ...metadata, - extensionLocation: metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined, + extensionLocation, localResourceRoots: metadata.localResourceRoots.map(x => URI.from(x)) }); + + this.portMappingProvider.registerWebview(id, webContentsId, { + extensionLocation, + mappings: metadata.portMappings, + resolvedAuthority: metadata.remoteConnectionData, + }); } public async unregisterWebview(id: string): Promise { - this.protocolProvider.unreigsterWebview(id); + this.protocolProvider.unregisterWebview(id); + this.portMappingProvider.unregisterWebview(id); } - public async updateWebviewMetadata(id: string, metadataDelta: Partial): Promise { + public async updateWebviewMetadata(id: string, metaDataDelta: Partial): Promise { + const extensionLocation = metaDataDelta.extensionLocation ? URI.from(metaDataDelta.extensionLocation) : undefined; + this.protocolProvider.updateWebviewMetadata(id, { - ...metadataDelta, - localResourceRoots: metadataDelta.localResourceRoots?.map(x => URI.from(x)), - extensionLocation: metadataDelta.extensionLocation ? URI.from(metadataDelta.extensionLocation) : undefined, + ...metaDataDelta, + extensionLocation, + localResourceRoots: metaDataDelta.localResourceRoots?.map(x => URI.from(x)), + }); + + this.portMappingProvider.updateWebviewMetadata(id, { + ...metaDataDelta, + extensionLocation, }); } diff --git a/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts b/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts new file mode 100644 index 0000000000000..51cf2eb746f4d --- /dev/null +++ b/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { session } from 'electron'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IAddress } from 'vs/platform/remote/common/remoteAgentConnection'; +import { ITunnelService } from 'vs/platform/remote/common/tunnel'; +import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader'; +import { IWebviewPortMapping, WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping'; + +interface PortMappingData { + readonly extensionLocation: URI | undefined; + readonly mappings: readonly IWebviewPortMapping[]; + readonly resolvedAuthority: IAddress | null | undefined; +} + +export class WebviewPortMappingProvider extends Disposable { + + private readonly _webviewData = new Map(); + + private _webContentsIdsToWebviewIds = new Map(); + + constructor( + @ITunnelService private readonly _tunnelService: ITunnelService, + ) { + super(); + + const sess = session.fromPartition(webviewPartitionId); + + sess.webRequest.onBeforeRequest({ + urls: [ + '*://localhost:*/', + '*://127.0.0.1:*/', + '*://0.0.0.0:*/', + ] + }, async (details, callback) => { + const webviewId = details.webContentsId && this._webContentsIdsToWebviewIds.get(details.webContentsId); + if (!webviewId) { + return callback({}); + } + + const entry = this._webviewData.get(webviewId); + if (!entry || !entry.metadata.resolvedAuthority) { + return callback({}); + } + + const redirect = await entry.manager.getRedirect(entry.metadata.resolvedAuthority, details.url); + return callback(redirect ? { redirectURL: redirect } : {}); + }); + } + + public async registerWebview(id: string, webContentsId: number, metadata: PortMappingData): Promise { + const manager = new WebviewPortMappingManager( + () => this._webviewData.get(id)?.metadata.extensionLocation, + () => this._webviewData.get(id)?.metadata.mappings || [], + this._tunnelService); + + this._webviewData.set(id, { webContentsId, metadata, manager }); + this._webContentsIdsToWebviewIds.set(webContentsId, id); + } + + public unregisterWebview(id: string): void { + const existing = this._webviewData.get(id); + if (existing) { + existing.manager.dispose(); + this._webviewData.delete(id); + this._webContentsIdsToWebviewIds.delete(existing.webContentsId); + } + } + + public async updateWebviewMetadata(id: string, metadataDelta: Partial): Promise { + const entry = this._webviewData.get(id); + if (entry) { + this._webviewData.set(id, { + ...entry, + ...metadataDelta, + }); + } + } +} diff --git a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts index 57441f63b13c7..047c752ba1dbb 100644 --- a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts +++ b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts @@ -3,16 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { protocol } from 'electron'; +import { session } from 'electron'; +import { Readable } from 'stream'; +import { VSBufferReadableStream } from 'vs/base/common/buffer'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRequestService } from 'vs/platform/request/common/request'; -import { loadLocalResourceStream, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader'; -import { VSBufferReadableStream } from 'vs/base/common/buffer'; -import { Readable } from 'stream'; +import { loadLocalResourceStream, webviewPartitionId, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader'; interface WebviewMetadata { readonly extensionLocation: URI | undefined; @@ -30,7 +30,9 @@ export class WebviewProtocolProvider extends Disposable { ) { super(); - protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, async (request, callback): Promise => { + const sess = session.fromPartition(webviewPartitionId); + + sess.protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, async (request, callback): Promise => { try { const uri = URI.parse(request.url); @@ -65,7 +67,7 @@ export class WebviewProtocolProvider extends Disposable { return callback({ data: null, statusCode: 404 }); }); - this._register(toDisposable(() => protocol.unregisterProtocol(Schemas.vscodeWebviewResource))); + this._register(toDisposable(() => sess.protocol.unregisterProtocol(Schemas.vscodeWebviewResource))); } private streamToNodeReadable(stream: VSBufferReadableStream): Readable { @@ -116,7 +118,7 @@ export class WebviewProtocolProvider extends Disposable { this.webviewMetadata.set(id, metadata); } - public unreigsterWebview(id: string): void { + public unregisterWebview(id: string): void { this.webviewMetadata.delete(id); } diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index d629a6165fd47..b5a88e8158d8e 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -5,6 +5,7 @@ import { addDisposableListener } from 'vs/base/browser/dom'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { isWeb } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -12,12 +13,12 @@ import { IFileService } from 'vs/platform/files/common/files'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { loadLocalResource, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader'; +import { WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping'; import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement'; -import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; -import { WebviewPortMappingManager } from 'vs/workbench/contrib/webview/common/portMapping'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; +import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { isWeb } from 'vs/base/common/platform'; +import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; export class IFrameWebview extends BaseWebview implements Webview { private readonly _portMappingManager: WebviewPortMappingManager; @@ -32,17 +33,18 @@ export class IFrameWebview extends BaseWebview implements Web @IFileService private readonly fileService: IFileService, @IConfigurationService private readonly _configurationService: IConfigurationService, @ITelemetryService telemetryService: ITelemetryService, - @IEnvironmentService environementService: IEnvironmentService, - @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IEnvironmentService environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly _workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, ) { - super(id, options, contentOptions, extension, webviewThemeDataProvider, telemetryService, environementService, workbenchEnvironmentService); + super(id, options, contentOptions, extension, webviewThemeDataProvider, telemetryService, environmentService, _workbenchEnvironmentService); - if (!this.useExternalEndpoint && (!workbenchEnvironmentService.options || typeof workbenchEnvironmentService.webviewExternalEndpoint !== 'string')) { + if (!this.useExternalEndpoint && (!_workbenchEnvironmentService.options || typeof _workbenchEnvironmentService.webviewExternalEndpoint !== 'string')) { throw new Error('To use iframe based webviews, you must configure `environmentService.webviewExternalEndpoint`'); } this._portMappingManager = this._register(new WebviewPortMappingManager( - () => this.extension ? this.extension.location : undefined, + () => this.extension?.location, () => this.content.options.portMapping || [], tunnelService )); @@ -156,7 +158,9 @@ export class IFrameWebview extends BaseWebview implements Web } private async localLocalhost(origin: string) { - const redirect = await this._portMappingManager.getRedirect(origin); + const authority = this._workbenchEnvironmentService.configuration.remoteAuthority; + const resolveAuthority = authority ? await this._remoteAuthorityResolverService.resolveAuthority(authority) : undefined; + const redirect = resolveAuthority ? await this._portMappingManager.getRedirect(resolveAuthority.authority, origin) : undefined; return this._send('did-load-localhost', { origin, location: redirect diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 421aaca22c53b..31e1e26d27a9a 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { FindInPageOptions, OnBeforeRequestListenerDetails, OnHeadersReceivedListenerDetails, Response, WebContents, WebviewTag } from 'electron'; +import { FindInPageOptions, WebviewTag } from 'electron'; import { addDisposableListener } from 'vs/base/browser/dom'; import { equals } from 'vs/base/common/arrays'; import { ThrottledDelayer } from 'vs/base/common/async'; @@ -20,125 +20,54 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader'; import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService'; import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; -import { WebviewPortMappingManager } from 'vs/workbench/contrib/webview/common/portMapping'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { WebviewFindDelegate, WebviewFindWidget } from '../browser/webviewFindWidget'; -class WebviewTagHandle extends Disposable { +class WebviewResourceRequestManager extends Disposable { - private _webContents: undefined | WebContents | 'destroyed'; + private readonly _webviewManagerService: IWebviewManagerService; - public constructor( - public readonly webview: WebviewTag, - ) { - super(); - - this._register(addDisposableListener(this.webview, 'destroyed', () => { - this._webContents = 'destroyed'; - })); - - this._register(addDisposableListener(this.webview, 'did-start-loading', once(() => { - const contents = this.webContents; - if (contents) { - this._onFirstLoad.fire(contents); - this._register(toDisposable(() => { - contents.removeAllListeners(); - })); - } - }))); - } - - private readonly _onFirstLoad = this._register(new Emitter()); - public readonly onFirstLoad = this._onFirstLoad.event; - - public get webContents(): WebContents | undefined { - if (this._webContents === 'destroyed') { - return undefined; - } - if (this._webContents) { - return this._webContents; - } - this._webContents = this.webview.getWebContents(); - return this._webContents; - } -} - -type OnBeforeRequestDelegate = (details: OnBeforeRequestListenerDetails) => Promise; -type OnHeadersReceivedDelegate = (details: OnHeadersReceivedListenerDetails) => { cancel: boolean; } | undefined; - -class WebviewSession extends Disposable { - - private readonly _onBeforeRequestDelegates: Array = []; - private readonly _onHeadersReceivedDelegates: Array = []; - - public constructor( - webviewHandle: WebviewTagHandle, - ) { - super(); - - this._register(webviewHandle.onFirstLoad(contents => { - contents.session.webRequest.onBeforeRequest(async (details, callback) => { - for (const delegate of this._onBeforeRequestDelegates) { - const result = await delegate(details); - if (typeof result !== 'undefined') { - callback(result); - return; - } - } - callback({}); - }); - - contents.session.webRequest.onHeadersReceived((details, callback) => { - for (const delegate of this._onHeadersReceivedDelegates) { - const result = delegate(details); - if (typeof result !== 'undefined') { - callback(result); - return; - } - } - callback({ cancel: false }); - }); - })); - } - - public onBeforeRequest(delegate: OnBeforeRequestDelegate) { - this._onBeforeRequestDelegates.push(delegate); - } - - public onHeadersReceived(delegate: OnHeadersReceivedDelegate) { - this._onHeadersReceivedDelegates.push(delegate); - } -} - -class WebviewProtocolProvider extends Disposable { + private _localResourceRoots: ReadonlyArray; + private _portMappings: ReadonlyArray; private _ready?: Promise; - private _localResourceRoots: ReadonlyArray; - constructor( private readonly id: string, private readonly extension: WebviewExtensionDescription | undefined, - initialLocalResourceRoots: ReadonlyArray, - private readonly _webviewManagerService: IWebviewManagerService, - remoteAuthorityResolverService: IRemoteAuthorityResolverService, - environmentService: IWorkbenchEnvironmentService, + webview: WebviewTag, + initialContentOptions: WebviewContentOptions, + @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IMainProcessService mainProcessService: IMainProcessService, ) { super(); - this._localResourceRoots = initialLocalResourceRoots; + this._webviewManagerService = createChannelSender(mainProcessService.getChannel('webview')); + + this._localResourceRoots = initialContentOptions.localResourceRoots || []; + this._portMappings = initialContentOptions.portMapping || []; const remoteAuthority = environmentService.configuration.remoteAuthority; - this._ready = _webviewManagerService.registerWebview(this.id, { - extensionLocation: this.extension?.location.toJSON(), - localResourceRoots: initialLocalResourceRoots.map(x => x.toJSON()), - remoteConnectionData: remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null, + const remoteConnectionData = remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null; + + this._ready = new Promise(resolve => { + this._register(addDisposableListener(webview!, 'did-start-loading', once(() => { + const webContentsId = webview.getWebContentsId(); + + this._webviewManagerService.registerWebview(this.id, webContentsId, { + extensionLocation: this.extension?.location.toJSON(), + localResourceRoots: this._localResourceRoots.map(x => x.toJSON()), + remoteConnectionData: remoteConnectionData, + portMappings: this._portMappings, + }).finally(() => resolve()); + }))); }); if (remoteAuthority) { @@ -153,16 +82,25 @@ class WebviewProtocolProvider extends Disposable { this._register(toDisposable(() => this._webviewManagerService.unregisterWebview(this.id))); } - public update(localResourceRoots: ReadonlyArray) { - if (equals(this._localResourceRoots, localResourceRoots, (a, b) => a.toString() === b.toString())) { + public update(options: WebviewContentOptions) { + const localResourceRoots = options.localResourceRoots || []; + const portMappings = options.portMapping || []; + + if ( + equals(this._localResourceRoots, localResourceRoots, (a, b) => a.toString() === b.toString()) + && equals(this._portMappings, portMappings, (a, b) => a.extensionHostPort === b.extensionHostPort && a.webviewPort === b.webviewPort) + ) { return; } this._localResourceRoots = localResourceRoots; + this._portMappings = portMappings; const update = this._webviewManagerService.updateWebviewMetadata(this.id, { localResourceRoots: localResourceRoots.map(x => x.toJSON()), + portMappings: portMappings, }); + this._ready = this._ready?.then(() => update); } @@ -171,24 +109,6 @@ class WebviewProtocolProvider extends Disposable { } } -class WebviewPortMappingProvider extends Disposable { - - constructor( - session: WebviewSession, - getExtensionLocation: () => URI | undefined, - mappings: () => ReadonlyArray, - tunnelService: ITunnelService, - ) { - super(); - const manager = this._register(new WebviewPortMappingManager(getExtensionLocation, mappings, tunnelService)); - - session.onBeforeRequest(async details => { - const redirect = await manager.getRedirect(details.url); - return redirect ? { redirectURL: redirect } : undefined; - }); - } -} - class WebviewKeyboardHandler { private readonly _webviews = new Set(); @@ -266,7 +186,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme private _webviewFindWidget: WebviewFindWidget | undefined; private _findStarted: boolean = false; - private readonly _protocolProvider: WebviewProtocolProvider; + private readonly _resourceRequestManager: WebviewResourceRequestManager; private readonly _focusDelayer = this._register(new ThrottledDelayer(10)); private _elementFocusImpl!: (options?: FocusOptions | undefined) => void; @@ -280,29 +200,15 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme extension: WebviewExtensionDescription | undefined, private readonly _webviewThemeDataProvider: WebviewThemeDataProvider, @IInstantiationService instantiationService: IInstantiationService, - @ITunnelService tunnelService: ITunnelService, @ITelemetryService telemetryService: ITelemetryService, - @IEnvironmentService environementService: IEnvironmentService, + @IEnvironmentService environmentService: IEnvironmentService, @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService, @IConfigurationService configurationService: IConfigurationService, @IMainProcessService mainProcessService: IMainProcessService, - @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, ) { - super(id, options, contentOptions, extension, _webviewThemeDataProvider, telemetryService, environementService, workbenchEnvironmentService); - - const webviewManagerService = createChannelSender(mainProcessService.getChannel('webview')); + super(id, options, contentOptions, extension, _webviewThemeDataProvider, telemetryService, environmentService, workbenchEnvironmentService); - const webviewAndContents = this._register(new WebviewTagHandle(this.element!)); - const session = this._register(new WebviewSession(webviewAndContents)); - - this._protocolProvider = this._register(new WebviewProtocolProvider(id, extension, this.content.options.localResourceRoots || [], webviewManagerService, remoteAuthorityResolverService, workbenchEnvironmentService)); - - this._register(new WebviewPortMappingProvider( - session, - () => this.extension ? this.extension.location : undefined, - () => (this.content.options.portMapping || []), - tunnelService, - )); + this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.element!, this.content.options)); this._register(addDisposableListener(this.element!, 'did-start-loading', once(() => { this._register(ElectronWebviewBasedWebview.getWebviewKeyboardHandler(configurationService, mainProcessService).add(this.element!)); @@ -370,6 +276,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme element.focus = () => { this.doFocus(); }; + element.setAttribute('partition', webviewPartitionId); element.setAttribute('webpreferences', 'contextIsolation=yes'); element.className = `webview ${options.customClasses || ''}`; @@ -385,12 +292,15 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme } public set contentOptions(options: WebviewContentOptions) { - this._protocolProvider.update(options.localResourceRoots || []); + this._resourceRequestManager.update(options); super.contentOptions = options; } public set localResourcesRoot(resources: URI[]) { - this._protocolProvider.update(resources || []); + this._resourceRequestManager.update({ + ...this.contentOptions, + localResourceRoots: resources, + }); super.localResourcesRoot = resources; } @@ -428,7 +338,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme protected async doPostMessage(channel: string, data?: any): Promise { this._messagePromise = this._messagePromise - .then(() => this._protocolProvider.synchronize()) + .then(() => this._resourceRequestManager.synchronize()) .then(() => this.element?.send(channel, data)); } diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 1107f2a1bcb87..cbd9955eb0c35 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -64,6 +64,7 @@ import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/ import { Event } from 'vs/base/common/event'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; import { clearAllFontInfos } from 'vs/editor/browser/config/configuration'; +import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; export class NativeWindow extends Disposable { @@ -107,7 +108,8 @@ export class NativeWindow extends Disposable { @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @IStorageService private readonly storageService: IStorageService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService, ) { super(); @@ -471,7 +473,8 @@ export class NativeWindow extends Disposable { if (options?.allowTunneling) { const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(uri); if (portMappingRequest) { - const tunnel = await this.tunnelService.openTunnel(undefined, portMappingRequest.port); + const resolvedRemote = this.environmentService.configuration.remoteAuthority ? await this.remoteAuthorityResolverService.resolveAuthority(this.environmentService.configuration.remoteAuthority) : undefined; + const tunnel = await this.tunnelService.openTunnel(resolvedRemote?.authority, undefined, portMappingRequest.port); if (tunnel) { return { resolved: uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}` }), diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 3227a9aff6711..3b1e6af96484f 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -11,7 +11,8 @@ import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IEditableData } from 'vs/workbench/common/views'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TunnelInformation, TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { TunnelInformation, TunnelDescription, IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export const IRemoteExplorerService = createDecorator('remoteExplorerService'); export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType'; @@ -75,7 +76,9 @@ export class TunnelModel extends Disposable { constructor( @ITunnelService private readonly tunnelService: ITunnelService, @IStorageService private readonly storageService: IStorageService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService, ) { super(); this.forwarded = new Map(); @@ -137,7 +140,13 @@ export class TunnelModel extends Disposable { async forward(remote: { host: string, port: number }, local?: number, name?: string): Promise { const key = MakeAddress(remote.host, remote.port); if (!this.forwarded.has(key)) { - const tunnel = await this.tunnelService.openTunnel(remote.host, remote.port, local); + const authority = this.environmentService.configuration.remoteAuthority; + const resolvedRemote = authority ? await this.remoteAuthorityResolverService.resolveAuthority(authority) : undefined; + if (!resolvedRemote) { + return; + } + + const tunnel = await this.tunnelService.openTunnel(resolvedRemote.authority, remote.host, remote.port, local); if (tunnel && tunnel.localAddress) { const newForward: Tunnel = { remoteHost: tunnel.tunnelRemoteHost, @@ -253,9 +262,11 @@ class RemoteExplorerService implements IRemoteExplorerService { constructor( @IStorageService private readonly storageService: IStorageService, @ITunnelService tunnelService: ITunnelService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, ) { - this._tunnelModel = new TunnelModel(tunnelService, storageService, configurationService); + this._tunnelModel = new TunnelModel(tunnelService, storageService, configurationService, environmentService, remoteAuthorityResolverService); } set targetType(name: string[]) { diff --git a/src/vs/workbench/services/remote/common/tunnelService.ts b/src/vs/workbench/services/remote/common/tunnelService.ts deleted file mode 100644 index 1e725fed9d3df..0000000000000 --- a/src/vs/workbench/services/remote/common/tunnelService.ts +++ /dev/null @@ -1,151 +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 { ITunnelService, RemoteTunnel, ITunnelProvider } from 'vs/platform/remote/common/tunnel'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { ILogService } from 'vs/platform/log/common/log'; - -export abstract class AbstractTunnelService implements ITunnelService { - declare readonly _serviceBrand: undefined; - - private _onTunnelOpened: Emitter = new Emitter(); - public onTunnelOpened: Event = this._onTunnelOpened.event; - private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter(); - public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event; - protected readonly _tunnels = new Map }>>(); - protected _tunnelProvider: ITunnelProvider | undefined; - - public constructor( - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @ILogService protected readonly logService: ILogService - ) { } - - setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { - if (!provider) { - return { - dispose: () => { } - }; - } - this._tunnelProvider = provider; - return { - dispose: () => { - this._tunnelProvider = undefined; - } - }; - } - - public get tunnels(): Promise { - const promises: Promise[] = []; - Array.from(this._tunnels.values()).forEach(portMap => Array.from(portMap.values()).forEach(x => promises.push(x.value))); - return Promise.all(promises); - } - - dispose(): void { - for (const portMap of this._tunnels.values()) { - for (const { value } of portMap.values()) { - value.then(tunnel => tunnel.dispose()); - } - portMap.clear(); - } - this._tunnels.clear(); - } - - openTunnel(remoteHost: string | undefined, remotePort: number, localPort: number): Promise | undefined { - const remoteAuthority = this.environmentService.configuration.remoteAuthority; - if (!remoteAuthority) { - return undefined; - } - - if (!remoteHost || (remoteHost === '127.0.0.1')) { - remoteHost = 'localhost'; - } - - const resolvedTunnel = this.retainOrCreateTunnel(remoteAuthority, remoteHost, remotePort, localPort); - if (!resolvedTunnel) { - return resolvedTunnel; - } - - return resolvedTunnel.then(tunnel => { - const newTunnel = this.makeTunnel(tunnel); - if (tunnel.tunnelRemoteHost !== remoteHost || tunnel.tunnelRemotePort !== remotePort) { - this.logService.warn('Created tunnel does not match requirements of requested tunnel. Host or port mismatch.'); - } - this._onTunnelOpened.fire(newTunnel); - return newTunnel; - }); - } - - private makeTunnel(tunnel: RemoteTunnel): RemoteTunnel { - return { - tunnelRemotePort: tunnel.tunnelRemotePort, - tunnelRemoteHost: tunnel.tunnelRemoteHost, - tunnelLocalPort: tunnel.tunnelLocalPort, - localAddress: tunnel.localAddress, - dispose: () => { - const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost); - if (existingHost) { - const existing = existingHost.get(tunnel.tunnelRemotePort); - if (existing) { - existing.refcount--; - this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing); - } - } - } - }; - } - - private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise }): Promise { - if (tunnel.refcount <= 0) { - const disposePromise: Promise = tunnel.value.then(tunnel => { - tunnel.dispose(true); - this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); - }); - if (this._tunnels.has(remoteHost)) { - this._tunnels.get(remoteHost)!.delete(remotePort); - } - return disposePromise; - } - } - - async closeTunnel(remoteHost: string, remotePort: number): Promise { - const portMap = this._tunnels.get(remoteHost); - if (portMap && portMap.has(remotePort)) { - const value = portMap.get(remotePort)!; - value.refcount = 0; - await this.tryDisposeTunnel(remoteHost, remotePort, value); - } - } - - protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise) { - if (!this._tunnels.has(remoteHost)) { - this._tunnels.set(remoteHost, new Map()); - } - this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel }); - } - - protected abstract retainOrCreateTunnel(remoteAuthority: string, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined; -} - -export class TunnelService extends AbstractTunnelService { - protected retainOrCreateTunnel(remoteAuthority: string, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise | undefined { - const portMap = this._tunnels.get(remoteHost); - const existing = portMap ? portMap.get(remotePort) : undefined; - if (existing) { - ++existing.refcount; - return existing.value; - } - - if (this._tunnelProvider) { - const tunnel = this._tunnelProvider.forwardPort({ remoteAddress: { host: remoteHost, port: remotePort } }); - if (tunnel) { - this.addTunnelToMap(remoteHost, remotePort, tunnel); - } - return tunnel; - } - return undefined; - } -} diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 8b9eca4617120..f1dab15572905 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -52,7 +52,6 @@ import 'vs/workbench/services/telemetry/electron-browser/telemetryService'; import 'vs/workbench/services/configurationResolver/electron-browser/configurationResolverService'; import 'vs/workbench/services/extensionManagement/node/extensionManagementService'; import 'vs/workbench/services/accessibility/electron-browser/accessibilityService'; -import 'vs/workbench/services/remote/node/tunnelService'; import 'vs/workbench/services/backup/node/backupFileService'; import 'vs/workbench/services/workspaces/electron-browser/workspaceEditingService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncMachinesService'; @@ -68,9 +67,12 @@ import { ICredentialsService } from 'vs/platform/credentials/common/credentials' import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService'; import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataAutoSyncService } from 'vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService'; +import { ITunnelService } from 'vs/platform/remote/common/tunnel'; +import { TunnelService } from 'vs/platform/remote/node/tunnelService'; registerSingleton(ICredentialsService, KeytarCredentialsService, true); registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); +registerSingleton(ITunnelService, TunnelService); //#endregion diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 1b8690e456b39..153ac595d03ac 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -61,8 +61,7 @@ import { BackupFileService } from 'vs/workbench/services/backup/common/backupFil import { IExtensionManagementService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionTipsService'; import { ExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagementService'; -import { ITunnelService } from 'vs/platform/remote/common/tunnel'; -import { TunnelService } from 'vs/workbench/services/remote/common/tunnelService'; +import { ITunnelService, TunnelService } from 'vs/platform/remote/common/tunnel'; import { ILoggerService } from 'vs/platform/log/common/log'; import { FileLoggerService } from 'vs/platform/log/common/fileLogService'; import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';