From d05ded6d3b64fed4a3cc74106f9b6c72243b18de Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 7 Apr 2021 15:14:56 -0700 Subject: [PATCH] Use service workers for loading webview resources on desktop (#120654) This switches us from using a custom protocol to using a service worker to load resources inside webviews. Previously we had only been using service workers on web (since custom protocols are not supported on web). The service worker based approach is much cleaner to than our custom protocol work and lets us avoid some extra roundtrips between the main process and the renderer We were previously blocked from using service workers on desktop due to an electron issue. However this has now been resolved --- src/main.js | 2 +- src/vs/code/electron-main/app.ts | 2 +- .../webview/common/webviewManagerService.ts | 28 -- .../electron-main/webviewMainService.ts | 61 +--- .../webviewPortMappingProvider.ts | 103 ------- .../electron-main/webviewProtocolProvider.ts | 278 +----------------- .../webview/browser/baseWebviewElement.ts | 148 +++++++++- .../contrib/webview/browser/pre/host.js | 83 +----- .../contrib/webview/browser/pre/main.js | 115 ++++++-- .../webview/browser/pre/service-worker.js | 15 +- .../contrib/webview/browser/webviewElement.ts | 161 ++-------- .../electron-browser/pre/electron-index.js | 42 +-- .../webview/electron-browser/pre/index.html | 44 ++- .../electron-browser/webviewElement.ts | 56 ++-- .../electron-sandbox/iframeWebviewElement.ts | 54 ++-- .../electron-sandbox/resourceLoading.ts | 151 ---------- .../electron-sandbox/environmentService.ts | 8 +- 17 files changed, 379 insertions(+), 972 deletions(-) delete mode 100644 src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts diff --git a/src/main.js b/src/main.js index 6e4d13816180b..9f7e230472091 100644 --- a/src/main.js +++ b/src/main.js @@ -59,7 +59,7 @@ if (portable && portable.isPortable) { protocol.registerSchemesAsPrivileged([ { scheme: 'vscode-webview', - privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true } + privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, allowServiceWorkers: true, } }, { scheme: 'vscode-webview-resource', privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index da8f72239698b..761d20f378611 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -186,7 +186,7 @@ export class CodeApplication extends Disposable { const uri = URI.parse(source); if (uri.scheme === Schemas.vscodeWebview) { - return uri.path === '/index.html' || uri.path === '/electron-browser/index.html'; + return uri.path === '/index.html' || uri.path === '/electron-browser-index.html'; } const srcUri = uri.fsPath.toLowerCase(); diff --git a/src/vs/platform/webview/common/webviewManagerService.ts b/src/vs/platform/webview/common/webviewManagerService.ts index 98fe61bfae58e..192d4734051a2 100644 --- a/src/vs/platform/webview/common/webviewManagerService.ts +++ b/src/vs/platform/webview/common/webviewManagerService.ts @@ -3,11 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSBuffer } from 'vs/base/common/buffer'; -import { 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'); @@ -19,32 +15,8 @@ export interface WebviewWindowId { readonly windowId: number; } -export type WebviewManagerDidLoadResourceResponse = - VSBuffer - | 'not-modified' - | 'access-denied' - | 'not-found'; - -export interface WebviewManagerDidLoadResourceResponseDetails { - readonly etag?: string; -} - export interface IWebviewManagerService { _serviceBrand: unknown; - registerWebview(id: string, windowId: number, metadata: RegisterWebviewMetadata): Promise; - unregisterWebview(id: string): Promise; - updateWebviewMetadata(id: string, metadataDelta: Partial): Promise; - - /** Note: the VSBuffer must be a top level argument so that it can be serialized and deserialized properly */ - didLoadResource(requestId: number, response: WebviewManagerDidLoadResourceResponse, responseDetails?: WebviewManagerDidLoadResourceResponseDetails): void; - setIgnoreMenuShortcuts(id: WebviewWebContentsId | WebviewWindowId, enabled: boolean): Promise; } - -export interface RegisterWebviewMetadata { - readonly extensionLocation: UriComponents | undefined; - readonly localResourceRoots: readonly UriComponents[]; - readonly remoteConnectionData: IRemoteConnectionData | null; - readonly portMappings: readonly IWebviewPortMapping[]; -} diff --git a/src/vs/platform/webview/electron-main/webviewMainService.ts b/src/vs/platform/webview/electron-main/webviewMainService.ts index 549dda62ad3c9..074df4dc93eb0 100644 --- a/src/vs/platform/webview/electron-main/webviewMainService.ts +++ b/src/vs/platform/webview/electron-main/webviewMainService.ts @@ -5,14 +5,9 @@ import { session, WebContents, 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 { ILogService } from 'vs/platform/log/common/log'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; -import { IRequestService } from 'vs/platform/request/common/request'; import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader'; -import { IWebviewManagerService, RegisterWebviewMetadata, WebviewManagerDidLoadResourceResponse, WebviewManagerDidLoadResourceResponseDetails, WebviewWebContentsId, WebviewWindowId } from 'vs/platform/webview/common/webviewManagerService'; -import { WebviewPortMappingProvider } from 'vs/platform/webview/electron-main/webviewPortMappingProvider'; +import { IWebviewManagerService, WebviewWebContentsId, WebviewWindowId } from 'vs/platform/webview/common/webviewManagerService'; import { WebviewProtocolProvider } from 'vs/platform/webview/electron-main/webviewProtocolProvider'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; @@ -20,19 +15,12 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer declare readonly _serviceBrand: undefined; - private readonly protocolProvider: WebviewProtocolProvider; - private readonly portMappingProvider: WebviewPortMappingProvider; - constructor( - @IFileService fileService: IFileService, - @ILogService logService: ILogService, - @IRequestService requestService: IRequestService, @ITunnelService tunnelService: ITunnelService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, ) { super(); - this.protocolProvider = this._register(new WebviewProtocolProvider(fileService, logService, requestService, windowsMainService)); - this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService)); + this._register(new WebviewProtocolProvider()); const sess = session.fromPartition(webviewPartitionId); sess.setPermissionRequestHandler((_webContents, permission, callback) => { @@ -48,43 +36,6 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer }); } - public async registerWebview(id: string, windowId: number, metadata: RegisterWebviewMetadata): Promise { - const extensionLocation = metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined; - - this.protocolProvider.registerWebview(id, { - ...metadata, - windowId: windowId, - extensionLocation, - localResourceRoots: metadata.localResourceRoots.map(x => URI.from(x)) - }); - - this.portMappingProvider.registerWebview(id, { - extensionLocation, - mappings: metadata.portMappings, - resolvedAuthority: metadata.remoteConnectionData, - }); - } - - public async unregisterWebview(id: string): Promise { - this.protocolProvider.unregisterWebview(id); - this.portMappingProvider.unregisterWebview(id); - } - - public async updateWebviewMetadata(id: string, metaDataDelta: Partial): Promise { - const extensionLocation = metaDataDelta.extensionLocation ? URI.from(metaDataDelta.extensionLocation) : undefined; - - this.protocolProvider.updateWebviewMetadata(id, { - ...metaDataDelta, - extensionLocation, - localResourceRoots: metaDataDelta.localResourceRoots?.map(x => URI.from(x)), - }); - - this.portMappingProvider.updateWebviewMetadata(id, { - ...metaDataDelta, - extensionLocation, - }); - } - public async setIgnoreMenuShortcuts(id: WebviewWebContentsId | WebviewWindowId, enabled: boolean): Promise { let contents: WebContents | undefined; @@ -107,12 +58,4 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer contents.setIgnoreMenuShortcuts(enabled); } } - - public async didLoadResource( - requestId: number, - response: WebviewManagerDidLoadResourceResponse, - responseDetails?: WebviewManagerDidLoadResourceResponseDetails, - ): Promise { - this.protocolProvider.didLoadResource(requestId, response, responseDetails); - } } diff --git a/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts b/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts deleted file mode 100644 index f97a8bc0aae9a..0000000000000 --- a/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts +++ /dev/null @@ -1,103 +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 { OnBeforeRequestListenerDetails, session, webContents } from 'electron'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; -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 OnBeforeRequestListenerDetails_Extended extends OnBeforeRequestListenerDetails { - readonly lastCommittedOrigin?: string; -} - -interface PortMappingData { - readonly extensionLocation: URI | undefined; - readonly mappings: readonly IWebviewPortMapping[]; - readonly resolvedAuthority: IAddress | null | undefined; -} - -interface WebviewData { - readonly manager: WebviewPortMappingManager; - readonly metadata: PortMappingData; -} - -export class WebviewPortMappingProvider extends Disposable { - - private readonly _webviewData = 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: OnBeforeRequestListenerDetails_Extended, callback) => { - let webviewId: string | undefined; - try { - if (details.lastCommittedOrigin) { - const origin = URI.parse(details.lastCommittedOrigin); - webviewId = origin.authority; - } else if (typeof details.webContentsId === 'number') { - const contents = webContents.fromId(details.webContentsId); - const url = URI.parse(contents.getURL()); - if (url.scheme === Schemas.vscodeWebview) { - webviewId = url.authority; - } - } - } catch { - return callback({}); - } - - if (!webviewId) { - return callback({}); - } - - const entry = this._webviewData.get(webviewId); - if (!entry) { - return callback({}); - } - - const redirect = await entry.manager.getRedirect(entry.metadata.resolvedAuthority, details.url); - return callback(redirect ? { redirectURL: redirect } : {}); - }); - } - - public async registerWebview(id: string, 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, { metadata, manager }); - } - - public unregisterWebview(id: string): void { - const existing = this._webviewData.get(id); - if (existing) { - existing.manager.dispose(); - this._webviewData.delete(id); - } - } - - 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 c25169b9f5dda..f194444556918 100644 --- a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts +++ b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts @@ -4,53 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import { protocol, session } from 'electron'; -import { Readable } from 'stream'; -import { bufferToStream, VSBufferReadableStream } from 'vs/base/common/buffer'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { listenStream } from 'vs/base/common/stream'; import { URI } from 'vs/base/common/uri'; -import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { IRequestService } from 'vs/platform/request/common/request'; -import { loadLocalResource, readFileStream, WebviewFileReadResponse, webviewPartitionId, WebviewResourceFileReader, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader'; -import { WebviewManagerDidLoadResourceResponse, WebviewManagerDidLoadResourceResponseDetails } from 'vs/platform/webview/common/webviewManagerService'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader'; -interface WebviewMetadata { - readonly windowId: number; - readonly extensionLocation: URI | undefined; - readonly localResourceRoots: readonly URI[]; - readonly remoteConnectionData: IRemoteConnectionData | null; -} - -interface PendingResourceResult { - readonly response: WebviewManagerDidLoadResourceResponse; - readonly responseDetails?: WebviewManagerDidLoadResourceResponseDetails; -} export class WebviewProtocolProvider extends Disposable { private static validWebviewFilePaths = new Map([ ['/index.html', 'index.html'], - ['/electron-browser/index.html', 'index.html'], + ['/fake.html', 'fake.html'], + ['/electron-browser-index.html', 'index.html'], ['/main.js', 'main.js'], ['/host.js', 'host.js'], + ['/service-worker.js', 'service-worker.js'], ]); - private readonly webviewMetadata = new Map(); - - private requestIdPool = 1; - private readonly pendingResourceReads = new Map void }>(); - - constructor( - @IFileService private readonly fileService: IFileService, - @ILogService private readonly logService: ILogService, - @IRequestService private readonly requestService: IRequestService, - @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - ) { + constructor() { super(); const sess = session.fromPartition(webviewPartitionId); @@ -59,79 +30,6 @@ export class WebviewProtocolProvider extends Disposable { const webviewHandler = this.handleWebviewRequest.bind(this); protocol.registerFileProtocol(Schemas.vscodeWebview, webviewHandler); sess.protocol.registerFileProtocol(Schemas.vscodeWebview, webviewHandler); - - // Register the protocol loading webview resources both inside the webview and at the top level - const webviewResourceHandler = this.handleWebviewResourceRequest.bind(this); - protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, webviewResourceHandler); - sess.protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, webviewResourceHandler); - - this._register(toDisposable(() => { - protocol.unregisterProtocol(Schemas.vscodeWebviewResource); - sess.protocol.unregisterProtocol(Schemas.vscodeWebviewResource); - protocol.unregisterProtocol(Schemas.vscodeWebview); - sess.protocol.unregisterProtocol(Schemas.vscodeWebview); - })); - } - - private streamToNodeReadable(stream: VSBufferReadableStream): Readable { - return new class extends Readable { - private listening = false; - - _read(size?: number): void { - if (!this.listening) { - this.listening = true; - - listenStream(stream, { - onData: data => { - try { - if (!this.push(data.buffer)) { - stream.pause(); // pause the stream if we should not push anymore - } - } catch (error) { - this.emit(error); - } - }, - onError: error => { - this.emit('error', error); - }, - onEnd: () => { - try { - this.push(null); // signal EOS - } catch (error) { - this.emit(error); - } - } - }); - } - - // ensure the stream is flowing - stream.resume(); - } - - _destroy(error: Error | null, callback: (error: Error | null) => void): void { - stream.destroy(); - - callback(null); - } - }; - } - - public async registerWebview(id: string, metadata: WebviewMetadata): Promise { - this.webviewMetadata.set(id, metadata); - } - - public unregisterWebview(id: string): void { - this.webviewMetadata.delete(id); - } - - public async updateWebviewMetadata(id: string, metadataDelta: Partial): Promise { - const entry = this.webviewMetadata.get(id); - if (entry) { - this.webviewMetadata.set(id, { - ...entry, - ...metadataDelta, - }); - } } private async handleWebviewRequest( @@ -143,8 +41,8 @@ export class WebviewProtocolProvider extends Disposable { const entry = WebviewProtocolProvider.validWebviewFilePaths.get(uri.path); if (typeof entry === 'string') { const relativeResourcePath = uri.path.startsWith('/electron-browser') - ? `vs/workbench/contrib/webview/electron-browser/pre/${entry}` - : `vs/workbench/contrib/webview/browser/pre/${entry}`; + ? `vs/workbench/contrib/webview/electron-browser/pre/${entry} ` + : `vs/workbench/contrib/webview/browser/pre/${entry} `; const url = FileAccess.asFileUri(relativeResourcePath, require); return callback(decodeURIComponent(url.fsPath)); @@ -154,164 +52,4 @@ export class WebviewProtocolProvider extends Disposable { } callback({ error: -10 /* ACCESS_DENIED - https://cs.chromium.org/chromium/src/net/base/net_error_list.h?l=32 */ }); } - - private async handleWebviewResourceRequest( - request: Electron.ProtocolRequest, - callback: (stream: NodeJS.ReadableStream | Electron.ProtocolResponse) => void - ) { - try { - const uri = URI.parse(request.url); - const ifNoneMatch = request.headers['If-None-Match']; - - const id = uri.authority; - const metadata = this.webviewMetadata.get(id); - if (metadata) { - - // Try to further rewrite remote uris so that they go to the resolved server on the main thread - let rewriteUri: undefined | ((uri: URI) => URI); - if (metadata.remoteConnectionData) { - rewriteUri = (uri) => { - if (metadata.remoteConnectionData) { - if (uri.scheme === Schemas.vscodeRemote || (metadata.extensionLocation?.scheme === Schemas.vscodeRemote)) { - let host = metadata.remoteConnectionData.host; - if (host && host.indexOf(':') !== -1) { // IPv6 address - host = `[${host}]`; - } - return URI.parse(`http://${host}:${metadata.remoteConnectionData.port}`).with({ - path: '/vscode-remote-resource', - query: `tkn=${metadata.remoteConnectionData.connectionToken}&path=${encodeURIComponent(uri.path)}`, - }); - } - } - return uri; - }; - } - - const fileReader: WebviewResourceFileReader = { - readFileStream: async (resource: URI, etag: string | undefined): Promise => { - if (resource.scheme === Schemas.file) { - return readFileStream(this.fileService, resource, etag); - } - - // Unknown uri scheme. Try delegating the file read back to the renderer - // process which should have a file system provider registered for the uri. - - const window = this.windowsMainService.getWindowById(metadata.windowId); - if (!window) { - throw new FileOperationError('Could not find window for resource', FileOperationResult.FILE_NOT_FOUND); - } - - const requestId = this.requestIdPool++; - const p = new Promise(resolve => { - this.pendingResourceReads.set(requestId, { resolve }); - }); - - window.send(`vscode:loadWebviewResource-${id}`, requestId, uri, etag); - - const result = await p; - switch (result.response) { - case 'access-denied': - throw new FileOperationError('Could not read file', FileOperationResult.FILE_PERMISSION_DENIED); - - case 'not-found': - throw new FileOperationError('Could not read file', FileOperationResult.FILE_NOT_FOUND); - - case 'not-modified': - return WebviewFileReadResponse.NotModified; - - default: - return new WebviewFileReadResponse.StreamSuccess(bufferToStream(result.response), result.responseDetails?.etag); - } - } - }; - - const result = await loadLocalResource(uri, ifNoneMatch, { - extensionLocation: metadata.extensionLocation, - roots: metadata.localResourceRoots, - remoteConnectionData: metadata.remoteConnectionData, - rewriteUri, - }, fileReader, this.requestService, this.logService, CancellationToken.None); - - switch (result.type) { - case WebviewResourceResponse.Type.Success: - { - const cacheHeaders: Record = result.etag ? { - 'ETag': result.etag, - 'Cache-Control': 'no-cache' - } : {}; - - const ifNoneMatch = request.headers['If-None-Match']; - if (ifNoneMatch && result.etag === ifNoneMatch) { - /* - * Note that the server generating a 304 response MUST - * generate any of the following header fields that would - * have been sent in a 200 (OK) response to the same request: - * Cache-Control, Content-Location, Date, ETag, Expires, and Vary. - * (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) - */ - return callback({ - statusCode: 304, // not modified - data: undefined, // The request fails if `data` is not set - headers: { - 'Content-Type': result.mimeType, - 'Access-Control-Allow-Origin': '*', - ...cacheHeaders - } - }); - } - - return callback({ - statusCode: 200, - data: this.streamToNodeReadable(result.stream), - headers: { - 'Content-Type': result.mimeType, - 'Access-Control-Allow-Origin': '*', - ...cacheHeaders - } - }); - } - case WebviewResourceResponse.Type.NotModified: - { - /* - * Note that the server generating a 304 response MUST - * generate any of the following header fields that would - * have been sent in a 200 (OK) response to the same request: - * Cache-Control, Content-Location, Date, ETag, Expires, and Vary. - * (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) - */ - return callback({ - statusCode: 304, // not modified - data: undefined, // The request fails if `data` is not set - headers: { - 'Content-Type': result.mimeType, - 'Access-Control-Allow-Origin': '*', - } - }); - } - case WebviewResourceResponse.Type.AccessDenied: - { - console.error('Webview: Cannot load resource outside of protocol root'); - return callback({ data: undefined, statusCode: 401 }); - } - } - } - } catch { - // noop - } - - return callback({ data: undefined, statusCode: 404 }); - } - - public didLoadResource( - requestId: number, - response: WebviewManagerDidLoadResourceResponse, - responseDetails?: WebviewManagerDidLoadResourceResponseDetails, - ) { - const pendingRead = this.pendingResourceReads.get(requestId); - if (!pendingRead) { - throw new Error('Unknown request'); - } - this.pendingResourceReads.delete(requestId); - pendingRead.resolve({ response, responseDetails }); - } } diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index 086e64311e551..c66e828383008 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -4,14 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { streamToBuffer } from 'vs/base/common/buffer'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { ITunnelService } from 'vs/platform/remote/common/tunnel'; +import { IRequestService } from 'vs/platform/request/common/request'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { loadLocalResource, readFileStream, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader'; +import { WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { areWebviewContentOptionsEqual, WebviewContentOptions, WebviewExtensionDescription, WebviewMessageReceivedEvent, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -78,25 +87,55 @@ export abstract class BaseWebview extends Disposable { protected content: WebviewContent; + private readonly _portMappingManager: WebviewPortMappingManager; + + private readonly _fileService: IFileService; + private readonly _logService: ILogService; + private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService; + private readonly _requestService: IRequestService; + private readonly _telemetryService: ITelemetryService; + private readonly _tunnelService: ITunnelService; + protected readonly _environmentService: IWorkbenchEnvironmentService; + constructor( public readonly id: string, private readonly options: WebviewOptions, contentOptions: WebviewContentOptions, public extension: WebviewExtensionDescription | undefined, private readonly webviewThemeDataProvider: WebviewThemeDataProvider, - @INotificationService notificationService: INotificationService, - @ILogService private readonly _logService: ILogService, - @ITelemetryService private readonly _telemetryService: ITelemetryService, - @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService + services: { + environmentService: IWorkbenchEnvironmentService, + fileService: IFileService, + logService: ILogService, + notificationService: INotificationService, + remoteAuthorityResolverService: IRemoteAuthorityResolverService, + requestService: IRequestService, + telemetryService: ITelemetryService, + tunnelService: ITunnelService, + } ) { super(); + this._environmentService = services.environmentService; + this._fileService = services.fileService; + this._logService = services.logService; + this._remoteAuthorityResolverService = services.remoteAuthorityResolverService; + this._requestService = services.requestService; + this._telemetryService = services.telemetryService; + this._tunnelService = services.tunnelService; + this.content = { html: '', options: contentOptions, state: undefined }; + this._portMappingManager = this._register(new WebviewPortMappingManager( + () => this.extension?.location, + () => this.content.options.portMapping || [], + this._tunnelService + )); + this._element = this.createElement(options, contentOptions); const subscription = this._register(this.on(WebviewMessageChannels.webviewReady, () => { @@ -153,7 +192,7 @@ export abstract class BaseWebview extends Disposable { })); this._register(this.on<{ message: string }>(WebviewMessageChannels.fatalError, (e) => { - notificationService.error(localize('fatalErrorMessage', "Error loading webview: {0}", e.message)); + services.notificationService.error(localize('fatalErrorMessage', "Error loading webview: {0}", e.message)); })); this._register(this.on('did-keydown', (data: KeyboardEvent) => { @@ -167,6 +206,17 @@ export abstract class BaseWebview extends Disposable { this.handleKeyEvent('keyup', data); })); + this._register(this.on(WebviewMessageChannels.loadResource, (entry: any) => { + const rawPath = entry.path; + const normalizedPath = decodeURIComponent(rawPath); + const uri = URI.parse(normalizedPath.replace(/^\/([\w\-]+)\/(.+)$/, (_, scheme, path) => scheme + ':/' + path)); + this.loadResource(entry.id, rawPath, uri, entry.ifNoneMatch); + })); + + this._register(this.on(WebviewMessageChannels.loadLocalhost, (entry: any) => { + this.localLocalhost(entry.id, entry.origin); + })); + this.style(); this._register(webviewThemeDataProvider.onThemeDataChanged(this.style, this)); } @@ -240,7 +290,7 @@ export abstract class BaseWebview extends Disposable { this._hasAlertedAboutMissingCsp = true; if (this.extension && this.extension.id) { - if (this.environmentService.isExtensionDevelopment) { + if (this._environmentService.isExtensionDevelopment) { this._onMissingCsp.fire(this.extension.id); } @@ -391,4 +441,90 @@ export abstract class BaseWebview extends Disposable { this._send('execCommand', command); } } + + private async loadResource(id: number, requestPath: string, uri: URI, ifNoneMatch: string | undefined) { + try { + const remoteAuthority = this._environmentService.remoteAuthority; + const remoteConnectionData = remoteAuthority ? this._remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null; + const extensionLocation = this.extension?.location; + + // If we are loading a file resource from a remote extension, rewrite the uri to go remote + let rewriteUri: undefined | ((uri: URI) => URI); + if (extensionLocation?.scheme === Schemas.vscodeRemote) { + rewriteUri = (uri) => { + if (uri.scheme === Schemas.file && extensionLocation?.scheme === Schemas.vscodeRemote) { + return URI.from({ + scheme: Schemas.vscodeRemote, + authority: extensionLocation.authority, + path: '/vscode-resource', + query: JSON.stringify({ + requestResourcePath: uri.path + }) + }); + } + return uri; + }; + } + + const result = await loadLocalResource(uri, ifNoneMatch, { + extensionLocation: extensionLocation, + roots: this.content.options.localResourceRoots || [], + remoteConnectionData, + rewriteUri, + }, { + readFileStream: (resource, etag) => readFileStream(this._fileService, resource, etag), + }, this._requestService, this._logService, CancellationToken.None); + + switch (result.type) { + case WebviewResourceResponse.Type.Success: + { + const { buffer } = await streamToBuffer(result.stream); + return this._send('did-load-resource', { + id, + status: 200, + path: requestPath, + mime: result.mimeType, + data: buffer, + etag: result.etag, + }); + } + case WebviewResourceResponse.Type.NotModified: + { + return this._send('did-load-resource', { + id, + status: 304, // not modified + path: requestPath, + mime: result.mimeType, + }); + } + case WebviewResourceResponse.Type.AccessDenied: + { + return this._send('did-load-resource', { + id, + status: 401, // unauthorized + path: requestPath, + }); + } + } + } catch { + // noop + } + + return this._send('did-load-resource', { + id, + status: 404, + path: requestPath + }); + } + + private async localLocalhost(id: string, origin: string) { + const authority = this._environmentService.remoteAuthority; + const resolveAuthority = authority ? await this._remoteAuthorityResolverService.resolveAuthority(authority) : undefined; + const redirect = resolveAuthority ? await this._portMappingManager.getRedirect(resolveAuthority.authority, origin) : undefined; + return this._send('did-load-localhost', { + id, + origin, + location: redirect + }); + } } diff --git a/src/vs/workbench/contrib/webview/browser/pre/host.js b/src/vs/workbench/contrib/webview/browser/pre/host.js index 65fcf9bcab677..43ef1d029222b 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/host.js +++ b/src/vs/workbench/contrib/webview/browser/pre/host.js @@ -47,75 +47,6 @@ } }(); - function fatalError(/** @type {string} */ message) { - console.error(`Webview fatal error: ${message}`); - hostMessaging.postMessage('fatal-error', { message }); - } - - /** @type {Promise} */ - const workerReady = new Promise(async (resolveWorkerReady) => { - if (onElectron) { - return resolveWorkerReady(); - } - - if (!areServiceWorkersEnabled()) { - fatalError('Service Workers are not enabled in browser. Webviews will not work.'); - return resolveWorkerReady(); - } - - const expectedWorkerVersion = 1; - - navigator.serviceWorker.register('service-worker.js').then( - async registration => { - await navigator.serviceWorker.ready; - - const versionHandler = (event) => { - if (event.data.channel !== 'version') { - return; - } - - navigator.serviceWorker.removeEventListener('message', versionHandler); - if (event.data.version === expectedWorkerVersion) { - return resolveWorkerReady(); - } else { - // If we have the wrong version, try once to unregister and re-register - return registration.update() - .then(() => navigator.serviceWorker.ready) - .finally(resolveWorkerReady); - } - }; - navigator.serviceWorker.addEventListener('message', versionHandler); - registration.active.postMessage({ channel: 'version' }); - }, - error => { - fatalError(`Could not register service workers: ${error}.`); - resolveWorkerReady(); - }); - - const forwardFromHostToWorker = (channel) => { - hostMessaging.onMessage(channel, event => { - navigator.serviceWorker.ready.then(registration => { - registration.active.postMessage({ channel: channel, data: event.data.args }); - }); - }); - }; - forwardFromHostToWorker('did-load-resource'); - forwardFromHostToWorker('did-load-localhost'); - - navigator.serviceWorker.addEventListener('message', event => { - if (['load-resource', 'load-localhost'].includes(event.data.channel)) { - hostMessaging.postMessage(event.data.channel, event.data); - } - }); - }); - - function areServiceWorkersEnabled() { - try { - return !!navigator.serviceWorker; - } catch (e) { - return false; - } - } const unloadMonitor = new class { @@ -175,21 +106,15 @@ const host = { postMessage: hostMessaging.postMessage.bind(hostMessaging), onMessage: hostMessaging.onMessage.bind(hostMessaging), - ready: workerReady, - fakeLoad: !onElectron, onElectron: onElectron, useParentPostMessage: false, onIframeLoaded: (/** @type {HTMLIFrameElement} */ frame) => { unloadMonitor.onIframeLoaded(frame); }, - rewriteCSP: onElectron - ? (csp) => { - return csp.replace(/vscode-resource:(?=(\s|;|$))/g, 'vscode-webview-resource:'); - } - : (csp, endpoint) => { - const endpointUrl = new URL(endpoint); - return csp.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin); - } + rewriteCSP: (csp, endpoint) => { + const endpointUrl = new URL(endpoint); + return csp.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin); + } }; (/** @type {any} */ (window)).createWebviewManager(host); diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index a19f58048c240..edd0befa40bce 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -11,7 +11,6 @@ * focusIframeOnCreate?: boolean, * ready?: Promise, * onIframeLoaded?: (iframe: HTMLIFrameElement) => void, - * fakeLoad?: boolean, * rewriteCSP: (existingCSP: string, endpoint?: string) => string, * onElectron?: boolean, * useParentPostMessage: boolean, @@ -204,6 +203,68 @@ initialScrollProgress: undefined, }; + function fatalError(/** @type {string} */ message) { + console.error(`Webview fatal error: ${message}`); + host.postMessage('fatal-error', { message }); + } + + /** @type {Promise} */ + const workerReady = new Promise(async (resolveWorkerReady) => { + // if (onElectron) { + // return resolveWorkerReady(); + // } + + if (!areServiceWorkersEnabled()) { + fatalError('Service Workers are not enabled in browser. Webviews will not work.'); + return resolveWorkerReady(); + } + + const expectedWorkerVersion = 1; + + navigator.serviceWorker.register(`service-worker.js${self.location.search}`).then( + async registration => { + await navigator.serviceWorker.ready; + + const versionHandler = (event) => { + if (event.data.channel !== 'version') { + return; + } + + navigator.serviceWorker.removeEventListener('message', versionHandler); + if (event.data.version === expectedWorkerVersion) { + return resolveWorkerReady(); + } else { + // If we have the wrong version, try once to unregister and re-register + return registration.update() + .then(() => navigator.serviceWorker.ready) + .finally(resolveWorkerReady); + } + }; + navigator.serviceWorker.addEventListener('message', versionHandler); + registration.active.postMessage({ channel: 'version' }); + }, + error => { + fatalError(`Could not register service workers: ${error}.`); + resolveWorkerReady(); + }); + + const forwardFromHostToWorker = (channel) => { + host.onMessage(channel, (_event, data) => { + navigator.serviceWorker.ready.then(registration => { + registration.active.postMessage({ channel, data }); + }); + }); + }; + forwardFromHostToWorker('did-load-resource'); + forwardFromHostToWorker('did-load-localhost'); + + navigator.serviceWorker.addEventListener('message', event => { + if (['load-resource', 'load-localhost'].includes(event.data.channel)) { + host.postMessage(event.data.channel, event.data); + } + }); + }); + /** * @param {HTMLDocument?} document * @param {HTMLElement?} body @@ -489,7 +550,7 @@ let updateId = 0; host.onMessage('content', async (_event, data) => { const currentUpdateId = ++updateId; - await host.ready; + await workerReady; if (currentUpdateId !== updateId) { return; } @@ -534,42 +595,33 @@ newFrame.setAttribute('frameborder', '0'); newFrame.setAttribute('sandbox', options.allowScripts ? 'allow-scripts allow-forms allow-same-origin allow-pointer-lock allow-downloads' : 'allow-same-origin allow-pointer-lock'); newFrame.setAttribute('allow', options.allowScripts ? 'clipboard-read; clipboard-write;' : ''); - if (host.fakeLoad) { - // We should just be able to use srcdoc, but I wasn't - // seeing the service worker applying properly. - // Fake load an empty on the correct origin and then write real html - // into it to get around this. - newFrame.src = `./fake.html?id=${ID}`; - } else { - newFrame.src = `about:blank?webviewFrame`; - } + // We should just be able to use srcdoc, but I wasn't + // seeing the service worker applying properly. + // Fake load an empty on the correct origin and then write real html + // into it to get around this. + newFrame.src = `./fake.html?id=${ID}`; + newFrame.style.cssText = 'display: block; margin: 0; overflow: hidden; position: absolute; width: 100%; height: 100%; visibility: hidden'; document.body.appendChild(newFrame); - if (!host.fakeLoad) { - // write new content onto iframe - newFrame.contentDocument.open(); - } - /** * @param {Document} contentDocument */ function onFrameLoaded(contentDocument) { // Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=978325 setTimeout(() => { - if (host.fakeLoad) { - contentDocument.open(); - contentDocument.write(newDocument); - contentDocument.close(); - hookupOnLoadHandlers(newFrame); - } + contentDocument.open(); + contentDocument.write(newDocument); + contentDocument.close(); + hookupOnLoadHandlers(newFrame); + if (contentDocument) { applyStyles(contentDocument, contentDocument.body); } }, 0); } - if (host.fakeLoad && !options.allowScripts && isSafari) { + if (!options.allowScripts && isSafari) { // On Safari for iframes with scripts disabled, the `DOMContentLoaded` never seems to be fired. // Use polling instead. const interval = setInterval(() => { @@ -666,15 +718,6 @@ } } - if (!host.fakeLoad) { - hookupOnLoadHandlers(newFrame); - } - - if (!host.fakeLoad) { - newFrame.contentDocument.write(newDocument); - newFrame.contentDocument.close(); - } - host.postMessage('did-set-content', undefined); }); @@ -722,6 +765,14 @@ }); } + function areServiceWorkersEnabled() { + try { + return !!navigator.serviceWorker; + } catch (e) { + return false; + } + } + if (typeof module !== 'undefined') { module.exports = createWebviewManager; } else { diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index f6c93682fea0f..16cc5b4dce54a 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -15,11 +15,19 @@ const resourceCacheName = `vscode-resource-cache-${VERSION}`; const rootPath = sw.location.pathname.replace(/\/service-worker.js$/, ''); + +const searchParams = new URL(location.toString()).searchParams; +/** + * Origin used for resources + */ +const resourceOrigin = searchParams.get('vscode-resource-origin') ?? sw.origin; + /** * Root path for resources */ const resourceRoot = rootPath + '/vscode-resource'; + const resolveTimeout = 30000; /** @@ -163,12 +171,12 @@ sw.addEventListener('fetch', (event) => { const requestUrl = new URL(event.request.url); // See if it's a resource request - if (requestUrl.origin === sw.origin && requestUrl.pathname.startsWith(resourceRoot + '/')) { + if (requestUrl.origin === resourceOrigin && requestUrl.pathname.startsWith(resourceRoot + '/')) { return event.respondWith(processResourceRequest(event, requestUrl)); } // See if it's a localhost request - if (requestUrl.origin !== sw.origin && requestUrl.host.match(/^localhost:(\d+)$/)) { + if (requestUrl.origin !== sw.origin && requestUrl.host.match(/^(localhost|127.0.0.1|0.0.0.0):(\d+)$/)) { return event.respondWith(processLocalhostRequest(event, requestUrl)); } }); @@ -308,6 +316,7 @@ async function getOuterIframeClient(webviewId) { const allClients = await sw.clients.matchAll({ includeUncontrolled: true }); return allClients.find(client => { const clientUrl = new URL(client.url); - return (clientUrl.pathname === `${rootPath}/` || clientUrl.pathname === `${rootPath}/index.html`) && clientUrl.search.match(new RegExp('\\bid=' + webviewId)); + const hasExpectedPathName = (clientUrl.pathname === `${rootPath}/` || clientUrl.pathname === `${rootPath}/index.html` || clientUrl.pathname === `${rootPath}/electron-browser-index.html`); + return hasExpectedPathName && clientUrl.search.match(new RegExp('\\bid=' + webviewId)); }); } diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 34a48da259aa1..d3925d3bfac97 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -5,11 +5,7 @@ import { addDisposableListener } from 'vs/base/browser/dom'; import { ThrottledDelayer } from 'vs/base/common/async'; -import { streamToBuffer } from 'vs/base/common/buffer'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; -import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; @@ -18,8 +14,6 @@ import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remot import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { IRequestService } from 'vs/platform/request/common/request'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { loadLocalResource, readFileStream, 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 { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; @@ -27,7 +21,6 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ export class IFrameWebview extends BaseWebview implements Webview { - private readonly _portMappingManager: WebviewPortMappingManager; private _confirmBeforeClose: string; private readonly _focusDelayer = this._register(new ThrottledDelayer(10)); @@ -39,17 +32,26 @@ export class IFrameWebview extends BaseWebview implements Web contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined, webviewThemeDataProvider: WebviewThemeDataProvider, + @IConfigurationService configurationService: IConfigurationService, + @IFileService fileService: IFileService, + @ILogService logService: ILogService, @INotificationService notificationService: INotificationService, - @ITunnelService tunnelService: ITunnelService, - @IFileService private readonly fileService: IFileService, - @IRequestService private readonly requestService: IRequestService, + @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, + @IRequestService requestService: IRequestService, @ITelemetryService telemetryService: ITelemetryService, + @ITunnelService tunnelService: ITunnelService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, - @ILogService private readonly logService: ILogService, ) { - super(id, options, contentOptions, extension, webviewThemeDataProvider, notificationService, logService, telemetryService, environmentService); + super(id, options, contentOptions, extension, webviewThemeDataProvider, { + notificationService, + logService, + telemetryService, + environmentService, + requestService, + fileService, + tunnelService, + remoteAuthorityResolverService + }); /* __GDPR__ "webview.createWebview" : { @@ -62,28 +64,11 @@ export class IFrameWebview extends BaseWebview implements Web webviewElementType: 'iframe', }); - this._portMappingManager = this._register(new WebviewPortMappingManager( - () => this.extension?.location, - () => this.content.options.portMapping || [], - tunnelService - )); - - this._register(this.on(WebviewMessageChannels.loadResource, (entry: any) => { - const rawPath = entry.path; - const normalizedPath = decodeURIComponent(rawPath); - const uri = URI.parse(normalizedPath.replace(/^\/([\w\-]+)\/(.+)$/, (_, scheme, path) => scheme + ':/' + path)); - this.loadResource(entry.id, rawPath, uri, entry.ifNoneMatch); - })); - - this._register(this.on(WebviewMessageChannels.loadLocalhost, (entry: any) => { - this.localLocalhost(entry.id, entry.origin); - })); - - this._confirmBeforeClose = this._configurationService.getValue('window.confirmBeforeClose'); + this._confirmBeforeClose = configurationService.getValue('window.confirmBeforeClose'); - this._register(this._configurationService.onDidChangeConfiguration(e => { + this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('window.confirmBeforeClose')) { - this._confirmBeforeClose = this._configurationService.getValue('window.confirmBeforeClose'); + this._confirmBeforeClose = configurationService.getValue('window.confirmBeforeClose'); this._send(WebviewMessageChannels.setConfirmBeforeClose, this._confirmBeforeClose); } })); @@ -119,20 +104,24 @@ export class IFrameWebview extends BaseWebview implements Web } as const; const queryString = (Object.keys(params) as Array) - .map((key) => `${key}=${params[key]}`) + .map((key) => `${key}=${encodeURIComponent(params[key]!)}`) .join('&'); - this.element!.setAttribute('src', `${this.externalEndpoint}/index.html?${queryString}`); + this.element!.setAttribute('src', `${this.webviewContentEndpoint}/index.html?${queryString}`); } - private get externalEndpoint(): string { - const endpoint = this.environmentService.webviewExternalEndpoint!.replace('{{uuid}}', this.id); + protected get webviewContentEndpoint(): string { + const endpoint = this._environmentService.webviewExternalEndpoint!.replace('{{uuid}}', this.id); if (endpoint[endpoint.length - 1] === '/') { return endpoint.slice(0, endpoint.length - 1); } return endpoint; } + protected get webviewResourceEndpoint(): string { + return this.webviewContentEndpoint; + } + public mountTo(parent: HTMLElement) { if (this.element) { parent.appendChild(this.element); @@ -147,21 +136,21 @@ export class IFrameWebview extends BaseWebview implements Web return value .replace(/(["'])(?:vscode-resource):(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => { if (scheme) { - return `${startQuote}${this.externalEndpoint}/vscode-resource/${scheme}${path}${endQuote}`; + return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/${scheme}${path}${endQuote}`; } - return `${startQuote}${this.externalEndpoint}/vscode-resource/file${path}${endQuote}`; + return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/file${path}${endQuote}`; }) .replace(/(["'])(?:vscode-webview-resource):(\/\/[^\s\/'"]+\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => { if (scheme) { - return `${startQuote}${this.externalEndpoint}/vscode-resource/${scheme}${path}${endQuote}`; + return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/${scheme}${path}${endQuote}`; } - return `${startQuote}${this.externalEndpoint}/vscode-resource/file${path}${endQuote}`; + return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/file${path}${endQuote}`; }); } protected get extraContentOptions(): any { return { - endpoint: this.externalEndpoint, + endpoint: this.webviewContentEndpoint, confirmBeforeClose: this._confirmBeforeClose, }; } @@ -178,92 +167,6 @@ export class IFrameWebview extends BaseWebview implements Web throw new Error('Method not implemented.'); } - private async loadResource(id: number, requestPath: string, uri: URI, ifNoneMatch: string | undefined) { - try { - const remoteAuthority = this.environmentService.remoteAuthority; - const remoteConnectionData = remoteAuthority ? this._remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null; - const extensionLocation = this.extension?.location; - - // If we are loading a file resource from a remote extension, rewrite the uri to go remote - let rewriteUri: undefined | ((uri: URI) => URI); - if (extensionLocation?.scheme === Schemas.vscodeRemote) { - rewriteUri = (uri) => { - if (uri.scheme === Schemas.file && extensionLocation?.scheme === Schemas.vscodeRemote) { - return URI.from({ - scheme: Schemas.vscodeRemote, - authority: extensionLocation.authority, - path: '/vscode-resource', - query: JSON.stringify({ - requestResourcePath: uri.path - }) - }); - } - return uri; - }; - } - - const result = await loadLocalResource(uri, ifNoneMatch, { - extensionLocation: extensionLocation, - roots: this.content.options.localResourceRoots || [], - remoteConnectionData, - rewriteUri, - }, { - readFileStream: (resource, etag) => readFileStream(this.fileService, resource, etag), - }, this.requestService, this.logService, CancellationToken.None); - - switch (result.type) { - case WebviewResourceResponse.Type.Success: - { - const { buffer } = await streamToBuffer(result.stream); - return this._send('did-load-resource', { - id, - status: 200, - path: requestPath, - mime: result.mimeType, - data: buffer, - etag: result.etag, - }); - } - case WebviewResourceResponse.Type.NotModified: - { - return this._send('did-load-resource', { - id, - status: 304, // not modified - path: requestPath, - mime: result.mimeType, - }); - } - case WebviewResourceResponse.Type.AccessDenied: - { - return this._send('did-load-resource', { - id, - status: 401, // unauthorized - path: requestPath, - }); - } - } - } catch { - // noop - } - - return this._send('did-load-resource', { - id, - status: 404, - path: requestPath - }); - } - - private async localLocalhost(id: string, origin: string) { - const authority = this.environmentService.remoteAuthority; - const resolveAuthority = authority ? await this._remoteAuthorityResolverService.resolveAuthority(authority) : undefined; - const redirect = resolveAuthority ? await this._portMappingManager.getRedirect(resolveAuthority.authority, origin) : undefined; - return this._send('did-load-localhost', { - id, - origin, - location: redirect - }); - } - protected doPostMessage(channel: string, data?: any): void { if (this.element) { this.element.contentWindow!.postMessage({ channel, args: data }, '*'); diff --git a/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js b/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js index 5c6a14cbd4b87..735478b391a23 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js +++ b/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js @@ -6,12 +6,10 @@ (function () { 'use strict'; - const ipcRenderer = require('electron').ipcRenderer; - - let isInDevelopmentMode = false; + const { ipcRenderer, contextBridge } = require('electron'); /** - * @type {import('../../browser/pre/main').WebviewHost} + * @type {import('../../browser/pre/main').WebviewHost & {isInDevelopmentMode: boolean}} */ const host = { onElectron: true, @@ -23,44 +21,14 @@ ipcRenderer.on(channel, handler); }, focusIframeOnCreate: true, - onIframeLoaded: (newFrame) => { - newFrame.contentWindow.onbeforeunload = () => { - if (isInDevelopmentMode) { // Allow reloads while developing a webview - host.postMessage('do-reload'); - return false; - } - // Block navigation when not in development mode - console.log('prevented webview navigation'); - return false; - }; - - // Electron 4 eats mouseup events from inside webviews - // https://github.com/microsoft/vscode/issues/75090 - // Try to fix this by rebroadcasting mouse moves and mouseups so that we can - // emulate these on the main window - let isMouseDown = false; - newFrame.contentWindow.addEventListener('mousedown', () => { - isMouseDown = true; - }); - - const tryDispatchSyntheticMouseEvent = (e) => { - if (!isMouseDown) { - host.postMessage('synthetic-mouse-event', { type: e.type, screenX: e.screenX, screenY: e.screenY, clientX: e.clientX, clientY: e.clientY }); - } - }; - newFrame.contentWindow.addEventListener('mouseup', e => { - tryDispatchSyntheticMouseEvent(e); - isMouseDown = false; - }); - newFrame.contentWindow.addEventListener('mousemove', tryDispatchSyntheticMouseEvent); - }, rewriteCSP: (csp) => { return csp.replace(/vscode-resource:(?=(\s|;|$))/g, 'vscode-webview-resource:'); }, + isInDevelopmentMode: false }; host.onMessage('devtools-opened', () => { - isInDevelopmentMode = true; + host.isInDevelopmentMode = true; }); document.addEventListener('DOMContentLoaded', e => { @@ -70,5 +38,5 @@ }; }); - require('../../browser/pre/main')(host); + contextBridge.exposeInMainWorld('vscodeHost', host); }()); diff --git a/src/vs/workbench/contrib/webview/electron-browser/pre/index.html b/src/vs/workbench/contrib/webview/electron-browser/pre/index.html index 591caaf7d3a53..3b42f3510831e 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/electron-browser/pre/index.html @@ -1,8 +1,50 @@ + -Virtual Document + Virtual Document + + + + diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index d3d4930750cbf..0db327e9c512b 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -10,12 +10,15 @@ import { Emitter, Event } from 'vs/base/common/event'; import { once } from 'vs/base/common/functional'; import { IDisposable } from 'vs/base/common/lifecycle'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { ITunnelService } from 'vs/platform/remote/common/tunnel'; +import { IRequestService } from 'vs/platform/request/common/request'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader'; import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement'; @@ -23,7 +26,7 @@ import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/t import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewFindDelegate, WebviewFindWidget } from 'vs/workbench/contrib/webview/browser/webviewFindWidget'; import { WebviewIgnoreMenuShortcutsManager } from 'vs/workbench/contrib/webview/electron-browser/webviewIgnoreMenuShortcutsManager'; -import { rewriteVsCodeResourceUrls, WebviewResourceRequestManager } from 'vs/workbench/contrib/webview/electron-sandbox/resourceLoading'; +import { rewriteVsCodeResourceUrls } from 'vs/workbench/contrib/webview/electron-sandbox/resourceLoading'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export class ElectronWebviewBasedWebview extends BaseWebview implements Webview, WebviewFindDelegate { @@ -43,14 +46,9 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme private _webviewFindWidget: WebviewFindWidget | undefined; private _findStarted: boolean = false; - private readonly _resourceRequestManager: WebviewResourceRequestManager; - private readonly _focusDelayer = this._register(new ThrottledDelayer(10)); private _elementFocusImpl!: (options?: FocusOptions | undefined) => void; - private _isWebviewReadyForMessages = false; - private readonly _pendingMessages: Array<{ channel: string, data: any }> = []; - constructor( id: string, options: WebviewOptions, @@ -64,8 +62,21 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme @IConfigurationService configurationService: IConfigurationService, @IMainProcessService mainProcessService: IMainProcessService, @INotificationService notificationService: INotificationService, + @IFileService fileService: IFileService, + @IRequestService requestService: IRequestService, + @ITunnelService tunnelService: ITunnelService, + @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, ) { - super(id, options, contentOptions, extension, _webviewThemeDataProvider, notificationService, _myLogService, telemetryService, environmentService); + super(id, options, contentOptions, extension, _webviewThemeDataProvider, { + notificationService, + logService: _myLogService, + telemetryService, + environmentService, + fileService, + requestService, + tunnelService, + remoteAuthorityResolverService + }); /* __GDPR__ "webview.createWebview" : { @@ -82,18 +93,6 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme this._myLogService.debug(`Webview(${this.id}): init`); - this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options)); - this._resourceRequestManager.ensureReady() - .then(() => { - this._isWebviewReadyForMessages = true; - - while (this._pendingMessages.length) { - const { channel, data } = this._pendingMessages.shift()!; - this._myLogService.debug(`Webview(${this.id}): did post message on '${channel}'`); - this.element?.send(channel, data); - } - }); - this._register(addDisposableListener(this.element!, 'dom-ready', once(() => { this._register(ElectronWebviewBasedWebview.getWebviewKeyboardHandler(configurationService, mainProcessService).add(this.element!)); }))); @@ -162,7 +161,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme // and not the `vscode-file` URI because preload scripts are loaded // via node.js from the main side and only allow `file:` protocol this.element!.preload = FileAccess.asFileUri('./pre/electron-index.js', require).toString(true); - this.element!.src = `${Schemas.vscodeWebview}://${this.id}/electron-browser/index.html?platform=electron`; + this.element!.src = `${Schemas.vscodeWebview}://${this.id}/electron-browser-index.html?platform=electron&id=${this.id}&vscode-resource-origin=${encodeURIComponent(this.webviewResourceEndpoint)}`; } protected createElement(options: WebviewOptions) { @@ -189,16 +188,11 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme public set contentOptions(options: WebviewContentOptions) { this._myLogService.debug(`Webview(${this.id}): will set content options`); - this._resourceRequestManager.update(options); super.contentOptions = options; } - public set localResourcesRoot(resources: URI[]) { - this._resourceRequestManager.update({ - ...this.contentOptions, - localResourceRoots: resources, - }); - super.localResourcesRoot = resources; + private get webviewResourceEndpoint(): string { + return `https://${this.id}.vscode-webview-test.com`; } protected readonly extraContentOptions = {}; @@ -221,12 +215,6 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme } protected async doPostMessage(channel: string, data?: any): Promise { - this._myLogService.debug(`Webview(${this.id}): will post message on '${channel}'`); - if (!this._isWebviewReadyForMessages) { - this._pendingMessages.push({ channel, data }); - return; - } - this._myLogService.debug(`Webview(${this.id}): did post message on '${channel}'`); this.element?.send(channel, data); } diff --git a/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts b/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts index f39d993e1f025..3749c997f9752 100644 --- a/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts @@ -3,10 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFileService } from 'vs/platform/files/common/files'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { ILogService } from 'vs/platform/log/common/log'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; @@ -19,7 +17,7 @@ import { WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/bas import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { IFrameWebview } from 'vs/workbench/contrib/webview/browser/webviewElement'; -import { rewriteVsCodeResourceUrls, WebviewResourceRequestManager } from 'vs/workbench/contrib/webview/electron-sandbox/resourceLoading'; +import { rewriteVsCodeResourceUrls } from 'vs/workbench/contrib/webview/electron-sandbox/resourceLoading'; import { WindowIgnoreMenuShortcutsManager } from 'vs/workbench/contrib/webview/electron-sandbox/windowIgnoreMenuShortcutsManager'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -28,11 +26,6 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ */ export class ElectronIframeWebview extends IFrameWebview { - private readonly _resourceRequestManager: WebviewResourceRequestManager; - - private _isWebviewReadyForMessages = false; - private readonly _pendingMessages: Array<{ channel: string, data: any }> = []; - private readonly _webviewKeyboardHandler: WindowIgnoreMenuShortcutsManager; constructor( @@ -48,25 +41,13 @@ export class ElectronIframeWebview extends IFrameWebview { @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IRemoteAuthorityResolverService _remoteAuthorityResolverService: IRemoteAuthorityResolverService, @ILogService logService: ILogService, - @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IMainProcessService mainProcessService: IMainProcessService, @INotificationService noficationService: INotificationService, @INativeHostService nativeHostService: INativeHostService, ) { super(id, options, contentOptions, extension, webviewThemeDataProvider, - noficationService, tunnelService, fileService, requestService, telemetryService, environmentService, configurationService, _remoteAuthorityResolverService, logService); - - this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options)); - this._resourceRequestManager.ensureReady() - .then(() => { - this._isWebviewReadyForMessages = true; - - while (this._pendingMessages.length) { - const { channel, data } = this._pendingMessages.shift()!; - this.element?.contentWindow!.postMessage({ channel, args: data }, '*'); - } - }); + configurationService, fileService, logService, noficationService, _remoteAuthorityResolverService, requestService, telemetryService, tunnelService, environmentService); this._webviewKeyboardHandler = new WindowIgnoreMenuShortcutsManager(configurationService, mainProcessService, nativeHostService); @@ -80,32 +61,31 @@ export class ElectronIframeWebview extends IFrameWebview { } protected initElement(extension: WebviewExtensionDescription | undefined, options: WebviewOptions) { - super.initElement(extension, options, { platform: 'electron' }); + super.initElement(extension, options, { + platform: 'electron', + 'vscode-resource-origin': this.webviewResourceEndpoint, + }); } - public set contentOptions(options: WebviewContentOptions) { - this._resourceRequestManager.update(options); - super.contentOptions = options; + protected get webviewContentEndpoint(): string { + const endpoint = this._environmentService.webviewExternalEndpoint!.replace('{{uuid}}', this.id); + if (endpoint[endpoint.length - 1] === '/') { + return endpoint.slice(0, endpoint.length - 1); + } + return endpoint; } - public set localResourcesRoot(resources: URI[]) { - this._resourceRequestManager.update({ - ...this.contentOptions, - localResourceRoots: resources, - }); - super.localResourcesRoot = resources; + protected get webviewResourceEndpoint(): string { + return `https://${this.id}.vscode-webview-test.com`; } protected get extraContentOptions() { - return {}; + return { + endpoint: this.webviewContentEndpoint, + }; } protected async doPostMessage(channel: string, data?: any): Promise { - if (!this._isWebviewReadyForMessages) { - this._pendingMessages.push({ channel, data }); - return; - } - this.element?.contentWindow!.postMessage({ channel, args: data }, '*'); } diff --git a/src/vs/workbench/contrib/webview/electron-sandbox/resourceLoading.ts b/src/vs/workbench/contrib/webview/electron-sandbox/resourceLoading.ts index 1c7d715524086..6c120ef8a6216 100644 --- a/src/vs/workbench/contrib/webview/electron-sandbox/resourceLoading.ts +++ b/src/vs/workbench/contrib/webview/electron-sandbox/resourceLoading.ts @@ -3,25 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equals } from 'vs/base/common/arrays'; -import { streamToBuffer } from 'vs/base/common/buffer'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; -import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; -import { IFileService } from 'vs/platform/files/common/files'; -import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; -import { ILogService } from 'vs/platform/log/common/log'; -import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; -import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { IRequestService } from 'vs/platform/request/common/request'; -import { loadLocalResource, readFileStream, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader'; -import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService'; -import { IWebviewPortMapping } from 'vs/platform/webview/common/webviewPortMapping'; -import { WebviewContentOptions, WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; /** * Try to rewrite `vscode-resource:` urls in html @@ -43,136 +25,3 @@ export function rewriteVsCodeResourceUrls( }); } -/** - * Manages the loading of resources inside of a webview. - */ -export class WebviewResourceRequestManager extends Disposable { - - private readonly _webviewManagerService: IWebviewManagerService; - - private _localResourceRoots: ReadonlyArray; - private _portMappings: ReadonlyArray; - - private _ready: Promise; - - constructor( - private readonly id: string, - private readonly extension: WebviewExtensionDescription | undefined, - initialContentOptions: WebviewContentOptions, - @ILogService private readonly _logService: ILogService, - @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IMainProcessService mainProcessService: IMainProcessService, - @INativeHostService nativeHostService: INativeHostService, - @IFileService fileService: IFileService, - @IRequestService requestService: IRequestService, - ) { - super(); - - this._logService.debug(`WebviewResourceRequestManager(${this.id}): init`); - - this._webviewManagerService = ProxyChannel.toService(mainProcessService.getChannel('webview')); - - this._localResourceRoots = initialContentOptions.localResourceRoots || []; - this._portMappings = initialContentOptions.portMapping || []; - - const remoteAuthority = environmentService.remoteAuthority; - const remoteConnectionData = remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null; - - this._logService.debug(`WebviewResourceRequestManager(${this.id}): did-start-loading`); - - this._ready = this._webviewManagerService.registerWebview(this.id, nativeHostService.windowId, { - extensionLocation: this.extension?.location.toJSON(), - localResourceRoots: this._localResourceRoots.map(x => x.toJSON()), - remoteConnectionData: remoteConnectionData, - portMappings: this._portMappings, - }).then(() => { - this._logService.debug(`WebviewResourceRequestManager(${this.id}): did register`); - }); - - if (remoteAuthority) { - this._register(remoteAuthorityResolverService.onDidChangeConnectionData(() => { - const update = this._webviewManagerService.updateWebviewMetadata(this.id, { - remoteConnectionData: remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null, - }); - this._ready = this._ready.then(() => update); - })); - } - - this._register(toDisposable(() => this._webviewManagerService.unregisterWebview(this.id))); - - const loadResourceChannel = `vscode:loadWebviewResource-${id}`; - const loadResourceListener = async (_event: any, requestId: number, resource: UriComponents, ifNoneMatch: string | undefined) => { - const uri = URI.revive(resource); - try { - this._logService.debug(`WebviewResourceRequestManager(${this.id}): starting resource load. uri: ${uri}`); - - const response = await loadLocalResource(uri, ifNoneMatch, { - extensionLocation: this.extension?.location, - roots: this._localResourceRoots, - remoteConnectionData: remoteConnectionData, - }, { - readFileStream: (resource, etag) => readFileStream(fileService, resource, etag), - }, requestService, this._logService, CancellationToken.None); - - this._logService.debug(`WebviewResourceRequestManager(${this.id}): finished resource load. uri: ${uri}, type=${response.type}`); - - switch (response.type) { - case WebviewResourceResponse.Type.Success: - { - const buffer = await streamToBuffer(response.stream); - return this._webviewManagerService.didLoadResource(requestId, buffer, { etag: response.etag }); - } - case WebviewResourceResponse.Type.NotModified: - return this._webviewManagerService.didLoadResource(requestId, 'not-modified'); - - case WebviewResourceResponse.Type.AccessDenied: - return this._webviewManagerService.didLoadResource(requestId, 'access-denied'); - } - } catch { - // Noop - } - this._webviewManagerService.didLoadResource(requestId, 'not-found'); - }; - - ipcRenderer.on(loadResourceChannel, loadResourceListener); - this._register(toDisposable(() => ipcRenderer.removeListener(loadResourceChannel, loadResourceListener))); - } - - public update(options: WebviewContentOptions) { - const localResourceRoots = options.localResourceRoots || []; - const portMappings = options.portMapping || []; - - if (!this.needsUpdate(localResourceRoots, portMappings)) { - return; - } - - this._localResourceRoots = localResourceRoots; - this._portMappings = portMappings; - - this._logService.debug(`WebviewResourceRequestManager(${this.id}): will update`); - - const update = this._webviewManagerService.updateWebviewMetadata(this.id, { - localResourceRoots: localResourceRoots.map(x => x.toJSON()), - portMappings: portMappings, - }).then(() => { - this._logService.debug(`WebviewResourceRequestManager(${this.id}): did update`); - }); - - this._ready = this._ready.then(() => update); - } - - private needsUpdate( - localResourceRoots: readonly URI[], - portMappings: readonly IWebviewPortMapping[], - ): boolean { - return !( - equals(this._localResourceRoots, localResourceRoots, (a, b) => a.toString() === b.toString()) - && equals(this._portMappings, portMappings, (a, b) => a.extensionHostPort === b.extensionHostPort && a.webviewPort === b.webviewPort) - ); - } - - public ensureReady(): Promise { - return this._ready; - } -} diff --git a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts index 30a6f0125a4dd..8ce34492271a2 100644 --- a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts @@ -72,7 +72,13 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment get webviewExternalEndpoint(): string { return `${Schemas.vscodeWebview}://{{uuid}}`; } @memoize - get webviewResourceRoot(): string { return `${Schemas.vscodeWebviewResource}://{{uuid}}/{{resource}}`; } + get webviewResourceRoot(): string { + // On desktop, this endpoint is only used for the service worker to identify resouce loads and + // should never actually be requested. + // + // Required due to https://github.com/electron/electron/issues/28528 + return 'https://{{uuid}}.vscode-webview-test.com/vscode-resource/{{resource}}'; + } @memoize get webviewCspSource(): string { return `${Schemas.vscodeWebviewResource}:`; }