diff --git a/src/main.js b/src/main.js index 6e4d13816180b2..9f7e2304720914 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 da8f72239698bd..761d20f378611b 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 98fe61bfae58e2..192d4734051a2b 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 549dda62ad3c99..074df4dc93eb01 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 f97a8bc0aae9aa..00000000000000 --- 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 c25169b9f5dda4..f1944445569181 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 086e64311e5519..c66e8283830087 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 65fcf9bcab6772..43ef1d029222b0 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 a19f58048c240c..edd0befa40bce0 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 f6c93682fea0fc..16cc5b4dce54a9 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 34a48da259aa1b..d3925d3bfac97b 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 5c6a14cbd4b87c..735478b391a232 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 591caaf7d3a537..3b42f3510831e7 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 d3d4930750cbf8..0db327e9c512b0 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 f39d993e1f025a..3749c997f9752e 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 1c7d7155240863..6c120ef8a62164 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 30a6f0125a4dd4..8ce34492271a2b 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}:`; }