Skip to content

Commit cf0094f

Browse files
authored
Add delegation back to renderer process for cetain webview resource reeds (microsoft#102442)
Fixes microsoft#102191 **Problem** Webviews used to load local resources in the renderer process. This gave them access to all of the file system providers registered for that renderer (such as `untitled` and `git`). However, last iteration we moved the webview protocol to the main process. This works fine for file resources, but the main process cannot read fancy uris such as `git:` or `untitled:` **Fix** To fix this, I've added code that delegates resource requests for non-file uris from the main process back to the renderer process. We tried to avoid this as it adds another round trip to each request, but I don't see a better way of doing this. This problem only effects desktop VS Code. Web VS Code uses a service worker which always reads resources through the renderer process
1 parent a7285b4 commit cf0094f

6 files changed

Lines changed: 118 additions & 19 deletions

File tree

src/vs/platform/webview/common/resourceLoader.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { isUNC } from 'vs/base/common/extpath';
99
import { Schemas } from 'vs/base/common/network';
1010
import { sep } from 'vs/base/common/path';
1111
import { URI } from 'vs/base/common/uri';
12-
import { IFileService } from 'vs/platform/files/common/files';
1312
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
1413
import { IRequestService } from 'vs/platform/request/common/request';
1514
import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes';
@@ -35,6 +34,10 @@ export namespace WebviewResourceResponse {
3534
export type StreamResponse = StreamSuccess | typeof Failed | typeof AccessDenied;
3635
}
3736

37+
interface FileReader {
38+
readFileStream(resource: URI): Promise<VSBufferReadableStream>;
39+
}
40+
3841
export async function loadLocalResource(
3942
requestUri: URI,
4043
options: {
@@ -43,7 +46,7 @@ export async function loadLocalResource(
4346
remoteConnectionData?: IRemoteConnectionData | null;
4447
rewriteUri?: (uri: URI) => URI,
4548
},
46-
fileService: IFileService,
49+
fileReader: FileReader,
4750
requestService: IRequestService,
4851
): Promise<WebviewResourceResponse.StreamResponse> {
4952
let resourceToLoad = getResourceToLoad(requestUri, options.roots);
@@ -67,8 +70,8 @@ export async function loadLocalResource(
6770
}
6871

6972
try {
70-
const contents = await fileService.readFileStream(resourceToLoad);
71-
return new WebviewResourceResponse.StreamSuccess(contents.value, mime);
73+
const contents = await fileReader.readFileStream(resourceToLoad);
74+
return new WebviewResourceResponse.StreamSuccess(contents, mime);
7275
} catch (err) {
7376
console.log(err);
7477
return WebviewResourceResponse.Failed;
@@ -96,8 +99,14 @@ function normalizeRequestPath(requestUri: URI) {
9699
//
97100
// vscode-webview-resource://id/scheme//authority?/path
98101
//
99-
const resourceUri = URI.parse(requestUri.path.replace(/^\/([a-z0-9\-]+)\/{1,2}/i, '$1://'));
100-
102+
const resourceUri = URI.parse(requestUri.path.replace(/^\/([a-z0-9\-]+)(\/{1,2})/i, (_: string, scheme: string, sep: string) => {
103+
if (sep.length === 1) {
104+
return `${scheme}:///`; // Add empty authority.
105+
} else {
106+
return `${scheme}://`; // Url has own authority.
107+
}
108+
}));
109+
console.log(requestUri, resourceUri);
101110
return resourceUri.with({
102111
query: requestUri.query,
103112
fragment: requestUri.fragment

src/vs/platform/webview/common/webviewManagerService.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { VSBuffer } from 'vs/base/common/buffer';
67
import { UriComponents } from 'vs/base/common/uri';
78
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
89
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
@@ -13,10 +14,12 @@ export const IWebviewManagerService = createDecorator<IWebviewManagerService>('w
1314
export interface IWebviewManagerService {
1415
_serviceBrand: unknown;
1516

16-
registerWebview(id: string, webContentsId: number | undefined, metadata: RegisterWebviewMetadata): Promise<void>;
17+
registerWebview(id: string, webContentsId: number | undefined, windowId: number, metadata: RegisterWebviewMetadata): Promise<void>;
1718
unregisterWebview(id: string): Promise<void>;
1819
updateWebviewMetadata(id: string, metadataDelta: Partial<RegisterWebviewMetadata>): Promise<void>;
1920

21+
didLoadResource(requestId: number, content: VSBuffer | undefined): void;
22+
2023
setIgnoreMenuShortcuts(webContentsId: number, enabled: boolean): Promise<void>;
2124
}
2225

src/vs/platform/webview/electron-main/webviewMainService.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { webContents } from 'electron';
7+
import { VSBuffer } from 'vs/base/common/buffer';
78
import { Disposable } from 'vs/base/common/lifecycle';
89
import { URI } from 'vs/base/common/uri';
910
import { IFileService } from 'vs/platform/files/common/files';
@@ -12,6 +13,7 @@ import { IRequestService } from 'vs/platform/request/common/request';
1213
import { IWebviewManagerService, RegisterWebviewMetadata } from 'vs/platform/webview/common/webviewManagerService';
1314
import { WebviewPortMappingProvider } from 'vs/platform/webview/electron-main/webviewPortMappingProvider';
1415
import { WebviewProtocolProvider } from 'vs/platform/webview/electron-main/webviewProtocolProvider';
16+
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
1517

1618
export class WebviewMainService extends Disposable implements IWebviewManagerService {
1719

@@ -24,17 +26,19 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer
2426
@IFileService fileService: IFileService,
2527
@IRequestService requestService: IRequestService,
2628
@ITunnelService tunnelService: ITunnelService,
29+
@IWindowsMainService windowsMainService: IWindowsMainService,
2730
) {
2831
super();
29-
this.protocolProvider = this._register(new WebviewProtocolProvider(fileService, requestService));
32+
this.protocolProvider = this._register(new WebviewProtocolProvider(fileService, requestService, windowsMainService));
3033
this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService));
3134
}
3235

33-
public async registerWebview(id: string, webContentsId: number | undefined, metadata: RegisterWebviewMetadata): Promise<void> {
36+
public async registerWebview(id: string, webContentsId: number | undefined, windowId: number, metadata: RegisterWebviewMetadata): Promise<void> {
3437
const extensionLocation = metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined;
3538

3639
this.protocolProvider.registerWebview(id, {
3740
...metadata,
41+
windowId: windowId,
3842
extensionLocation,
3943
localResourceRoots: metadata.localResourceRoots.map(x => URI.from(x))
4044
});
@@ -75,4 +79,8 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer
7579
contents.setIgnoreMenuShortcuts(enabled);
7680
}
7781
}
82+
83+
public async didLoadResource(requestId: number, content: VSBuffer | undefined): Promise<void> {
84+
this.protocolProvider.didLoadResource(requestId, content);
85+
}
7886
}

src/vs/platform/webview/electron-main/webviewProtocolProvider.ts

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,21 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { session, protocol } from 'electron';
6+
import { protocol, session } from 'electron';
77
import { Readable } from 'stream';
8-
import { VSBufferReadableStream } from 'vs/base/common/buffer';
8+
import { bufferToStream, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer';
99
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
1010
import { Schemas } from 'vs/base/common/network';
1111
import { URI } from 'vs/base/common/uri';
12-
import { IFileService } from 'vs/platform/files/common/files';
12+
import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files';
1313
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
14+
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
1415
import { IRequestService } from 'vs/platform/request/common/request';
1516
import { loadLocalResource, webviewPartitionId, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
16-
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
17+
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
1718

1819
interface WebviewMetadata {
20+
readonly windowId: number;
1921
readonly extensionLocation: URI | undefined;
2022
readonly localResourceRoots: readonly URI[];
2123
readonly remoteConnectionData: IRemoteConnectionData | null;
@@ -32,9 +34,13 @@ export class WebviewProtocolProvider extends Disposable {
3234

3335
private readonly webviewMetadata = new Map<string, WebviewMetadata>();
3436

37+
private requestIdPool = 1;
38+
private readonly pendingResourceReads = new Map<number, { resolve: (content: VSBuffer | undefined) => void }>();
39+
3540
constructor(
3641
@IFileService private readonly fileService: IFileService,
3742
@IRequestService private readonly requestService: IRequestService,
43+
@IWindowsMainService readonly windowsMainService: IWindowsMainService,
3844
) {
3945
super();
4046

@@ -166,12 +172,42 @@ export class WebviewProtocolProvider extends Disposable {
166172
};
167173
}
168174

175+
const fileService = {
176+
readFileStream: async (resource: URI): Promise<VSBufferReadableStream> => {
177+
if (uri.scheme === Schemas.file) {
178+
return (await this.fileService.readFileStream(resource)).value;
179+
}
180+
181+
// Unknown uri scheme. Try delegating the file read back to the renderer
182+
// process which should have a file system provider registered for the uri.
183+
184+
const window = this.windowsMainService.getWindowById(metadata.windowId);
185+
if (!window) {
186+
throw new FileOperationError('Could not find window for resource', FileOperationResult.FILE_NOT_FOUND);
187+
}
188+
189+
const requestId = this.requestIdPool++;
190+
const p = new Promise<VSBuffer | undefined>(resolve => {
191+
this.pendingResourceReads.set(requestId, { resolve });
192+
});
193+
194+
window.send(`vscode:loadWebviewResource-${id}`, requestId, uri);
195+
196+
const result = await p;
197+
if (!result) {
198+
throw new FileOperationError('Could not read file', FileOperationResult.FILE_NOT_FOUND);
199+
}
200+
201+
return bufferToStream(result);
202+
}
203+
};
204+
169205
const result = await loadLocalResource(uri, {
170206
extensionLocation: metadata.extensionLocation,
171207
roots: metadata.localResourceRoots,
172208
remoteConnectionData: metadata.remoteConnectionData,
173209
rewriteUri,
174-
}, this.fileService, this.requestService);
210+
}, fileService, this.requestService);
175211

176212
if (result.type === WebviewResourceResponse.Type.Success) {
177213
return callback({
@@ -195,4 +231,13 @@ export class WebviewProtocolProvider extends Disposable {
195231

196232
return callback({ data: null, statusCode: 404 });
197233
}
234+
235+
public didLoadResource(requestId: number, content: VSBuffer | undefined) {
236+
const pendingRead = this.pendingResourceReads.get(requestId);
237+
if (!pendingRead) {
238+
throw new Error('Unknown request');
239+
}
240+
this.pendingResourceReads.delete(requestId);
241+
pendingRead.resolve(content);
242+
}
198243
}

src/vs/workbench/contrib/webview/browser/webviewElement.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,9 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
167167
roots: this.content.options.localResourceRoots || [],
168168
remoteConnectionData,
169169
rewriteUri,
170-
}, this.fileService, this.requestService);
170+
}, {
171+
readFileStream: (resource) => this.fileService.readFileStream(resource).then(x => x.value),
172+
}, this.requestService);
171173

172174
if (result.type === WebviewResourceResponse.Type.Success) {
173175
const { buffer } = await streamToBuffer(result.stream);

src/vs/workbench/contrib/webview/electron-browser/resourceLoading.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,23 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { equals } from 'vs/base/common/arrays';
7+
import { streamToBuffer } from 'vs/base/common/buffer';
78
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
8-
import { URI } from 'vs/base/common/uri';
9+
import { Schemas } from 'vs/base/common/network';
10+
import { URI, UriComponents } from 'vs/base/common/uri';
911
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
12+
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
1013
import * as modes from 'vs/editor/common/modes';
14+
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
15+
import { IFileService } from 'vs/platform/files/common/files';
1116
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
1217
import { ILogService } from 'vs/platform/log/common/log';
1318
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
19+
import { IRequestService } from 'vs/platform/request/common/request';
20+
import { loadLocalResource, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
1421
import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService';
1522
import { WebviewContentOptions, WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview';
1623
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
17-
import { Schemas } from 'vs/base/common/network';
1824

1925
/**
2026
* Try to rewrite `vscode-resource:` urls in html
@@ -57,6 +63,9 @@ export class WebviewResourceRequestManager extends Disposable {
5763
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService,
5864
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
5965
@IMainProcessService mainProcessService: IMainProcessService,
66+
@IElectronService electronService: IElectronService,
67+
@IFileService fileService: IFileService,
68+
@IRequestService requestService: IRequestService,
6069
) {
6170
super();
6271

@@ -72,8 +81,7 @@ export class WebviewResourceRequestManager extends Disposable {
7281

7382
this._ready = getWebContentsId.then(async (webContentsId) => {
7483
this._logService.debug(`WebviewResourceRequestManager(${this.id}): did-start-loading`);
75-
76-
await this._webviewManagerService.registerWebview(this.id, webContentsId, {
84+
await this._webviewManagerService.registerWebview(this.id, webContentsId, electronService.windowId, {
7785
extensionLocation: this.extension?.location.toJSON(),
7886
localResourceRoots: this._localResourceRoots.map(x => x.toJSON()),
7987
remoteConnectionData: remoteConnectionData,
@@ -93,6 +101,30 @@ export class WebviewResourceRequestManager extends Disposable {
93101
}
94102

95103
this._register(toDisposable(() => this._webviewManagerService.unregisterWebview(this.id)));
104+
105+
const loadResourceChannel = `vscode:loadWebviewResource-${id}`;
106+
const loadResourceListener = async (_event: any, requestId: number, resource: UriComponents) => {
107+
try {
108+
const response = await loadLocalResource(URI.revive(resource), {
109+
extensionLocation: this.extension?.location,
110+
roots: this._localResourceRoots,
111+
remoteConnectionData: remoteConnectionData,
112+
}, {
113+
readFileStream: (resource) => fileService.readFileStream(resource).then(x => x.value),
114+
}, requestService);
115+
116+
if (response.type === WebviewResourceResponse.Type.Success) {
117+
const buffer = await streamToBuffer(response.stream);
118+
return this._webviewManagerService.didLoadResource(requestId, buffer);
119+
}
120+
} catch {
121+
// Noop
122+
}
123+
this._webviewManagerService.didLoadResource(requestId, undefined);
124+
};
125+
126+
ipcRenderer.on(loadResourceChannel, loadResourceListener);
127+
this._register(toDisposable(() => ipcRenderer.removeListener(loadResourceChannel, loadResourceListener)));
96128
}
97129

98130
public update(options: WebviewContentOptions) {

0 commit comments

Comments
 (0)