Skip to content

Commit 07f2fec

Browse files
mjbvzalexdima
authored andcommitted
More webview ports
1 parent e9a9f99 commit 07f2fec

6 files changed

Lines changed: 183 additions & 16 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 { createDecorator } from 'vs/platform/instantiation/common/instantiation';
7+
8+
export const ITunnelService = createDecorator<ITunnelService>('tunnelService');
9+
10+
export interface RemoteTunnel {
11+
readonly tunnelRemotePort: number;
12+
readonly tunnelLocalPort: number;
13+
14+
dispose(): void;
15+
}
16+
17+
export interface ITunnelService {
18+
_serviceBrand: any;
19+
20+
openTunnel(remotePort: number): Promise<RemoteTunnel> | undefined;
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
7+
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
8+
9+
export class TunnelService implements ITunnelService {
10+
_serviceBrand: any;
11+
12+
public constructor(
13+
) {
14+
}
15+
16+
openTunnel(remotePort: number): Promise<RemoteTunnel> | undefined {
17+
return undefined;
18+
}
19+
}
20+
21+
registerSingleton(ITunnelService, TunnelService);

src/vs/workbench/api/browser/mainThreadWindow.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,22 @@ import { URI, UriComponents } from 'vs/base/common/uri';
99
import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows';
1010
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
1111
import { ExtHostContext, ExtHostWindowShape, IExtHostContext, MainContext, MainThreadWindowShape } from '../common/extHost.protocol';
12+
import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
13+
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
1214

1315
@extHostNamedCustomer(MainContext.MainThreadWindow)
1416
export class MainThreadWindow implements MainThreadWindowShape {
1517

1618
private readonly proxy: ExtHostWindowShape;
1719
private disposables: IDisposable[] = [];
20+
private readonly _tunnels = new Map<number, Promise<RemoteTunnel>>();
1821

1922
constructor(
2023
extHostContext: IExtHostContext,
2124
@IWindowService private readonly windowService: IWindowService,
22-
@IWindowsService private readonly windowsService: IWindowsService
25+
@IWindowsService private readonly windowsService: IWindowsService,
26+
@ITunnelService private readonly tunnelService: ITunnelService,
27+
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService
2328
) {
2429
this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostWindow);
2530

@@ -29,13 +34,54 @@ export class MainThreadWindow implements MainThreadWindowShape {
2934

3035
dispose(): void {
3136
this.disposables = dispose(this.disposables);
37+
38+
for (const tunnel of this._tunnels.values()) {
39+
tunnel.then(tunnel => tunnel.dispose());
40+
}
41+
this._tunnels.clear();
3242
}
3343

3444
$getWindowVisibility(): Promise<boolean> {
3545
return this.windowService.isFocused();
3646
}
3747

38-
$openUri(uri: UriComponents): Promise<boolean> {
39-
return this.windowsService.openExternal(URI.revive(uri).toString(true));
48+
async $openUri(uriComponent: UriComponents): Promise<boolean> {
49+
const uri = URI.revive(uriComponent);
50+
if (!!this.environmentService.configuration.remoteAuthority) {
51+
if (uri.scheme === 'http' || uri.scheme === 'https') {
52+
const port = this.getLocalhostPort(uri);
53+
if (typeof port === 'number') {
54+
const tunnel = await this.getOrCreateTunnel(port);
55+
if (tunnel) {
56+
const tunneledUrl = uri.toString().replace(
57+
new RegExp(`^${uri.scheme}://localhost:${port}/`),
58+
`${uri.scheme}://localhost:${tunnel.tunnelLocalPort}/`);
59+
return this.windowsService.openExternal(tunneledUrl);
60+
}
61+
}
62+
}
63+
}
64+
65+
return this.windowsService.openExternal(uri.toString(true));
66+
}
67+
68+
private getLocalhostPort(uri: URI): number | undefined {
69+
const match = /^localhost:(\d+)$/.exec(uri.authority);
70+
if (match) {
71+
return +match[1];
72+
}
73+
return undefined;
74+
}
75+
76+
private getOrCreateTunnel(remotePort: number): Promise<RemoteTunnel> | undefined {
77+
const existing = this._tunnels.get(remotePort);
78+
if (existing) {
79+
return existing;
80+
}
81+
const tunnel = this.tunnelService.openTunnel(remotePort);
82+
if (tunnel) {
83+
this._tunnels.set(remotePort, tunnel);
84+
}
85+
return tunnel;
4086
}
4187
}

src/vs/workbench/browser/nodeless.simpleservices.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/resour
5757
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
5858
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
5959
import { Color, RGBA } from 'vs/base/common/color';
60+
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
6061
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
6162

6263
export const workspaceResource = URI.from({
@@ -1434,4 +1435,17 @@ export class SimpleWorkspacesService implements IWorkspacesService {
14341435

14351436
registerSingleton(IWorkspacesService, SimpleWorkspacesService);
14361437

1438+
//#endregion
1439+
1440+
//#region remote
1441+
1442+
class SimpleTunnelService implements ITunnelService {
1443+
_serviceBrand: any;
1444+
openTunnel(remotePort: number) {
1445+
return undefined;
1446+
}
1447+
}
1448+
1449+
registerSingleton(ITunnelService, SimpleTunnelService);
1450+
14371451
//#endregion

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

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,36 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
1717
import * as colorRegistry from 'vs/platform/theme/common/colorRegistry';
1818
import { DARK, ITheme, IThemeService, LIGHT } from 'vs/platform/theme/common/themeService';
1919
import { registerFileProtocol, WebviewProtocol } from 'vs/workbench/contrib/webview/electron-browser/webviewProtocols';
20-
import { areWebviewInputOptionsEqual } from '../browser/webviewEditorService';
21-
import { WebviewFindWidget } from '../browser/webviewFindWidget';
20+
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
2221
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
2322
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
23+
import { areWebviewInputOptionsEqual } from '../browser/webviewEditorService';
24+
import { WebviewFindWidget } from '../browser/webviewFindWidget';
2425
import { WebviewContentOptions, WebviewPortMapping, WebviewOptions, Webview } from 'vs/workbench/contrib/webview/common/webview';
26+
import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
2527
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
2628
import { IEditorOptions, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
2729

30+
export interface WebviewPortMapping {
31+
readonly port: number;
32+
readonly resolvedPort: number;
33+
}
34+
35+
export interface WebviewOptions {
36+
readonly allowSvgs?: boolean;
37+
readonly extension?: {
38+
readonly location: URI;
39+
readonly id?: ExtensionIdentifier;
40+
};
41+
readonly enableFindWidget?: boolean;
42+
}
43+
44+
export interface WebviewContentOptions {
45+
readonly allowScripts?: boolean;
46+
readonly svgWhiteList?: string[];
47+
readonly localResourceRoots?: ReadonlyArray<URI>;
48+
readonly portMappings?: ReadonlyArray<WebviewPortMapping>;
49+
}
2850

2951
interface IKeydownEvent {
3052
key: string;
@@ -126,11 +148,15 @@ class WebviewProtocolProvider extends Disposable {
126148

127149
class WebviewPortMappingProvider extends Disposable {
128150

151+
private readonly _tunnels = new Map<number, Promise<RemoteTunnel>>();
152+
129153
constructor(
130154
session: WebviewSession,
155+
extensionLocation: URI | undefined,
131156
mappings: () => ReadonlyArray<WebviewPortMapping>,
157+
private readonly tunnelService: ITunnelService,
132158
extensionId: ExtensionIdentifier | undefined,
133-
@ITelemetryService telemetryService: ITelemetryService,
159+
@ITelemetryService telemetryService: ITelemetryService
134160
) {
135161
super();
136162

@@ -148,7 +174,7 @@ class WebviewPortMappingProvider extends Disposable {
148174
hasLogged = true;
149175

150176
/* __GDPR__
151-
"webview.accessLocalhost" : {
177+
"webview.accessLocalhost" : {
152178
"extension" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
153179
}
154180
*/
@@ -157,19 +183,53 @@ class WebviewPortMappingProvider extends Disposable {
157183

158184
const port = +localhostMatch[1];
159185
for (const mapping of mappings()) {
160-
if (mapping.port === port && mapping.port !== mapping.resolvedPort) {
161-
return {
162-
redirectURL: details.url.replace(
163-
new RegExp(`^${uri.scheme}://localhost:${mapping.port}/`),
164-
`${uri.scheme}://localhost:${mapping.resolvedPort}/`)
165-
};
186+
if (mapping.port === port) {
187+
if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) {
188+
const tunnel = await this.getOrCreateTunnel(mapping.resolvedPort);
189+
if (tunnel) {
190+
return {
191+
redirectURL: details.url.replace(
192+
new RegExp(`^${uri.scheme}://localhost:${mapping.port}/`),
193+
`${uri.scheme}://localhost:${tunnel.tunnelLocalPort}/`)
194+
};
195+
}
196+
}
197+
198+
if (mapping.port !== mapping.resolvedPort) {
199+
return {
200+
redirectURL: details.url.replace(
201+
new RegExp(`^${uri.scheme}://localhost:${mapping.port}/`),
202+
`${uri.scheme}://localhost:${mapping.resolvedPort}/`)
203+
};
204+
}
166205
}
167206
}
168207
}
169208

170209
return undefined;
171210
});
172211
}
212+
213+
dispose() {
214+
super.dispose();
215+
216+
for (const tunnel of this._tunnels.values()) {
217+
tunnel.then(tunnel => tunnel.dispose());
218+
}
219+
this._tunnels.clear();
220+
}
221+
222+
private getOrCreateTunnel(remotePort: number): Promise<RemoteTunnel> | undefined {
223+
const existing = this._tunnels.get(remotePort);
224+
if (existing) {
225+
return existing;
226+
}
227+
const tunnel = this.tunnelService.openTunnel(remotePort);
228+
if (tunnel) {
229+
this._tunnels.set(remotePort, tunnel);
230+
}
231+
return tunnel;
232+
}
173233
}
174234

175235
class SvgBlocker extends Disposable {
@@ -314,6 +374,7 @@ export class WebviewElement extends Disposable implements Webview {
314374
@IThemeService themeService: IThemeService,
315375
@IEnvironmentService environmentService: IEnvironmentService,
316376
@IFileService fileService: IFileService,
377+
@ITunnelService tunnelService: ITunnelService,
317378
@ITelemetryService telemetryService: ITelemetryService,
318379
@IConfigurationService private readonly _configurationService: IConfigurationService,
319380
) {
@@ -354,10 +415,12 @@ export class WebviewElement extends Disposable implements Webview {
354415

355416
this._register(new WebviewPortMappingProvider(
356417
session,
357-
() => (this._contentOptions.portMappings || []),
418+
_options.extension ? _options.extension.location : undefined,
419+
() => (this._contentOptions.portMappings || [{ port: 3000, resolvedPort: 4000 }]),
420+
tunnelService,
358421
_options.extension ? _options.extension.id : undefined,
359-
telemetryService));
360-
422+
telemetryService
423+
));
361424

362425
if (!this._options.allowSvgs) {
363426
const svgBlocker = this._register(new SvgBlocker(session, this._contentOptions));

src/vs/workbench/workbench.main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ import { MenubarService } from 'vs/platform/menubar/electron-browser/menubarServ
8888
import { IURLService } from 'vs/platform/url/common/url';
8989
import { RelayURLService } from 'vs/platform/url/electron-browser/urlService';
9090

91+
import 'vs/platform/remote/node/tunnelService';
92+
9193
import 'vs/workbench/services/bulkEdit/browser/bulkEditService';
9294
import 'vs/workbench/services/integrity/node/integrityService';
9395
import 'vs/workbench/services/keybinding/common/keybindingEditing';

0 commit comments

Comments
 (0)