Skip to content

Commit 55bb8f1

Browse files
authored
remote: allow loading resources for managed remote authorities (microsoft#185720)
This implements basically the same 'loopback' that vscode.dev does via a service worker to load resources for managed remote authorities in Electron. Except instead of using a service worker, it uses a buffer protocol via a new 'vscode-managed-remote-resource' scheme. It finds the window that the request came from and asks it to load the remote file. I initially looked at folding it into the existing 'vscode-remote-resource' scheme, but we needed a "buffer protocol" for this and it seems http protocols can _only_ respond with an HTTP URL, which we don't want here.
1 parent 70d6aa9 commit 55bb8f1

File tree

9 files changed

+150
-8
lines changed

9 files changed

+150
-8
lines changed

src/vs/base/common/network.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export namespace Schemas {
5353

5454
export const vscodeRemoteResource = 'vscode-remote-resource';
5555

56+
export const vscodeManagedRemoteResource = 'vscode-managed-remote-resource';
57+
5658
export const vscodeUserData = 'vscode-userdata';
5759

5860
export const vscodeCustomEditor = 'vscode-custom-editor';

src/vs/code/electron-main/app.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ import { firstOrDefault } from 'vs/base/common/arrays';
121121
import { ILocalPtyService, LocalReconnectConstants, TerminalIpcChannels, TerminalSettingId } from 'vs/platform/terminal/common/terminal';
122122
import { ElectronPtyHostStarter } from 'vs/platform/terminal/electron-main/electronPtyHostStarter';
123123
import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService';
124+
import { NODE_REMOTE_RESOURCE_CHANNEL_NAME, NODE_REMOTE_RESOURCE_IPC_METHOD_NAME, NodeRemoteResourceResponse, NodeRemoteResourceRouter } from 'vs/platform/remote/common/electronRemoteResources';
125+
import { Lazy } from 'vs/base/common/lazy';
124126

125127
/**
126128
* The main VS Code application. There will only ever be one instance,
@@ -569,6 +571,9 @@ export class CodeApplication extends Disposable {
569571
// Setup Protocol URL Handlers
570572
const initialProtocolUrls = appInstantiationService.invokeFunction(accessor => this.setupProtocolUrlHandlers(accessor, mainProcessElectronServer));
571573

574+
// Setup vscode-remote-resource protocol handler.
575+
this.setupManagedRemoteResourceUrlHandler(mainProcessElectronServer);
576+
572577
// Signal phase: ready - before opening first window
573578
this.lifecycleMainService.phase = LifecycleMainPhase.Ready;
574579

@@ -620,6 +625,28 @@ export class CodeApplication extends Disposable {
620625
return initialProtocolUrls;
621626
}
622627

628+
private setupManagedRemoteResourceUrlHandler(mainProcessElectronServer: ElectronIPCServer) {
629+
const notFound = (): Electron.ProtocolResponse => ({ statusCode: 404, data: 'Not found' });
630+
const remoteResourceChannel = new Lazy(() => mainProcessElectronServer.getChannel(
631+
NODE_REMOTE_RESOURCE_CHANNEL_NAME,
632+
new NodeRemoteResourceRouter(),
633+
));
634+
635+
protocol.registerBufferProtocol(Schemas.vscodeRemoteResource, (request, callback) => {
636+
const url = URI.parse(request.url);
637+
if (!url.authority.startsWith('window:')) {
638+
return callback(notFound());
639+
}
640+
641+
remoteResourceChannel.value.call<NodeRemoteResourceResponse>(NODE_REMOTE_RESOURCE_IPC_METHOD_NAME, [url]).then(
642+
r => callback({ ...r, data: Buffer.from(r.body, 'base64') }),
643+
err => {
644+
this.logService.warn('error dispatching remote resource call', err);
645+
callback({ statusCode: 500, data: String(err) });
646+
});
647+
});
648+
}
649+
623650
private resolveInitialProtocolUrls(): IInitialProtocolUrls | undefined {
624651

625652
/**

src/vs/code/electron-sandbox/workbench/workbench-dev.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
data:
1515
blob:
1616
vscode-remote-resource:
17+
vscode-managed-remote-resource:
1718
https:
1819
;
1920
media-src
@@ -40,6 +41,7 @@
4041
font-src
4142
'self'
4243
vscode-remote-resource:
44+
vscode-managed-remote-resource:
4345
;
4446
require-trusted-types-for
4547
'script'

src/vs/code/electron-sandbox/workbench/workbench.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
data:
1515
blob:
1616
vscode-remote-resource:
17+
vscode-managed-remote-resource:
1718
https:
1819
;
1920
media-src
@@ -40,6 +41,7 @@
4041
font-src
4142
'self'
4243
vscode-remote-resource:
44+
vscode-managed-remote-resource:
4345
;
4446
require-trusted-types-for
4547
'script'
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { UriComponents } from 'vs/base/common/uri';
7+
import { Client, IClientRouter, IConnectionHub } from 'vs/base/parts/ipc/common/ipc';
8+
9+
export const NODE_REMOTE_RESOURCE_IPC_METHOD_NAME = 'request';
10+
11+
export const NODE_REMOTE_RESOURCE_CHANNEL_NAME = 'remoteResourceHandler';
12+
13+
export type NodeRemoteResourceResponse = { body: /* base64 */ string; mimeType?: string; statusCode: number };
14+
15+
export class NodeRemoteResourceRouter implements IClientRouter<string> {
16+
async routeCall(hub: IConnectionHub<string>, command: string, arg?: any): Promise<Client<string>> {
17+
if (command !== NODE_REMOTE_RESOURCE_IPC_METHOD_NAME) {
18+
throw new Error(`Call not found: ${command}`);
19+
}
20+
21+
const uri = arg[0] as (UriComponents | undefined);
22+
if (uri?.authority) {
23+
const connection = hub.connections.find(c => c.ctx === uri.authority);
24+
if (connection) {
25+
return connection;
26+
}
27+
}
28+
29+
throw new Error(`Caller not found`);
30+
}
31+
32+
routeEvent(_: IConnectionHub<string>, event: string): Promise<Client<string>> {
33+
throw new Error(`Event not found: ${event}`);
34+
}
35+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { VSBuffer, encodeBase64 } from 'vs/base/common/buffer';
7+
import { Event } from 'vs/base/common/event';
8+
import { Disposable } from 'vs/base/common/lifecycle';
9+
import { getMediaOrTextMime } from 'vs/base/common/mime';
10+
import { Schemas } from 'vs/base/common/network';
11+
import { URI } from 'vs/base/common/uri';
12+
import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
13+
import { FileOperationError, FileOperationResult, IFileContent, IFileService } from 'vs/platform/files/common/files';
14+
import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService';
15+
import { NODE_REMOTE_RESOURCE_CHANNEL_NAME, NODE_REMOTE_RESOURCE_IPC_METHOD_NAME, NodeRemoteResourceResponse } from 'vs/platform/remote/common/electronRemoteResources';
16+
17+
export class ElectronRemoteResourceLoader extends Disposable {
18+
constructor(
19+
private readonly windowId: number,
20+
@IMainProcessService mainProcessService: IMainProcessService,
21+
@IFileService private readonly fileService: IFileService,
22+
) {
23+
super();
24+
25+
const channel: IServerChannel = {
26+
listen<T>(_: unknown, event: string): Event<T> {
27+
throw new Error(`Event not found: ${event}`);
28+
},
29+
30+
call: (_: unknown, command: string, arg?: any): Promise<any> => {
31+
switch (command) {
32+
case NODE_REMOTE_RESOURCE_IPC_METHOD_NAME: return this.doRequest(URI.revive(arg[0]));
33+
}
34+
35+
throw new Error(`Call not found: ${command}`);
36+
}
37+
};
38+
39+
mainProcessService.registerChannel(NODE_REMOTE_RESOURCE_CHANNEL_NAME, channel);
40+
}
41+
42+
private async doRequest(uri: URI): Promise<NodeRemoteResourceResponse> {
43+
let content: IFileContent;
44+
try {
45+
const params = new URLSearchParams(uri.query);
46+
const actual = uri.with({
47+
scheme: params.get('scheme')!,
48+
authority: params.get('authority')!,
49+
query: '',
50+
});
51+
content = await this.fileService.readFile(actual);
52+
} catch (e) {
53+
const str = encodeBase64(VSBuffer.fromString(e.message));
54+
if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
55+
return { statusCode: 404, body: str };
56+
} else {
57+
return { statusCode: 500, body: str };
58+
}
59+
}
60+
61+
const mimeType = uri.path && getMediaOrTextMime(uri.path);
62+
return { statusCode: 200, body: encodeBase64(content.value), mimeType };
63+
}
64+
65+
public getResourceUriProvider() {
66+
return (uri: URI) => uri.with({
67+
scheme: Schemas.vscodeRemoteResource,
68+
authority: `window:${this.windowId}`,
69+
query: new URLSearchParams({ authority: uri.authority, scheme: uri.scheme }).toString(),
70+
});
71+
}
72+
}

src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { URI } from 'vs/base/common/uri';
1212
import { IProductService } from 'vs/platform/product/common/productService';
1313
import { IRemoteAuthorityResolverService, IRemoteConnectionData, RemoteConnectionType, ResolvedAuthority, ResolvedOptions, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver';
1414
import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts';
15+
import { ElectronRemoteResourceLoader } from 'vs/platform/remote/electron-sandbox/electronRemoteResourceLoader';
1516

1617
export class RemoteAuthorityResolverService extends Disposable implements IRemoteAuthorityResolverService {
1718

@@ -25,7 +26,7 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
2526
private readonly _canonicalURIRequests: Map<string, { input: URI; result: DeferredPromise<URI> }>;
2627
private _canonicalURIProvider: ((uri: URI) => Promise<URI>) | null;
2728

28-
constructor(@IProductService productService: IProductService) {
29+
constructor(@IProductService productService: IProductService, private readonly remoteResourceLoader: ElectronRemoteResourceLoader) {
2930
super();
3031
this._resolveAuthorityRequests = new Map<string, DeferredPromise<ResolverResult>>();
3132
this._connectionTokens = new Map<string, string>();
@@ -81,8 +82,9 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
8182
if (this._resolveAuthorityRequests.has(resolvedAuthority.authority)) {
8283
const request = this._resolveAuthorityRequests.get(resolvedAuthority.authority)!;
8384
if (resolvedAuthority.connectTo.type === RemoteConnectionType.WebSocket) {
84-
// todo@connor4312 need to implement some kind of loopback for ext host based messaging
8585
RemoteAuthorities.set(resolvedAuthority.authority, resolvedAuthority.connectTo.host, resolvedAuthority.connectTo.port);
86+
} else {
87+
RemoteAuthorities.setDelegate(this.remoteResourceLoader.getResourceUriProvider());
8688
}
8789
if (resolvedAuthority.connectionToken) {
8890
RemoteAuthorities.setConnectionToken(resolvedAuthority.authority, resolvedAuthority.connectionToken);

src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-sand
1212
suite('RemoteAuthorityResolverService', () => {
1313
test('issue #147318: RemoteAuthorityResolverError keeps the same type', async () => {
1414
const productService: IProductService = { _serviceBrand: undefined, ...product };
15-
const service = new RemoteAuthorityResolverService(productService);
15+
const service = new RemoteAuthorityResolverService(productService, undefined as any);
1616
const result = service.resolveAuthority('test+x');
1717
service._setResolvedAuthorityError('test+x', new RemoteAuthorityResolverError('something', RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable));
1818
try {

src/vs/workbench/electron-sandbox/desktop.main.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/co
5757
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
5858
import { BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory';
5959
import { RemoteSocketFactoryService, IRemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService';
60+
import { ElectronRemoteResourceLoader } from 'vs/platform/remote/electron-sandbox/electronRemoteResourceLoader';
6061

6162
export class DesktopMain extends Disposable {
6263

@@ -197,11 +198,6 @@ export class DesktopMain extends Disposable {
197198
const utilityProcessWorkerWorkbenchService = new UtilityProcessWorkerWorkbenchService(this.configuration.windowId, logService, mainProcessService);
198199
serviceCollection.set(IUtilityProcessWorkerWorkbenchService, utilityProcessWorkerWorkbenchService);
199200

200-
// Remote
201-
const remoteAuthorityResolverService = new RemoteAuthorityResolverService(productService);
202-
serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService);
203-
204-
205201
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
206202
//
207203
// NOTE: Please do NOT register services here. Use `registerSingleton()`
@@ -220,6 +216,10 @@ export class DesktopMain extends Disposable {
220216
const fileService = this._register(new FileService(logService));
221217
serviceCollection.set(IWorkbenchFileService, fileService);
222218

219+
// Remote
220+
const remoteAuthorityResolverService = new RemoteAuthorityResolverService(productService, new ElectronRemoteResourceLoader(environmentService.window.id, mainProcessService, fileService));
221+
serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService);
222+
223223
// Local Files
224224
const diskFileSystemProvider = this._register(new DiskFileSystemProvider(mainProcessService, utilityProcessWorkerWorkbenchService, logService));
225225
fileService.registerProvider(Schemas.file, diskFileSystemProvider);

0 commit comments

Comments
 (0)