remote: first cut at 'inline' remote resolvers

For web, it seems the most feasible direction for resolvers as we make
existing remote extensions 'web enabled' is to allow them to run in the
extension host. However, in no case will there just be a simple
websocket we can connect to ordinarily.

This PR implements a first cut at 'inline' resolvers where messaging is
done in the extension host. I have not yet tested them on web, where I
think some more wiring is needed to mirror desktop. Also, resolution of
URLs is not in yet. I think for this we'd want to do some service-worker
-based 'loopback' approach to run requests inline in the remote
connection, similar to what I did for tunnels...

Resolvers are not yet run in a dedicated extension host, but I think
that should happen, at least on web where resolvers
will always(?) be 'inline'.

Most of the actual changes are genericizing places where we specified
the "host" and "port" previously into an enum. Additionally, instead of
having a single ISocketFactory, there's now a collection of them, which
the extension host manager registers into when a managed resolution
happens.
This commit is contained in:
Connor Peet 2023-04-18 14:51:14 -07:00
parent b242a8730c
commit f5427eed53
No known key found for this signature in database
GPG key ID: CF8FD2EA0DBC61BD
41 changed files with 826 additions and 272 deletions

View file

@ -66,6 +66,11 @@
"category": "Remote-TestResolver",
"command": "vscode-testresolver.currentWindow"
},
{
"title": "Connect to TestResolver in Current Window with Managed Connection",
"category": "Remote-TestResolver",
"command": "vscode-testresolver.currentWindowManaged"
},
{
"title": "Show TestResolver Log",
"category": "Remote-TestResolver",

View file

@ -27,7 +27,30 @@ export function activate(context: vscode.ExtensionContext) {
let connectionPaused = false;
const connectionPausedEvent = new vscode.EventEmitter<boolean>();
function doResolve(_authority: string, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise<vscode.ResolvedAuthority> {
function getTunnelFeatures(): vscode.TunnelInformation['tunnelFeatures'] {
return {
elevation: true,
privacyOptions: vscode.workspace.getConfiguration('testresolver').get('supportPublicPorts') ? [
{
id: 'public',
label: 'Public',
themeIcon: 'eye'
},
{
id: 'other',
label: 'Other',
themeIcon: 'circuit-board'
},
{
id: 'private',
label: 'Private',
themeIcon: 'eye-closed'
}
] : []
};
}
function doResolve(authority: string, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise<vscode.ResolverResult> {
if (connectionPaused) {
throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable('Not available right now');
}
@ -150,7 +173,35 @@ export function activate(context: vscode.ExtensionContext) {
}
});
});
return serverPromise.then(serverAddr => {
return serverPromise.then((serverAddr): Promise<vscode.ResolverResult> => {
if (authority.includes('managed')) {
console.log('Connecting via a managed authority');
return Promise.resolve(new vscode.ManagedResolvedAuthority(async () => {
const remoteSocket = net.createConnection({ port: serverAddr.port });
const dataEmitter = new vscode.EventEmitter<Uint8Array>();
const closeEmitter = new vscode.EventEmitter<Error | undefined>();
const endEmitter = new vscode.EventEmitter<void>();
await new Promise((res, rej) => {
remoteSocket.on('data', d => dataEmitter.fire(d))
.on('error', err => { rej(); closeEmitter.fire(err); })
.on('close', () => endEmitter.fire())
.on('end', () => endEmitter.fire())
.on('connect', res);
});
return {
onDidReceiveMessage: dataEmitter.event,
onDidClose: closeEmitter.event,
onDidEnd: endEmitter.event,
dataHandler: d => remoteSocket.write(d),
endHandler: () => remoteSocket.end(),
};
}, connectionToken));
}
return new Promise<vscode.ResolvedAuthority>((res, _rej) => {
const proxyServer = net.createServer(proxySocket => {
outputChannel.appendLine(`Proxy connection accepted`);
@ -228,28 +279,7 @@ export function activate(context: vscode.ExtensionContext) {
proxyServer.listen(0, '127.0.0.1', () => {
const port = (<net.AddressInfo>proxyServer.address()).port;
outputChannel.appendLine(`Going through proxy at port ${port}`);
const r: vscode.ResolverResult = new vscode.ResolvedAuthority('127.0.0.1', port, connectionToken);
r.tunnelFeatures = {
elevation: true,
privacyOptions: vscode.workspace.getConfiguration('testresolver').get('supportPublicPorts') ? [
{
id: 'public',
label: 'Public',
themeIcon: 'eye'
},
{
id: 'other',
label: 'Other',
themeIcon: 'circuit-board'
},
{
id: 'private',
label: 'Private',
themeIcon: 'eye-closed'
}
] : []
};
res(r);
res(new vscode.ResolvedAuthority('127.0.0.1', port, connectionToken));
});
context.subscriptions.push({
dispose: () => {
@ -264,12 +294,16 @@ export function activate(context: vscode.ExtensionContext) {
async getCanonicalURI(uri: vscode.Uri): Promise<vscode.Uri> {
return vscode.Uri.file(uri.path);
},
resolve(_authority: string): Thenable<vscode.ResolvedAuthority> {
resolve(_authority: string): Thenable<vscode.ResolverResult> {
return vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'Open TestResolver Remote ([details](command:vscode-testresolver.showLog))',
cancellable: false
}, (progress) => doResolve(_authority, progress));
}, async (progress) => {
const rr = await doResolve(_authority, progress);
rr.tunnelFeatures = getTunnelFeatures();
return rr;
});
},
tunnelFactory,
showCandidatePort
@ -282,6 +316,9 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.currentWindow', () => {
return vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: 'test+test', reuseWindow: true });
}));
context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.currentWindowManaged', () => {
return vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: 'test+managed', reuseWindow: true });
}));
context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.newWindowWithError', () => {
return vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: 'test+error' });
}));

View file

@ -1406,6 +1406,11 @@ export class IntervalCounter {
export type ValueCallback<T = unknown> = (value: T | Promise<T>) => void;
const enum DeferredOutcome {
Resolved,
Rejected
}
/**
* Creates a promise whose resolution or rejection can be controlled imperatively.
*/
@ -1413,19 +1418,22 @@ export class DeferredPromise<T> {
private completeCallback!: ValueCallback<T>;
private errorCallback!: (err: unknown) => void;
private rejected = false;
private resolved = false;
private outcome?: { outcome: DeferredOutcome.Rejected; value: any } | { outcome: DeferredOutcome.Resolved; value: T };
public get isRejected() {
return this.rejected;
return this.outcome?.outcome === DeferredOutcome.Rejected;
}
public get isResolved() {
return this.resolved;
return this.outcome?.outcome === DeferredOutcome.Resolved;
}
public get isSettled() {
return this.rejected || this.resolved;
return !!this.outcome;
}
public get value() {
return this.outcome?.outcome === DeferredOutcome.Resolved ? this.outcome?.value : undefined;
}
public readonly p: Promise<T>;
@ -1440,7 +1448,7 @@ export class DeferredPromise<T> {
public complete(value: T) {
return new Promise<void>(resolve => {
this.completeCallback(value);
this.resolved = true;
this.outcome = { outcome: DeferredOutcome.Resolved, value };
resolve();
});
}
@ -1448,17 +1456,13 @@ export class DeferredPromise<T> {
public error(err: unknown) {
return new Promise<void>(resolve => {
this.errorCallback(err);
this.rejected = true;
this.outcome = { outcome: DeferredOutcome.Rejected, value: err };
resolve();
});
}
public cancel() {
new Promise<void>(resolve => {
this.errorCallback(new CancellationError());
this.rejected = true;
resolve();
});
return this.error(new CancellationError());
}
}

View file

@ -3,11 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Lazy } from 'vs/base/common/lazy';
import * as streams from 'vs/base/common/stream';
declare const Buffer: any;
const hasBuffer = (typeof Buffer !== 'undefined');
const indexOfTable = new Lazy(() => new Uint8Array(256));
let textEncoder: TextEncoder | null;
let textDecoder: TextDecoder | null;
@ -169,6 +171,52 @@ export class VSBuffer {
writeUInt8(value: number, offset: number): void {
writeUInt8(this.buffer, value, offset);
}
indexOf(subarray: VSBuffer | Uint8Array) {
const needle = subarray instanceof VSBuffer ? subarray.buffer : subarray;
const needleLen = needle.byteLength;
const haystack = this.buffer;
const haystackLen = haystack.byteLength;
if (needleLen === 0) {
return 0;
}
if (needleLen === 1) {
return haystack.indexOf(needle[0]);
}
if (needleLen > haystackLen) {
return -1;
}
// find index of the subarray using boyer-moore-horspool algorithm
const table = indexOfTable.value;
table.fill(needle.length);
for (let i = 0; i < needle.length; i++) {
table[needle[i]] = needle.length - i - 1;
}
let i = needle.length - 1;
let j = i;
let result = -1;
while (i < haystackLen) {
if (haystack[i] === needle[j]) {
if (j === 0) {
result = i;
break;
}
i--;
j--;
} else {
i += Math.max(needle.length - j, table[haystack[i]]);
j = needle.length - 1;
}
}
return result;
}
}
export function readUInt16LE(source: Uint8Array, offset: number): number {

View file

@ -413,6 +413,22 @@ suite('Buffer', () => {
}
});
test('indexOf', () => {
const haystack = VSBuffer.fromString('abcaabbccaaabbbccc');
assert.strictEqual(haystack.indexOf(VSBuffer.fromString('')), 0);
assert.strictEqual(haystack.indexOf(VSBuffer.fromString('a'.repeat(100))), -1);
assert.strictEqual(haystack.indexOf(VSBuffer.fromString('a')), 0);
assert.strictEqual(haystack.indexOf(VSBuffer.fromString('c')), 2);
assert.strictEqual(haystack.indexOf(VSBuffer.fromString('abcaa')), 0);
assert.strictEqual(haystack.indexOf(VSBuffer.fromString('caaab')), 8);
assert.strictEqual(haystack.indexOf(VSBuffer.fromString('ccc')), 15);
assert.strictEqual(haystack.indexOf(VSBuffer.fromString('cccb')), -1);
});
suite('base64', () => {
/*
Generated with:

View file

@ -10,7 +10,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ISocket, SocketCloseEvent, SocketCloseEventType, SocketDiagnostics, SocketDiagnosticsEventType } from 'vs/base/parts/ipc/common/ipc.net';
import { IConnectCallback, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
import { RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode, WebSocketMessagingPassing } from 'vs/platform/remote/common/remoteAuthorityResolver';
export interface IWebSocketFactory {
create(url: string, debugLabel: string): IWebSocket;
@ -265,14 +265,14 @@ class BrowserSocket implements ISocket {
}
export class BrowserSocketFactory implements ISocketFactory {
export class BrowserSocketFactory implements ISocketFactory<WebSocketMessagingPassing> {
private readonly _webSocketFactory: IWebSocketFactory;
constructor(webSocketFactory: IWebSocketFactory | null | undefined) {
this._webSocketFactory = webSocketFactory || defaultWebSocketFactory;
}
connect(host: string, port: number, path: string, query: string, debugLabel: string, callback: IConnectCallback): void {
connect({ host, port }: WebSocketMessagingPassing, path: string, query: string, debugLabel: string, callback: IConnectCallback): void {
const webSocketSchema = (/^https:/.test(window.location.href) ? 'wss' : 'ws');
const socket = this._webSocketFactory.create(`${webSocketSchema}://${(/:/.test(host) && !/\[/.test(host)) ? `[${host}]` : host}:${port}${path}?${query}&skipWebSocketFrames=false`, debugLabel);
const errorListener = socket.onError((err) => callback(err, undefined));

View file

@ -3,6 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DeferredPromise } from 'vs/base/common/async';
import * as errors from 'vs/base/common/errors';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { RemoteAuthorities } from 'vs/base/common/network';
@ -11,7 +13,7 @@ import { StopWatch } from 'vs/base/common/stopwatch';
import { URI } from 'vs/base/common/uri';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { IRemoteAuthorityResolverService, IRemoteConnectionData, ResolvedAuthority, ResolverResult, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRemoteAuthorityResolverService, IRemoteConnectionData, MessagePassingType, ResolvedAuthority, ResolvedOptions, ResolverResult, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { getRemoteServerRootPath, parseAuthorityWithOptionalPort } from 'vs/platform/remote/common/remoteHosts';
export class RemoteAuthorityResolverService extends Disposable implements IRemoteAuthorityResolverService {
@ -21,12 +23,14 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
private readonly _onDidChangeConnectionData = this._register(new Emitter<void>());
public readonly onDidChangeConnectionData = this._onDidChangeConnectionData.event;
private readonly _promiseCache = new Map<string, Promise<ResolverResult>>();
private readonly _resolveAuthorityRequests = new Map<string, DeferredPromise<ResolverResult>>();
private readonly _cache = new Map<string, ResolverResult>();
private readonly _connectionToken: Promise<string> | string | undefined;
private readonly _connectionTokens: Map<string, string>;
private readonly _isWorkbenchOptionsBasedResolution: boolean;
constructor(
isWorkbenchOptionsBasedResolution: boolean,
connectionToken: Promise<string> | string | undefined,
resourceUriProvider: ((uri: URI) => URI) | undefined,
@IProductService productService: IProductService,
@ -35,6 +39,7 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
super();
this._connectionToken = connectionToken;
this._connectionTokens = new Map<string, string>();
this._isWorkbenchOptionsBasedResolution = isWorkbenchOptionsBasedResolution;
if (resourceUriProvider) {
RemoteAuthorities.setDelegate(resourceUriProvider);
}
@ -42,15 +47,20 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
}
async resolveAuthority(authority: string): Promise<ResolverResult> {
let result = this._promiseCache.get(authority);
let result = this._resolveAuthorityRequests.get(authority);
if (!result) {
result = this._doResolveAuthority(authority);
this._promiseCache.set(authority, result);
result = new DeferredPromise<ResolverResult>();
this._resolveAuthorityRequests.set(authority, result);
if (this._isWorkbenchOptionsBasedResolution) {
this._doResolveAuthority(authority).then(v => result!.complete(v), (err) => result!.error(err));
}
}
return result;
return result.p;
}
async getCanonicalURI(uri: URI): Promise<URI> {
// todo@connor4312 make this work for web
return uri;
}
@ -61,8 +71,7 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
const resolverResult = this._cache.get(authority)!;
const connectionToken = this._connectionTokens.get(authority) || resolverResult.authority.connectionToken;
return {
host: resolverResult.authority.host,
port: resolverResult.authority.port,
connectTo: resolverResult.authority.messaging,
connectionToken: connectionToken
};
}
@ -77,20 +86,42 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
this._logService.info(`Resolved connection token (${authorityPrefix}) after ${sw.elapsed()} ms`);
const defaultPort = (/^https:/.test(window.location.href) ? 443 : 80);
const { host, port } = parseAuthorityWithOptionalPort(authority, defaultPort);
const result: ResolverResult = { authority: { authority, host: host, port: port, connectionToken } };
RemoteAuthorities.set(authority, result.authority.host, result.authority.port);
const result: ResolverResult = { authority: { authority, messaging: { type: MessagePassingType.WebSocket, host: host, port: port }, connectionToken } };
RemoteAuthorities.set(authority, host, port);
this._cache.set(authority, result);
this._onDidChangeConnectionData.fire();
return result;
}
_clearResolvedAuthority(authority: string): void {
if (this._resolveAuthorityRequests.has(authority)) {
this._resolveAuthorityRequests.get(authority)!.cancel();
this._resolveAuthorityRequests.delete(authority);
}
}
_setResolvedAuthority(resolvedAuthority: ResolvedAuthority) {
_setResolvedAuthority(resolvedAuthority: ResolvedAuthority, options?: ResolvedOptions): void {
if (this._resolveAuthorityRequests.has(resolvedAuthority.authority)) {
const request = this._resolveAuthorityRequests.get(resolvedAuthority.authority)!;
if (resolvedAuthority.messaging.type === MessagePassingType.WebSocket) {
// todo@connor4312 need to implement some kind of loopback for ext host based messaging
RemoteAuthorities.set(resolvedAuthority.authority, resolvedAuthority.messaging.host, resolvedAuthority.messaging.port);
}
if (resolvedAuthority.connectionToken) {
RemoteAuthorities.setConnectionToken(resolvedAuthority.authority, resolvedAuthority.connectionToken);
}
request.complete({ authority: resolvedAuthority, options });
this._onDidChangeConnectionData.fire();
}
}
_setResolvedAuthorityError(authority: string, err: any): void {
if (this._resolveAuthorityRequests.has(authority)) {
const request = this._resolveAuthorityRequests.get(authority)!;
// Avoid that this error makes it to telemetry
request.error(errors.ErrorNoTelemetry.fromError(err));
}
}
_setAuthorityConnectionToken(authority: string, connectionToken: string): void {

View file

@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { VSBuffer, encodeBase64 } from 'vs/base/common/buffer';
export const makeRawSocketHeaders = (path: string, query: string, deubgLabel: string) => {
// https://tools.ietf.org/html/rfc6455#section-4
const buffer = new Uint8Array(16);
for (let i = 0; i < 16; i++) {
buffer[i] = Math.round(Math.random() * 256);
}
const nonce = encodeBase64(VSBuffer.wrap(buffer));
const headers = [
`GET ws://localhost${path}?${query}&skipWebSocketFrames=true HTTP/1.1`,
`Connection: Upgrade`,
`Upgrade: websocket`,
`Sec-WebSocket-Key: ${nonce}`
];
return headers.join('\r\n') + '\r\n\r\n';
};
export const socketRawEndHeaderSequence = VSBuffer.fromString('\r\n\r\n');

View file

@ -16,7 +16,7 @@ import { IIPCLogger } from 'vs/base/parts/ipc/common/ipc';
import { Client, ConnectionHealth, ISocket, PersistentProtocol, ProtocolConstants, SocketCloseEventType } from 'vs/base/parts/ipc/common/ipc.net';
import { ILogService } from 'vs/platform/log/common/log';
import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { RemoteAuthorityResolverError, ResolvedAuthorityMessagePassing } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts';
import { ISignService } from 'vs/platform/sign/common/sign';
@ -71,15 +71,14 @@ export interface OKMessage {
export type HandshakeMessage = AuthRequest | SignRequest | ConnectionTypeRequest | ErrorMessage | OKMessage;
interface ISimpleConnectionOptions {
interface ISimpleConnectionOptions<T = unknown> {
commit: string | undefined;
quality: string | undefined;
host: string;
port: number;
connectTo: T;
connectionToken: string | undefined;
reconnectionToken: string;
reconnectionProtocol: PersistentProtocol | null;
socketFactory: ISocketFactory;
socketFactory: ISocketFactory<T>;
signService: ISignService;
logService: ILogService;
}
@ -88,8 +87,8 @@ export interface IConnectCallback {
(err: any | undefined, socket: ISocket | undefined): void;
}
export interface ISocketFactory {
connect(host: string, port: number, path: string, query: string, debugLabel: string, callback: IConnectCallback): void;
export interface ISocketFactory<T> {
connect(connectTo: T, path: string, query: string, debugLabel: string, callback: IConnectCallback): void;
}
function createTimeoutCancellation(millis: number): CancellationToken {
@ -192,12 +191,12 @@ function readOneControlMessage<T>(protocol: PersistentProtocol, timeoutCancellat
return result.promise;
}
function createSocket(logService: ILogService, socketFactory: ISocketFactory, host: string, port: number, path: string, query: string, debugConnectionType: string, debugLabel: string, timeoutCancellationToken: CancellationToken): Promise<ISocket> {
function createSocket<T>(logService: ILogService, socketFactory: ISocketFactory<T>, connectTo: T, path: string, query: string, debugConnectionType: string, debugLabel: string, timeoutCancellationToken: CancellationToken): Promise<ISocket> {
const result = new PromiseWithTimeout<ISocket>(timeoutCancellationToken);
const sw = StopWatch.create(false);
logService.info(`Creating a socket (${debugLabel})...`);
performance.mark(`code/willCreateSocket/${debugConnectionType}`);
socketFactory.connect(host, port, path, query, debugLabel, (err: any, socket: ISocket | undefined) => {
socketFactory.connect(connectTo, path, query, debugLabel, (err: any, socket: ISocket | undefined) => {
if (result.didTimeout) {
performance.mark(`code/didCreateSocketError/${debugConnectionType}`);
logService.info(`Creating a socket (${debugLabel}) finished after ${sw.elapsed()} ms, but this is too late and has timed out already.`);
@ -237,14 +236,14 @@ function raceWithTimeoutCancellation<T>(promise: Promise<T>, timeoutCancellation
return result.promise;
}
async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions, connectionType: ConnectionType, args: any | undefined, timeoutCancellationToken: CancellationToken): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean }> {
async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptions<unknown>, connectionType: ConnectionType, args: any | undefined, timeoutCancellationToken: CancellationToken): Promise<{ protocol: PersistentProtocol; ownsProtocol: boolean }> {
const logPrefix = connectLogPrefix(options, connectionType);
options.logService.trace(`${logPrefix} 1/6. invoking socketFactory.connect().`);
let socket: ISocket;
try {
socket = await createSocket(options.logService, options.socketFactory, options.host, options.port, getRemoteServerRootPath(options), `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, connectionTypeToString(connectionType), `renderer-${connectionTypeToString(connectionType)}-${options.reconnectionToken}`, timeoutCancellationToken);
socket = await createSocket(options.logService, options.socketFactory, options.connectTo, getRemoteServerRootPath(options), `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, connectionTypeToString(connectionType), `renderer-${connectionTypeToString(connectionType)}-${options.reconnectionToken}`, timeoutCancellationToken);
} catch (error) {
options.logService.error(`${logPrefix} socketFactory.connect() failed or timed out. Error:`);
options.logService.error(error);
@ -389,23 +388,22 @@ async function doConnectRemoteAgentTunnel(options: ISimpleConnectionOptions, sta
return protocol;
}
export interface IConnectionOptions {
export interface IConnectionOptions<T = unknown> {
commit: string | undefined;
quality: string | undefined;
socketFactory: ISocketFactory;
addressProvider: IAddressProvider;
socketFactory: ISocketFactory<T>;
addressProvider: IAddressProvider<T>;
signService: ISignService;
logService: ILogService;
ipcLogger: IIPCLogger | null;
}
async function resolveConnectionOptions(options: IConnectionOptions, reconnectionToken: string, reconnectionProtocol: PersistentProtocol | null): Promise<ISimpleConnectionOptions> {
const { host, port, connectionToken } = await options.addressProvider.getAddress();
async function resolveConnectionOptions<T>(options: IConnectionOptions<T>, reconnectionToken: string, reconnectionProtocol: PersistentProtocol | null): Promise<ISimpleConnectionOptions<T>> {
const { connectTo, connectionToken } = await options.addressProvider.getAddress();
return {
commit: options.commit,
quality: options.quality,
host: host,
port: port,
connectTo,
connectionToken: connectionToken,
reconnectionToken: reconnectionToken,
reconnectionProtocol: reconnectionProtocol,
@ -415,14 +413,13 @@ async function resolveConnectionOptions(options: IConnectionOptions, reconnectio
};
}
export interface IAddress {
host: string;
port: number;
export interface IAddress<T = ResolvedAuthorityMessagePassing> {
connectTo: T;
connectionToken: string | undefined;
}
export interface IAddressProvider {
getAddress(): Promise<IAddress>;
export interface IAddressProvider<T = ResolvedAuthorityMessagePassing> {
getAddress(): Promise<IAddress<T>>;
}
export async function connectRemoteAgentManagement(options: IConnectionOptions, remoteAuthority: string, clientId: string): Promise<ManagementPersistentConnection> {
@ -448,7 +445,7 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption
/**
* Will attempt to connect 5 times. If it fails 5 consecutive times, it will give up.
*/
async function createInitialConnection<T extends PersistentConnection>(options: IConnectionOptions, connectionFactory: (simpleOptions: ISimpleConnectionOptions) => Promise<T>): Promise<T> {
async function createInitialConnection<T extends PersistentConnection, O>(options: IConnectionOptions<O>, connectionFactory: (simpleOptions: ISimpleConnectionOptions<O>) => Promise<T>): Promise<T> {
const MAX_ATTEMPTS = 5;
for (let attempt = 1; ; attempt++) {
@ -691,7 +688,7 @@ export abstract class PersistentConnection extends Disposable {
this._onDidStateChange.fire(new ReconnectionRunningEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1));
this._options.logService.info(`${logPrefix} resolving connection...`);
const simpleOptions = await resolveConnectionOptions(this._options, this.reconnectionToken, this.protocol);
this._options.logService.info(`${logPrefix} connecting to ${simpleOptions.host}:${simpleOptions.port}...`);
this._options.logService.info(`${logPrefix} connecting to ${simpleOptions.connectTo}...`);
await this._reconnect(simpleOptions, createTimeoutCancellation(RECONNECT_TIMEOUT));
this._options.logService.info(`${logPrefix} reconnected!`);
this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, this.protocol.getMillisSinceLastIncomingData(), attempt + 1));
@ -832,7 +829,7 @@ function commonLogPrefix(connectionType: ConnectionType, reconnectionToken: stri
}
function connectLogPrefix(options: ISimpleConnectionOptions, connectionType: ConnectionType): string {
return `${commonLogPrefix(connectionType, options.reconnectionToken, !!options.reconnectionProtocol)}[${options.host}:${options.port}]`;
return `${commonLogPrefix(connectionType, options.reconnectionToken, !!options.reconnectionProtocol)}[${options.connectTo}]`;
}
function logElapsed(startTime: number): string {

View file

@ -10,10 +10,29 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
export const IRemoteAuthorityResolverService = createDecorator<IRemoteAuthorityResolverService>('remoteAuthorityResolverService');
export const enum MessagePassingType {
WebSocket,
Managed
}
export interface ManagedMessagingPassing {
type: MessagePassingType.Managed;
id: number;
}
export interface WebSocketMessagingPassing {
type: MessagePassingType.WebSocket;
host: string;
port: number;
}
export type ResolvedAuthorityMessagePassing = WebSocketMessagingPassing | ManagedMessagingPassing;
export type MessagePassingOfType<T extends MessagePassingType> = ResolvedAuthorityMessagePassing & { type: T };
export interface ResolvedAuthority {
readonly authority: string;
readonly host: string;
readonly port: number;
readonly messaging: ResolvedAuthorityMessagePassing;
readonly connectionToken: string | undefined;
}
@ -50,8 +69,7 @@ export interface ResolverResult {
}
export interface IRemoteConnectionData {
host: string;
port: number;
connectTo: ResolvedAuthorityMessagePassing;
connectionToken: string | undefined;
}

View file

@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { mapFind } from 'vs/base/common/arrays';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
import { MessagePassingOfType, MessagePassingType, ResolvedAuthorityMessagePassing } from 'vs/platform/remote/common/remoteAuthorityResolver';
export const IRemoteSocketFactoryCollection = createDecorator<IRemoteSocketFactoryCollection>('remoteSocketFactoryCollection');
export interface IRemoteSocketFactoryCollection {
readonly _serviceBrand: undefined;
/**
* Register a socket factory for the given message passing type
* @param type passing type to register for
* @param factory function that returns the socket factory, or undefined if
* it can't handle the data.
*/
register<T extends MessagePassingType>(
type: T,
factory: (messagePassing: MessagePassingOfType<T>) => ISocketFactory<MessagePassingOfType<T>> | undefined
): void;
/**
* Gets a socket factory for the given message passing data.
*/
create<T extends ResolvedAuthorityMessagePassing>(messagePassing: T): ISocketFactory<T> | undefined;
}
export class RemoteSocketFactoryCollection implements IRemoteSocketFactoryCollection {
declare readonly _serviceBrand: undefined;
private readonly factories: { [T in MessagePassingType]?: ((messagePassing: MessagePassingOfType<T>) => ISocketFactory<MessagePassingOfType<T>> | undefined)[] } = {};
public register<T extends MessagePassingType>(
type: T,
factory: (messagePassing: MessagePassingOfType<T>) => ISocketFactory<MessagePassingOfType<T>> | undefined
): void {
this.factories[type] ??= [];
this.factories[type]!.push(factory);
}
public create<T extends ResolvedAuthorityMessagePassing>(messagePassing: T): ISocketFactory<T> | undefined {
return mapFind(
(this.factories[messagePassing.type] || []) as ((messagePassing: T) => ISocketFactory<T> | undefined)[],
factory => factory(messagePassing),
);
}
}

View file

@ -3,41 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//
import { DeferredPromise } from 'vs/base/common/async';
import * as errors from 'vs/base/common/errors';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { RemoteAuthorities } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { IProductService } from 'vs/platform/product/common/productService';
import { IRemoteAuthorityResolverService, IRemoteConnectionData, ResolvedAuthority, ResolvedOptions, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRemoteAuthorityResolverService, IRemoteConnectionData, MessagePassingType, ResolvedAuthority, ResolvedOptions, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts';
class PendingPromise<I, R> {
public readonly promise: Promise<R>;
public readonly input: I;
public result: R | null;
private _resolve!: (value: R) => void;
private _reject!: (err: any) => void;
constructor(request: I) {
this.input = request;
this.promise = new Promise<R>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
this.result = null;
}
resolve(result: R): void {
this.result = result;
this._resolve(this.result);
}
reject(err: any): void {
this._reject(err);
}
}
export class RemoteAuthorityResolverService extends Disposable implements IRemoteAuthorityResolverService {
declare readonly _serviceBrand: undefined;
@ -45,16 +20,16 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
private readonly _onDidChangeConnectionData = this._register(new Emitter<void>());
public readonly onDidChangeConnectionData = this._onDidChangeConnectionData.event;
private readonly _resolveAuthorityRequests: Map<string, PendingPromise<string, ResolverResult>>;
private readonly _resolveAuthorityRequests: Map<string, DeferredPromise<ResolverResult>>;
private readonly _connectionTokens: Map<string, string>;
private readonly _canonicalURIRequests: Map<string, PendingPromise<URI, URI>>;
private readonly _canonicalURIRequests: Map<string, DeferredPromise<URI>>;
private _canonicalURIProvider: ((uri: URI) => Promise<URI>) | null;
constructor(@IProductService productService: IProductService) {
super();
this._resolveAuthorityRequests = new Map<string, PendingPromise<string, ResolverResult>>();
this._resolveAuthorityRequests = new Map<string, DeferredPromise<ResolverResult>>();
this._connectionTokens = new Map<string, string>();
this._canonicalURIRequests = new Map<string, PendingPromise<URI, URI>>();
this._canonicalURIRequests = new Map<string, DeferredPromise<URI>>();
this._canonicalURIProvider = null;
RemoteAuthorities.setServerRootPath(getRemoteServerRootPath(productService));
@ -62,19 +37,19 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
resolveAuthority(authority: string): Promise<ResolverResult> {
if (!this._resolveAuthorityRequests.has(authority)) {
this._resolveAuthorityRequests.set(authority, new PendingPromise<string, ResolverResult>(authority));
this._resolveAuthorityRequests.set(authority, new DeferredPromise());
}
return this._resolveAuthorityRequests.get(authority)!.promise;
return this._resolveAuthorityRequests.get(authority)!.p;
}
async getCanonicalURI(uri: URI): Promise<URI> {
const key = uri.toString();
if (!this._canonicalURIRequests.has(key)) {
const request = new PendingPromise<URI, URI>(uri);
this._canonicalURIProvider?.(request.input).then((uri) => request.resolve(uri), (err) => request.reject(err));
const request = new DeferredPromise<URI>();
this._canonicalURIProvider?.(uri).then((uri) => request.complete(uri), (err) => request.error(err));
this._canonicalURIRequests.set(key, request);
}
return this._canonicalURIRequests.get(key)!.promise;
return this._canonicalURIRequests.get(key)!.p;
}
getConnectionData(authority: string): IRemoteConnectionData | null {
@ -82,20 +57,19 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
return null;
}
const request = this._resolveAuthorityRequests.get(authority)!;
if (!request.result) {
if (!request.isResolved) {
return null;
}
const connectionToken = this._connectionTokens.get(authority);
return {
host: request.result.authority.host,
port: request.result.authority.port,
connectTo: request.value!.authority.messaging,
connectionToken: connectionToken
};
}
_clearResolvedAuthority(authority: string): void {
if (this._resolveAuthorityRequests.has(authority)) {
this._resolveAuthorityRequests.get(authority)!.reject(errors.canceled());
this._resolveAuthorityRequests.get(authority)!.cancel();
this._resolveAuthorityRequests.delete(authority);
}
}
@ -103,11 +77,14 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
_setResolvedAuthority(resolvedAuthority: ResolvedAuthority, options?: ResolvedOptions): void {
if (this._resolveAuthorityRequests.has(resolvedAuthority.authority)) {
const request = this._resolveAuthorityRequests.get(resolvedAuthority.authority)!;
RemoteAuthorities.set(resolvedAuthority.authority, resolvedAuthority.host, resolvedAuthority.port);
if (resolvedAuthority.messaging.type === MessagePassingType.WebSocket) {
// todo@connor4312 need to implement some kind of loopback for ext host based messaging
RemoteAuthorities.set(resolvedAuthority.authority, resolvedAuthority.messaging.host, resolvedAuthority.messaging.port);
}
if (resolvedAuthority.connectionToken) {
RemoteAuthorities.setConnectionToken(resolvedAuthority.authority, resolvedAuthority.connectionToken);
}
request.resolve({ authority: resolvedAuthority, options });
request.complete({ authority: resolvedAuthority, options });
this._onDidChangeConnectionData.fire();
}
}
@ -116,7 +93,7 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
if (this._resolveAuthorityRequests.has(authority)) {
const request = this._resolveAuthorityRequests.get(authority)!;
// Avoid that this error makes it to telemetry
request.reject(errors.ErrorNoTelemetry.fromError(err));
request.error(errors.ErrorNoTelemetry.fromError(err));
}
}
@ -128,8 +105,8 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot
_setCanonicalURIProvider(provider: (uri: URI) => Promise<URI>): void {
this._canonicalURIProvider = provider;
this._canonicalURIRequests.forEach((value) => {
this._canonicalURIProvider!(value.input).then((uri) => value.resolve(uri), (err) => value.reject(err));
this._canonicalURIRequests.forEach((value, key) => {
this._canonicalURIProvider!(URI.parse(key)).then((uri) => value.complete(uri), (err) => value.error(err));
});
}
}

View file

@ -5,29 +5,18 @@
import * as net from 'net';
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { makeRawSocketHeaders } from 'vs/platform/remote/common/managedSocket';
import { IConnectCallback, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
import { WebSocketMessagingPassing } from 'vs/platform/remote/common/remoteAuthorityResolver';
export const nodeSocketFactory = new class implements ISocketFactory {
connect(host: string, port: number, path: string, query: string, debugLabel: string, callback: IConnectCallback): void {
export const nodeSocketFactory = new class implements ISocketFactory<WebSocketMessagingPassing> {
connect({ host, port }: WebSocketMessagingPassing, path: string, query: string, debugLabel: string, callback: IConnectCallback): void {
const errorListener = (err: any) => callback(err, undefined);
const socket = net.createConnection({ host: host, port: port }, () => {
socket.removeListener('error', errorListener);
// https://tools.ietf.org/html/rfc6455#section-4
const buffer = Buffer.alloc(16);
for (let i = 0; i < 16; i++) {
buffer[i] = Math.round(Math.random() * 256);
}
const nonce = buffer.toString('base64');
const headers = [
`GET ws://${/:/.test(host) ? `[${host}]` : host}:${port}${path}?${query}&skipWebSocketFrames=true HTTP/1.1`,
`Connection: Upgrade`,
`Upgrade: websocket`,
`Sec-WebSocket-Key: ${nonce}`
];
socket.write(headers.join('\r\n') + '\r\n\r\n');
socket.write(makeRawSocketHeaders(path, query, debugLabel));
const onData = (data: Buffer) => {
const strData = data.toString();

View file

@ -110,7 +110,7 @@ export interface ITunnel {
export interface ISharedTunnelsService {
readonly _serviceBrand: undefined;
openTunnel(authority: string, addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localHost: string, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise<RemoteTunnel | undefined> | undefined;
openTunnel(authority: string, addressProvider: IAddressProvider<unknown> | undefined, remoteHost: string | undefined, remotePort: number, localHost: string, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise<RemoteTunnel | undefined> | undefined;
}
export interface ITunnelService {
@ -126,7 +126,7 @@ export interface ITunnelService {
readonly onAddedTunnelProvider: Event<void>;
canTunnel(uri: URI): boolean;
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localHost?: string, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise<RemoteTunnel | undefined> | undefined;
openTunnel(addressProvider: IAddressProvider<unknown> | undefined, remoteHost: string | undefined, remotePort: number, localHost?: string, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise<RemoteTunnel | undefined> | undefined;
getExistingTunnel(remoteHost: string, remotePort: number): Promise<RemoteTunnel | undefined>;
setEnvironmentTunnel(remoteHost: string, remotePort: number, localAddress: string, privacy: string, protocol: string): void;
closeTunnel(remoteHost: string, remotePort: number): Promise<void>;

View file

@ -7,17 +7,17 @@ import * as net from 'net';
import * as os from 'os';
import { BROWSER_RESTRICTED_PORTS, findFreePortFaster } from 'vs/base/node/ports';
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory';
import { Barrier } from 'vs/base/common/async';
import { Disposable } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { connectRemoteAgentTunnel, IAddressProvider, IConnectionOptions, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
import { connectRemoteAgentTunnel, IAddressProvider, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection';
import { AbstractTunnelService, isAllInterfaces, ISharedTunnelsService as ISharedTunnelsService, isLocalhost, isPortPrivileged, ITunnelService, RemoteTunnel, TunnelPrivacyId } from 'vs/platform/tunnel/common/tunnel';
import { ISignService } from 'vs/platform/sign/common/sign';
import { OS } from 'vs/base/common/platform';
import { IRemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection';
async function createRemoteTunnel(options: IConnectionOptions, defaultTunnelHost: string, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise<RemoteTunnel> {
let readyTunnel: NodeRemoteTunnel | undefined;
@ -155,7 +155,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
export class BaseTunnelService extends AbstractTunnelService {
public constructor(
private readonly socketFactory: ISocketFactory,
@IRemoteSocketFactoryCollection private readonly socketFactories: IRemoteSocketFactoryCollection,
@ILogService logService: ILogService,
@ISignService private readonly signService: ISignService,
@IProductService private readonly productService: IProductService,
@ -179,32 +179,40 @@ export class BaseTunnelService extends AbstractTunnelService {
return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol);
} else {
this.logService.trace(`ForwardedPorts: (TunnelService) Creating tunnel without provider ${remoteHost}:${remotePort} on local port ${localPort}.`);
const options: IConnectionOptions = {
commit: this.productService.commit,
quality: this.productService.quality,
socketFactory: this.socketFactory,
addressProvider,
signService: this.signService,
logService: this.logService,
ipcLogger: null
};
return addressProvider.getAddress().then(address => {
const socketFactory = this.socketFactories.create(address.connectTo);
if (!socketFactory) {
throw new Error(`No socket factory found for ${address.connectTo}`);
}
const tunnel = createRemoteTunnel(options, localHost, remoteHost, remotePort, localPort);
this.logService.trace('ForwardedPorts: (TunnelService) Tunnel created without provider.');
this.addTunnelToMap(remoteHost, remotePort, tunnel);
return tunnel;
const options: IConnectionOptions = {
commit: this.productService.commit,
quality: this.productService.quality,
socketFactory,
addressProvider,
signService: this.signService,
logService: this.logService,
ipcLogger: null
};
const tunnel = createRemoteTunnel(options, localHost, remoteHost, remotePort, localPort);
this.logService.trace('ForwardedPorts: (TunnelService) Tunnel created without provider.');
this.addTunnelToMap(remoteHost, remotePort, tunnel);
return tunnel;
});
}
}
}
export class TunnelService extends BaseTunnelService {
public constructor(
@IRemoteSocketFactoryCollection socketFactories: IRemoteSocketFactoryCollection,
@ILogService logService: ILogService,
@ISignService signService: ISignService,
@IProductService productService: IProductService,
@IConfigurationService configurationService: IConfigurationService
) {
super(nodeSocketFactory, logService, signService, productService, configurationService);
super(socketFactories, logService, signService, productService, configurationService);
}
}
@ -213,6 +221,7 @@ export class SharedTunnelsService extends Disposable implements ISharedTunnelsSe
private readonly _tunnelServices: Map<string, ITunnelService> = new Map();
public constructor(
@IRemoteSocketFactoryCollection protected readonly socketFactories: IRemoteSocketFactoryCollection,
@ILogService protected readonly logService: ILogService,
@IProductService private readonly productService: IProductService,
@ISignService private readonly signService: ISignService,
@ -224,7 +233,7 @@ export class SharedTunnelsService extends Disposable implements ISharedTunnelsSe
async openTunnel(authority: string, addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localHost: string, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise<RemoteTunnel | undefined> {
this.logService.trace(`ForwardedPorts: (SharedTunnelService) openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`);
if (!this._tunnelServices.has(authority)) {
const tunnelService = new TunnelService(this.logService, this.signService, this.productService, this.configurationService);
const tunnelService = new TunnelService(this.socketFactories, this.logService, this.signService, this.productService, this.configurationService);
this._register(tunnelService);
this._tunnelServices.set(authority, tunnelService);
tunnelService.onTunnelClosed(async () => {

View file

@ -16,7 +16,7 @@ import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensio
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRemoteConnectionData, MessagePassingType } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { ExtHostContext, ExtHostExtensionServiceShape, MainContext, MainThreadExtensionServiceShape } from 'vs/workbench/api/common/extHost.protocol';
import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@ -25,7 +25,7 @@ import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/exten
import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
import { IExtensionHostProxy, IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy';
import { ActivationKind, ExtensionActivationReason, IExtensionService, IInternalExtensionService, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions';
import { extHostNamedCustomer, IExtHostContext, IInternalExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { extHostNamedCustomer, IExtHostContext, IInternalExtHostContext, IManagedSocketCallbacks } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { ITimerService } from 'vs/workbench/services/timer/browser/timerService';
@ -34,6 +34,7 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha
private readonly _extensionHostKind: ExtensionHostKind;
private readonly _internalExtensionService: IInternalExtensionService;
private readonly _managedSocketCallbacks: IManagedSocketCallbacks;
constructor(
extHostContext: IExtHostContext,
@ -50,6 +51,7 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha
const internalExtHostContext = (<IInternalExtHostContext>extHostContext);
this._internalExtensionService = internalExtHostContext.internalExtensionService;
this._managedSocketCallbacks = internalExtHostContext.managedSocketCallbacks;
internalExtHostContext._setExtensionHostProxy(
new ExtensionHostProxy(extHostContext.getProxy(ExtHostContext.ExtHostExtensionService))
);
@ -59,6 +61,18 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha
public dispose(): void {
}
$onDidRemoteSocketHaveData(id: number, data: VSBuffer): void {
this._managedSocketCallbacks.onDidRemoteSocketHaveData(id, data);
}
$onDidRemoteSocketClose(id: number, error: string | undefined): void {
this._managedSocketCallbacks.onDidRemoteSocketClose(id, error ? new Error(error) : undefined);
}
$onDidRemoteSocketEnd(id: number): void {
this._managedSocketCallbacks.onDidRemoteSocketEnd(id);
}
$getExtension(extensionId: string) {
return this._extensionService.getExtension(extensionId);
}
@ -199,8 +213,15 @@ class ExtensionHostProxy implements IExtensionHostProxy {
private readonly _actual: ExtHostExtensionServiceShape
) { }
resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise<IResolveAuthorityResult> {
return this._actual.$resolveAuthority(remoteAuthority, resolveAttempt);
async resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise<IResolveAuthorityResult> {
const resolved = await this._actual.$resolveAuthority(remoteAuthority, resolveAttempt);
if (resolved.type === 'ok') {
resolved.value.authority.toString = function () {
return this.messaging.type === MessagePassingType.Managed ? `ManagedSocket#${this.messaging.id}` : `${this.messaging.host}:${this.messaging.type}`;
};
}
return resolved;
}
async getCanonicalURI(remoteAuthority: string, uri: URI): Promise<URI | null> {
const uriComponents = await this._actual.$getCanonicalURI(remoteAuthority, uri);
@ -236,4 +257,16 @@ class ExtensionHostProxy implements IExtensionHostProxy {
test_down(size: number): Promise<VSBuffer> {
return this._actual.$test_down(size);
}
openRemoteSocket(factoryId: number): Promise<number> {
return this._actual.$openRemoteSocket(factoryId);
}
remoteSocketWrite(socketId: number, buffer: VSBuffer): void {
return this._actual.$remoteSocketWrite(socketId, buffer);
}
remoteSocketEnd(socketId: number): void {
return this._actual.$remoteSocketEnd(socketId);
}
remoteSocketDrain(socketId: number): Promise<void> {
return this._actual.$remoteSocketDrain(socketId);
}
}

View file

@ -1420,6 +1420,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
InlayHintKind: extHostTypes.InlayHintKind,
RemoteAuthorityResolverError: extHostTypes.RemoteAuthorityResolverError,
ResolvedAuthority: extHostTypes.ResolvedAuthority,
ManagedResolvedAuthority: extHostTypes.ManagedResolvedAuthority,
SourceControlInputBoxValidationType: extHostTypes.SourceControlInputBoxValidationType,
ExtensionRuntime: extHostTypes.ExtensionRuntime,
TimelineItem: extHostTypes.TimelineItem,

View file

@ -1237,6 +1237,10 @@ export interface MainThreadExtensionServiceShape extends IDisposable {
$onExtensionRuntimeError(extensionId: ExtensionIdentifier, error: SerializedError): void;
$setPerformanceMarks(marks: performance.PerformanceMark[]): Promise<void>;
$asBrowserUri(uri: UriComponents): Promise<UriComponents>;
$onDidRemoteSocketHaveData(id: number, data: VSBuffer): void;
$onDidRemoteSocketClose(id: number, error: string | undefined): void;
$onDidRemoteSocketEnd(id: number): void;
}
export interface SCMProviderFeatures {
@ -1593,6 +1597,11 @@ export interface ExtHostExtensionServiceShape {
$test_latency(n: number): Promise<number>;
$test_up(b: VSBuffer): Promise<number>;
$test_down(size: number): Promise<VSBuffer>;
$openRemoteSocket(factoryId: number): Promise<number>;
$remoteSocketWrite(socketId: number, buffer: VSBuffer): void;
$remoteSocketEnd(socketId: number): void;
$remoteSocketDrain(socketId: number): Promise<void>;
}
export interface FileSystemEvents {

View file

@ -8,7 +8,7 @@ import * as path from 'vs/base/common/path';
import * as performance from 'vs/base/common/performance';
import { originalFSPath, joinPath, extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
import { asPromise, Barrier, IntervalTimer, timeout } from 'vs/base/common/async';
import { dispose, toDisposable, Disposable } from 'vs/base/common/lifecycle';
import { dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { TernarySearchTree } from 'vs/base/common/ternarySearchTree';
import { URI, UriComponents } from 'vs/base/common/uri';
import { ILogService } from 'vs/platform/log/common/log';
@ -25,8 +25,8 @@ import type * as vscode from 'vscode';
import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { VSBuffer } from 'vs/base/common/buffer';
import { ExtensionGlobalMemento, ExtensionMemento } from 'vs/workbench/api/common/extHostMemento';
import { RemoteAuthorityResolverError, ExtensionKind, ExtensionMode, ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes';
import { ResolvedAuthority, ResolvedOptions, RemoteAuthorityResolverErrorCode, IRemoteConnectionData, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { RemoteAuthorityResolverError, ExtensionKind, ExtensionMode, ExtensionRuntime, ResolvedAuthority as ExtHostResolvedAuthority } from 'vs/workbench/api/common/extHostTypes';
import { ResolvedAuthority, ResolvedOptions, RemoteAuthorityResolverErrorCode, IRemoteConnectionData, getRemoteAuthorityPrefix, TunnelInformation, MessagePassingType } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
@ -79,6 +79,8 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
readonly _serviceBrand: undefined;
private static remoteSocketIdCounter = 0;
abstract readonly extensionRuntime: ExtensionRuntime;
private readonly _onDidChangeRemoteConnectionData = this._register(new Emitter<void>());
@ -118,6 +120,11 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
private _started: boolean;
private _isTerminating: boolean = false;
private _remoteConnectionData: IRemoteConnectionData | null;
private readonly _managedSocketFactories: Map<number, () => Thenable<vscode.ManagedMessagePassing>>;
private readonly _managedRemoteSockets: Map<number, {
object: vscode.ManagedMessagePassing;
disposer: DisposableStore;
}>;
constructor(
@IInstantiationService instaService: IInstantiationService,
@ -191,6 +198,8 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
this._resolvers = Object.create(null);
this._started = false;
this._remoteConnectionData = this._initData.remote.connectionData;
this._managedSocketFactories = new Map();
this._managedRemoteSockets = new Map();
}
public getRemoteConnectionData(): IRemoteConnectionData | null {
@ -822,30 +831,44 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
const result = await resolver.resolve(remoteAuthority, { resolveAttempt });
performance.mark(`code/extHost/didResolveAuthorityOK/${authorityPrefix}`);
intervalLogger.dispose();
logInfo(`returned ${result.host}:${result.port}`);
const tunnelInformation: TunnelInformation = {
environmentTunnels: result.environmentTunnels,
features: result.tunnelFeatures
};
// Split merged API result into separate authority/options
const authority: ResolvedAuthority = {
authority: remoteAuthority,
host: result.host,
port: result.port,
connectionToken: result.connectionToken
};
const options: ResolvedOptions = {
extensionHostEnv: result.extensionHostEnv,
isTrusted: result.isTrusted,
authenticationSession: result.authenticationSessionForInitializingExtensions ? { id: result.authenticationSessionForInitializingExtensions.id, providerId: result.authenticationSessionForInitializingExtensions.providerId } : undefined
};
logInfo(`returned ${result instanceof ExtHostResolvedAuthority ? `${result.host}:${result.port}` : 'managed authority'}`);
let authority: ResolvedAuthority;
if (result instanceof ExtHostResolvedAuthority) {
authority = {
authority: remoteAuthority,
messaging: { type: MessagePassingType.WebSocket, host: result.host, port: result.port },
connectionToken: result.connectionToken
};
} else {
const factoryId = AbstractExtHostExtensionService.remoteSocketIdCounter++;
this._managedSocketFactories.set(factoryId, result.makeConnection);
authority = {
authority: remoteAuthority,
messaging: { type: MessagePassingType.Managed, id: factoryId },
connectionToken: result.connectionToken
};
}
return {
type: 'ok',
value: {
authority,
options,
tunnelInformation: {
environmentTunnels: result.environmentTunnels,
features: result.tunnelFeatures
}
tunnelInformation,
}
};
} catch (err) {
@ -866,6 +889,47 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
}
}
public async $openRemoteSocket(factoryId: number): Promise<number> {
const factory = this._managedSocketFactories.get(factoryId);
if (!factory) {
throw new Error(`No socket factory with id ${factoryId}`);
}
const id = AbstractExtHostExtensionService.remoteSocketIdCounter++;
const socket = await factory();
const disposable = new DisposableStore();
this._managedRemoteSockets.set(id, { object: socket, disposer: disposable });
disposable.add(toDisposable(() => this._managedRemoteSockets.delete(id)));
disposable.add(socket.onDidEnd(() => {
this._mainThreadExtensionsProxy.$onDidRemoteSocketEnd(id);
disposable.dispose();
}));
disposable.add(socket.onDidClose(e => {
this._mainThreadExtensionsProxy.$onDidRemoteSocketClose(id, e?.stack ?? e?.message);
disposable.dispose();
}));
disposable.add(socket.onDidReceiveMessage(e => this._mainThreadExtensionsProxy.$onDidRemoteSocketHaveData(id, VSBuffer.wrap(e))));
return id;
}
public $remoteSocketDrain(id: number): Promise<void> {
return this._managedRemoteSockets.get(id)?.object.drainHandler?.() ?? Promise.resolve();
}
public $remoteSocketEnd(id: number): void {
const socket = this._managedRemoteSockets.get(id);
if (socket) {
socket.object.endHandler();
socket.disposer.dispose();
}
}
public $remoteSocketWrite(id: number, buffer: VSBuffer): void {
this._managedRemoteSockets.get(id)?.object.dataHandler(buffer.buffer);
}
public async $getCanonicalURI(remoteAuthority: string, uriComponents: UriComponents): Promise<UriComponents | null> {
this._logService.info(`$getCanonicalURI invoked for authority (${getRemoteAuthorityPrefix(remoteAuthority)})`);

View file

@ -479,6 +479,13 @@ export class Selection extends Range {
}
}
const validateConnectionToken = (connectionToken: string) => {
if (typeof connectionToken !== 'string' || connectionToken.length === 0 || !/^[0-9A-Za-z_\-]+$/.test(connectionToken)) {
throw illegalArgument('connectionToken');
}
};
export class ResolvedAuthority {
readonly host: string;
readonly port: number;
@ -492,9 +499,7 @@ export class ResolvedAuthority {
throw illegalArgument('port');
}
if (typeof connectionToken !== 'undefined') {
if (typeof connectionToken !== 'string' || connectionToken.length === 0 || !/^[0-9A-Za-z_\-]+$/.test(connectionToken)) {
throw illegalArgument('connectionToken');
}
validateConnectionToken(connectionToken);
}
this.host = host;
this.port = Math.round(port);
@ -502,6 +507,14 @@ export class ResolvedAuthority {
}
}
export class ManagedResolvedAuthority {
constructor(public readonly makeConnection: () => Thenable<vscode.ManagedMessagePassing>, public readonly connectionToken?: string) {
if (typeof connectionToken !== 'undefined') {
validateConnectionToken(connectionToken);
}
}
}
export class RemoteAuthorityResolverError extends Error {
static NotAvailable(message?: string, handled?: boolean): RemoteAuthorityResolverError {

View file

@ -18,7 +18,7 @@ import { IProductService } from 'vs/platform/product/common/productService';
import product from 'vs/platform/product/common/product';
import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteAgentService';
import { RemoteAuthorityResolverService } from 'vs/platform/remote/browser/remoteAuthorityResolverService';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRemoteAuthorityResolverService, MessagePassingType } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
@ -84,6 +84,8 @@ import { BrowserUserDataProfilesService } from 'vs/platform/userDataProfile/brow
import { timeout } from 'vs/base/common/async';
import { windowLogId } from 'vs/workbench/services/log/common/logConstants';
import { LogService } from 'vs/platform/log/common/logService';
import { IRemoteSocketFactoryCollection, RemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection';
import { BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory';
export class BrowserMain extends Disposable {
@ -253,7 +255,8 @@ export class BrowserMain extends Disposable {
// Remote
const connectionToken = environmentService.options.connectionToken || getCookieValue(connectionTokenCookieName);
const remoteAuthorityResolverService = new RemoteAuthorityResolverService(connectionToken, this.configuration.resourceUriProvider, productService, logService);
const expectResolverExtension = !!environmentService.remoteAuthority?.includes('+') && !environmentService.options.webSocketFactory;
const remoteAuthorityResolverService = new RemoteAuthorityResolverService(!expectResolverExtension, connectionToken, this.configuration.resourceUriProvider, productService, logService);
serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService);
// Signing
@ -292,7 +295,10 @@ export class BrowserMain extends Disposable {
serviceCollection.set(IUserDataProfileService, userDataProfileService);
// Remote Agent
const remoteAgentService = this._register(new RemoteAgentService(this.configuration.webSocketFactory, userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService));
const socketFactories = new RemoteSocketFactoryCollection();
socketFactories.register(MessagePassingType.WebSocket, () => new BrowserSocketFactory(this.configuration.webSocketFactory));
serviceCollection.set(IRemoteSocketFactoryCollection, socketFactories);
const remoteAgentService = this._register(new RemoteAgentService(socketFactories, userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService));
serviceCollection.set(IRemoteAgentService, remoteAgentService);
await this.registerFileSystemProviders(environmentService, fileService, remoteAgentService, bufferLogger, logService, loggerService, logsPath);

View file

@ -782,7 +782,10 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
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;
const redirect = resolveAuthority ? await this._portMappingManager.getRedirect({
connectionToken: resolveAuthority.authority.connectionToken,
connectTo: resolveAuthority.authority.messaging,
}, origin) : undefined;
return this._send('did-load-localhost', {
id,
origin,

View file

@ -25,7 +25,7 @@ import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services
import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService';
import { SharedProcessService } from 'vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService';
import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-sandbox/remoteAuthorityResolverService';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRemoteAuthorityResolverService, MessagePassingType } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentService';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { FileService } from 'vs/platform/files/common/fileService';
@ -55,6 +55,8 @@ import { PolicyChannelClient } from 'vs/platform/policy/common/policyIpc';
import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy';
import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService';
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory';
import { RemoteSocketFactoryCollection, IRemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection';
export class DesktopMain extends Disposable {
@ -236,7 +238,10 @@ export class DesktopMain extends Disposable {
serviceCollection.set(IUserDataProfileService, userDataProfileService);
// Remote Agent
const remoteAgentService = this._register(new RemoteAgentService(userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService));
const socketFactories = new RemoteSocketFactoryCollection();
socketFactories.register(MessagePassingType.WebSocket, () => new BrowserSocketFactory(null));
serviceCollection.set(IRemoteSocketFactoryCollection, socketFactories);
const remoteAgentService = this._register(new RemoteAgentService(socketFactories, userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService));
serviceCollection.set(IRemoteAgentService, remoteAgentService);
// Remote Files

View file

@ -842,7 +842,8 @@ export class NativeWindow extends Disposable {
const remoteAuthority = this.environmentService.remoteAuthority;
const addressProvider: IAddressProvider | undefined = remoteAuthority ? {
getAddress: async (): Promise<IAddress> => {
return (await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority)).authority;
const { authority } = await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority);
return { connectTo: authority.messaging, connectionToken: authority.connectionToken };
}
} : undefined;
let tunnel = await this.tunnelService.getExistingTunnel(portMappingRequest.address, portMappingRequest.port);

View file

@ -113,7 +113,7 @@ suite('ConfigurationEditing', () => {
const uriIdentityService = new UriIdentityService(fileService);
const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService));
userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService);
const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService, null));
const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService));
disposables.add(fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, logService))));
instantiationService.stub(IFileService, fileService);
instantiationService.stub(IRemoteAgentService, remoteAgentService);

View file

@ -51,6 +51,7 @@ import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler';
import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService';
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { TasksSchemaProperties } from 'vs/workbench/contrib/tasks/common/tasks';
import { RemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection';
function convertToWorkspacePayload(folder: URI): ISingleFolderWorkspaceIdentifier {
return {
@ -89,7 +90,7 @@ suite('WorkspaceContextService - Folder', () => {
const uriIdentityService = new UriIdentityService(fileService);
const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService);
const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService);
testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(null, userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService()));
testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryCollection(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService()));
await (<WorkspaceService>testObject).initialize(convertToWorkspacePayload(folder));
});
@ -132,7 +133,7 @@ suite('WorkspaceContextService - Folder', () => {
const uriIdentityService = new UriIdentityService(fileService);
const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService);
const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService);
const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(null, userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService()));
const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryCollection(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService()));
await (<WorkspaceService>testObject).initialize(convertToWorkspacePayload(folder));
const actual = testObject.getWorkspaceFolder(joinPath(folder, 'a'));
@ -155,7 +156,7 @@ suite('WorkspaceContextService - Folder', () => {
const uriIdentityService = new UriIdentityService(fileService);
const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService);
const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService);
const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(null, userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService()));
const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryCollection(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService()));
await (<WorkspaceService>testObject).initialize(convertToWorkspacePayload(folder));
@ -199,7 +200,7 @@ suite('WorkspaceContextService - Workspace', () => {
const instantiationService = <TestInstantiationService>workbenchInstantiationService(undefined, disposables);
const environmentService = TestEnvironmentService;
const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService, null));
const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService));
instantiationService.stub(IRemoteAgentService, remoteAgentService);
fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService())));
const uriIdentityService = new UriIdentityService(fileService);
@ -259,7 +260,7 @@ suite('WorkspaceContextService - Workspace Editing', () => {
const instantiationService = <TestInstantiationService>workbenchInstantiationService(undefined, disposables);
const environmentService = TestEnvironmentService;
const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null);
const remoteAgentService = instantiationService.createInstance(RemoteAgentService);
instantiationService.stub(IRemoteAgentService, remoteAgentService);
fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService())));
const uriIdentityService = new UriIdentityService(fileService);
@ -505,7 +506,7 @@ suite('WorkspaceService - Initialization', () => {
const instantiationService = <TestInstantiationService>workbenchInstantiationService(undefined, disposables);
environmentService = TestEnvironmentService;
const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null);
const remoteAgentService = instantiationService.createInstance(RemoteAgentService);
instantiationService.stub(IRemoteAgentService, remoteAgentService);
fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService())));
const uriIdentityService = new UriIdentityService(fileService);
@ -766,7 +767,7 @@ suite('WorkspaceConfigurationService - Folder', () => {
instantiationService = <TestInstantiationService>workbenchInstantiationService(undefined, disposables);
environmentService = TestEnvironmentService;
environmentService.policyFile = joinPath(folder, 'policies.json');
const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null);
const remoteAgentService = instantiationService.createInstance(RemoteAgentService);
instantiationService.stub(IRemoteAgentService, remoteAgentService);
fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService())));
const uriIdentityService = new UriIdentityService(fileService);
@ -1562,7 +1563,7 @@ suite('WorkspaceConfigurationService - Profiles', () => {
instantiationService = <TestInstantiationService>workbenchInstantiationService(undefined, disposables);
environmentService = TestEnvironmentService;
environmentService.policyFile = joinPath(folder, 'policies.json');
const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null);
const remoteAgentService = instantiationService.createInstance(RemoteAgentService);
instantiationService.stub(IRemoteAgentService, remoteAgentService);
fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService())));
const uriIdentityService = new UriIdentityService(fileService);
@ -1801,7 +1802,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => {
const instantiationService = <TestInstantiationService>workbenchInstantiationService(undefined, disposables);
environmentService = TestEnvironmentService;
const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null);
const remoteAgentService = instantiationService.createInstance(RemoteAgentService);
instantiationService.stub(IRemoteAgentService, remoteAgentService);
fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService())));
const uriIdentityService = new UriIdentityService(fileService);

View file

@ -170,7 +170,7 @@ class BrowserExtensionHostFactory implements IExtensionHostFactory {
case ExtensionHostKind.Remote: {
const remoteAgentConnection = this._remoteAgentService.getConnection();
if (remoteAgentConnection) {
return this._instantiationService.createInstance(RemoteExtensionHost, runningLocation, this._createRemoteExtensionHostDataProvider(runningLocations, remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory);
return this._instantiationService.createInstance(RemoteExtensionHost, runningLocation, this._createRemoteExtensionHostDataProvider(runningLocations, remoteAgentConnection.remoteAuthority));
}
return null;
}

View file

@ -938,7 +938,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
},
_onExtensionRuntimeError: (extensionId: ExtensionIdentifier, err: Error): void => {
return this._onExtensionRuntimeError(extensionId, err);
}
},
};
}

View file

@ -3,6 +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 { IDisposable } from 'vs/base/common/lifecycle';
import { BrandedService, IConstructorSignature } from 'vs/platform/instantiation/common/instantiation';
import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensionHostKind';
@ -15,8 +16,15 @@ export interface IExtHostContext extends IRPCProtocol {
readonly extensionHostKind: ExtensionHostKind;
}
export interface IManagedSocketCallbacks {
onDidRemoteSocketHaveData(id: number, data: VSBuffer): void;
onDidRemoteSocketEnd(id: number): void;
onDidRemoteSocketClose(id: number, error: Error | undefined): void;
}
export interface IInternalExtHostContext extends IExtHostContext {
readonly internalExtensionService: IInternalExtensionService;
readonly managedSocketCallbacks: IManagedSocketCallbacks;
_setExtensionHostProxy(extensionHostProxy: IExtensionHostProxy): void;
_setAllMainProxyIdentifiers(mainProxyIdentifiers: ProxyIdentifier<any>[]): void;
}

View file

@ -11,13 +11,15 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { StopWatch } from 'vs/base/common/stopwatch';
import { URI } from 'vs/base/common/uri';
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
import { SocketCloseEvent, SocketCloseEventType } from 'vs/base/parts/ipc/common/ipc.net';
import * as nls from 'vs/nls';
import { Categories } from 'vs/platform/action/common/actionCommonCategories';
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { RemoteAuthorityResolverErrorCode, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { ManagedMessagingPassing, MessagePassingType, RemoteAuthorityResolverErrorCode, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@ -29,6 +31,7 @@ import { ExtensionRunningLocation } from 'vs/workbench/services/extensions/commo
import { ActivationKind, ExtensionActivationReason, ExtensionHostExtensions, ExtensionHostStartup, IExtensionHost, IInternalExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { Proxied, ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier';
import { IRPCProtocolLogger, RPCProtocol, RequestInitiator, ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol';
import { ManagedSocket } from 'vs/workbench/services/remote/common/managedSocket';
// Enable to see detailed message communication between window and extension host
const LOG_EXTENSION_HOST_COMMUNICATION = false;
@ -85,6 +88,13 @@ type ExtensionHostStartupEvent = {
errorStack?: string;
};
interface RemoteSocketHalf {
onData: Emitter<VSBuffer>;
onClose: Emitter<SocketCloseEvent>;
onEnd: Emitter<void>;
}
class ExtensionHostManager extends Disposable implements IExtensionHostManager {
public readonly onDidExit: Event<[number, string | null]>;
@ -102,6 +112,7 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager {
private readonly _extensionHost: IExtensionHost;
private _proxy: Promise<IExtensionHostProxy | null> | null;
private _hasStarted = false;
private readonly _remoteSockets = new Map<number, RemoteSocketHalf>();
public get kind(): ExtensionHostKind {
return this._extensionHost.runningLocation.kind;
@ -115,6 +126,7 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager {
extensionHost: IExtensionHost,
initialActivationEvents: string[],
private readonly _internalExtensionService: IInternalExtensionService,
@IRemoteSocketFactoryCollection private readonly _remoteSocketFactoryCollection: IRemoteSocketFactoryCollection,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@ -287,6 +299,23 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager {
//#region internal
internalExtensionService: this._internalExtensionService,
managedSocketCallbacks: {
onDidRemoteSocketHaveData: (id, data) => {
this._remoteSockets.get(id)?.onData.fire(data);
},
onDidRemoteSocketEnd: id => {
this._remoteSockets.get(id)?.onEnd.fire();
this._remoteSockets.delete(id);
},
onDidRemoteSocketClose: (id, error) => {
this._remoteSockets.get(id)?.onClose.fire({
type: SocketCloseEventType.NodeSocketCloseEvent,
error,
hadError: !!error
});
this._remoteSockets.delete(id);
},
},
_setExtensionHostProxy: (value: IExtensionHostProxy): void => {
extensionHostProxy = value;
},
@ -409,7 +438,10 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager {
const resolverResult = await proxy.resolveAuthority(remoteAuthority, resolveAttempt);
intervalLogger.dispose();
if (resolverResult.type === 'ok') {
logInfo(`returned ${resolverResult.value.authority.host}:${resolverResult.value.authority.port}`);
logInfo(`returned ${resolverResult.value.authority}`);
if (resolverResult.value.authority.messaging.type === MessagePassingType.Managed) {
this.registerManagedSocketFactory(resolverResult.value.authority.messaging, proxy);
}
} else {
logError(`returned an error`, resolverResult.error);
}
@ -428,6 +460,38 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager {
}
}
private registerManagedSocketFactory(messaging: ManagedMessagingPassing, proxy: IExtensionHostProxy) {
this._remoteSocketFactoryCollection.register(MessagePassingType.Managed, resolved => {
if (resolved.id !== messaging.id) {
return undefined;
}
return {
connect: ({ id: factoryId }, path, query, debugLabel, callback) => {
proxy.openRemoteSocket(factoryId).then(socketId => {
const half: RemoteSocketHalf = {
onClose: new Emitter(),
onData: new Emitter(),
onEnd: new Emitter(),
};
this._remoteSockets.set(socketId, half);
ManagedSocket.connect(socketId, proxy, path, query, debugLabel, half)
.then(
socket => {
socket.onDidDispose(() => this._remoteSockets.delete(socketId));
callback(undefined, socket);
},
err => {
this._remoteSockets.delete(socketId);
callback(err, undefined);
});
}).catch(err => callback(err, undefined));
},
};
});
}
public async getCanonicalURI(remoteAuthority: string, uri: URI): Promise<URI | null> {
const proxy = await this._proxy;
if (!proxy) {

View file

@ -42,4 +42,9 @@ export interface IExtensionHostProxy {
test_latency(n: number): Promise<number>;
test_up(b: VSBuffer): Promise<number>;
test_down(size: number): Promise<VSBuffer>;
openRemoteSocket(factoryId: number): Promise<number>;
remoteSocketWrite(socketId: number, buffer: VSBuffer): void;
remoteSocketEnd(socketId: number): void;
remoteSocketDrain(socketId: number): Promise<void>;
}

View file

@ -16,8 +16,9 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio
import { ILabelService } from 'vs/platform/label/common/label';
import { ILogService, ILoggerService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { IConnectionOptions, IRemoteExtensionHostStartParams, ISocketFactory, connectRemoteAgentExtensionHost } from 'vs/platform/remote/common/remoteAgentConnection';
import { IConnectionOptions, IRemoteExtensionHostStartParams, connectRemoteAgentExtensionHost } from 'vs/platform/remote/common/remoteAgentConnection';
import { IRemoteAuthorityResolverService, IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection';
import { ISignService } from 'vs/platform/sign/common/sign';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isLoggingOnly } from 'vs/platform/telemetry/common/telemetryUtils';
@ -61,7 +62,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost {
constructor(
public readonly runningLocation: RemoteRunningLocation,
private readonly _initDataProvider: IRemoteExtensionHostDataProvider,
private readonly _socketFactory: ISocketFactory,
@IRemoteSocketFactoryCollection private readonly socketFactories: IRemoteSocketFactoryCollection,
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@ -84,21 +85,26 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost {
}
public start(): Promise<IMessagePassingProtocol> {
const options: IConnectionOptions = {
commit: this._productService.commit,
quality: this._productService.quality,
socketFactory: this._socketFactory,
addressProvider: {
getAddress: async () => {
const { authority } = await this.remoteAuthorityResolverService.resolveAuthority(this._initDataProvider.remoteAuthority);
return { host: authority.host, port: authority.port, connectionToken: authority.connectionToken };
}
},
signService: this._signService,
logService: this._logService,
ipcLogger: null
};
return this.remoteAuthorityResolverService.resolveAuthority(this._initDataProvider.remoteAuthority).then((resolverResult) => {
const socketFactory = this.socketFactories.create(resolverResult.authority.messaging);
if (!socketFactory) {
throw new Error('No socket factory found for remote authority');
}
const options: IConnectionOptions = {
commit: this._productService.commit,
quality: this._productService.quality,
socketFactory,
addressProvider: {
getAddress: async () => {
const { authority } = await this.remoteAuthorityResolverService.resolveAuthority(this._initDataProvider.remoteAuthority);
return { connectTo: authority.messaging, connectionToken: authority.connectionToken };
}
},
signService: this._signService,
logService: this._logService,
ipcLogger: null
};
const startParams: IRemoteExtensionHostStartParams = {
language: platform.language,

View file

@ -29,7 +29,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IProductService } from 'vs/platform/product/common/productService';
import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection';
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode, ResolverResult, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRemoteAuthorityResolverService, MessagePassingType, RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode, ResolverResult, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner';
import { getRemoteName, parseAuthorityWithPort } from 'vs/platform/remote/common/remoteHosts';
import { updateProxyConfigurationsScope } from 'vs/platform/request/common/request';
@ -277,15 +277,22 @@ export class NativeExtensionService extends AbstractExtensionService implements
const authorityPlusIndex = remoteAuthority.indexOf('+');
if (authorityPlusIndex === -1) {
// This authority does not need to be resolved, simply parse the port number
const { host, port } = parseAuthorityWithPort(remoteAuthority);
return {
authority: {
authority: remoteAuthority,
host,
port,
connectionToken: undefined
}
};
try {
const { host, port } = parseAuthorityWithPort(remoteAuthority);
return {
authority: {
authority: remoteAuthority,
messaging: {
type: MessagePassingType.WebSocket,
host,
port
},
connectionToken: undefined
}
};
} catch {
// continue
}
}
const localProcessExtensionHosts = this._getExtensionHostManagers(ExtensionHostKind.LocalProcess);
@ -391,7 +398,7 @@ export class NativeExtensionService extends AbstractExtensionService implements
performance.mark(`code/willResolveAuthority/${authorityPrefix}`);
const result = await this._resolveAuthority(remoteAuthority);
performance.mark(`code/didResolveAuthorityOK/${authorityPrefix}`);
this._logService.info(`resolveAuthority(${authorityPrefix}) returned '${result.authority.host}:${result.authority.port}' after ${sw.elapsed()} ms`);
this._logService.info(`resolveAuthority(${authorityPrefix}) returned '${result.authority}' after ${sw.elapsed()} ms`);
return result;
} catch (err) {
performance.mark(`code/didResolveAuthorityError/${authorityPrefix}`);
@ -631,7 +638,7 @@ class NativeExtensionHostFactory implements IExtensionHostFactory {
case ExtensionHostKind.Remote: {
const remoteAgentConnection = this._remoteAgentService.getConnection();
if (remoteAgentConnection) {
return this._instantiationService.createInstance(RemoteExtensionHost, runningLocation, this._createRemoteExtensionHostDataProvider(runningLocations, remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory);
return this._instantiationService.createInstance(RemoteExtensionHost, runningLocation, this._createRemoteExtensionHostDataProvider(runningLocations, remoteAgentConnection.remoteAuthority));
}
return null;
}

View file

@ -9,7 +9,6 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA
import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { AbstractRemoteAgentService } from 'vs/workbench/services/remote/common/abstractRemoteAgentService';
import { IProductService } from 'vs/platform/product/common/productService';
import { IWebSocketFactory, BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory';
import { ISignService } from 'vs/platform/sign/common/sign';
import { ILogService } from 'vs/platform/log/common/log';
import { Severity } from 'vs/platform/notification/common/notification';
@ -19,11 +18,12 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } f
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { IRemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection';
export class RemoteAgentService extends AbstractRemoteAgentService implements IRemoteAgentService {
constructor(
webSocketFactory: IWebSocketFactory | null | undefined,
@IRemoteSocketFactoryCollection socketFactories: IRemoteSocketFactoryCollection,
@IUserDataProfileService userDataProfileService: IUserDataProfileService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IProductService productService: IProductService,
@ -31,7 +31,7 @@ export class RemoteAgentService extends AbstractRemoteAgentService implements IR
@ISignService signService: ISignService,
@ILogService logService: ILogService
) {
super(new BrowserSocketFactory(webSocketFactory), userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService);
super(socketFactories, userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService);
}
}

View file

@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { IChannel, IServerChannel, getDelayedChannel, IPCLogger } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/common/ipc.net';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { connectRemoteAgentManagement, IConnectionOptions, ISocketFactory, ManagementPersistentConnection, PersistentConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection';
import { connectRemoteAgentManagement, IConnectionOptions, ManagementPersistentConnection, PersistentConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection';
import { IExtensionHostExitInfo, IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { RemoteAgentConnectionContext, IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
@ -19,17 +19,17 @@ import { ILogService } from 'vs/platform/log/common/log';
import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
import { IProductService } from 'vs/platform/product/common/productService';
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { IRemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection';
export abstract class AbstractRemoteAgentService extends Disposable implements IRemoteAgentService {
declare readonly _serviceBrand: undefined;
public readonly socketFactory: ISocketFactory;
private readonly _connection: IRemoteAgentConnection | null;
private _environment: Promise<IRemoteAgentEnvironment | null> | null;
constructor(
socketFactory: ISocketFactory,
@IRemoteSocketFactoryCollection private readonly socketFactories: IRemoteSocketFactoryCollection,
@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,
@IWorkbenchEnvironmentService protected readonly _environmentService: IWorkbenchEnvironmentService,
@IProductService productService: IProductService,
@ -38,9 +38,8 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I
@ILogService logService: ILogService
) {
super();
this.socketFactory = socketFactory;
if (this._environmentService.remoteAuthority) {
this._connection = this._register(new RemoteAgentConnection(this._environmentService.remoteAuthority, productService.commit, productService.quality, this.socketFactory, this._remoteAuthorityResolverService, signService, logService));
this._connection = this._register(new RemoteAgentConnection(this._environmentService.remoteAuthority, productService.commit, productService.quality, this.socketFactories, this._remoteAuthorityResolverService, signService, logService));
} else {
this._connection = null;
}
@ -150,7 +149,7 @@ class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection
remoteAuthority: string,
private readonly _commit: string | undefined,
private readonly _quality: string | undefined,
private readonly _socketFactory: ISocketFactory,
private readonly _socketFactories: IRemoteSocketFactoryCollection,
private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
private readonly _signService: ISignService,
private readonly _logService: ILogService
@ -193,28 +192,35 @@ class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection
private async _createConnection(): Promise<Client<RemoteAgentConnectionContext>> {
let firstCall = true;
const options: IConnectionOptions = {
commit: this._commit,
quality: this._quality,
socketFactory: this._socketFactory,
addressProvider: {
getAddress: async () => {
if (firstCall) {
firstCall = false;
} else {
this._onReconnecting.fire(undefined);
}
const { authority } = await this._remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority);
return { host: authority.host, port: authority.port, connectionToken: authority.connectionToken };
}
},
signService: this._signService,
logService: this._logService,
ipcLogger: false ? new IPCLogger(`Local \u2192 Remote`, `Remote \u2192 Local`) : null
};
let connection: ManagementPersistentConnection;
const start = Date.now();
try {
const { authority } = await this._remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority);
const socketFactory = this._socketFactories.create(authority.messaging);
if (!socketFactory) {
throw new Error(`No socket factory found for ${authority}`);
}
const options: IConnectionOptions = {
commit: this._commit,
quality: this._quality,
socketFactory,
addressProvider: {
getAddress: async () => {
if (firstCall) {
firstCall = false;
} else {
this._onReconnecting.fire(undefined);
}
const { authority } = await this._remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority);
return { connectTo: authority.messaging, connectionToken: authority.connectionToken };
}
},
signService: this._signService,
logService: this._logService,
ipcLogger: false ? new IPCLogger(`Local \u2192 Remote`, `Remote \u2192 Local`) : null
};
connection = this._register(await connectRemoteAgentManagement(options, this.remoteAuthority, `renderer`));
} finally {
this._initialConnectionMs = Date.now() - start;

View file

@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { VSBuffer } from 'vs/base/common/buffer';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { ISocket, SocketCloseEvent, SocketDiagnostics, SocketDiagnosticsEventType } from 'vs/base/parts/ipc/common/ipc.net';
import { makeRawSocketHeaders, socketRawEndHeaderSequence } from 'vs/platform/remote/common/managedSocket';
import { IExtensionHostProxy } from 'vs/workbench/services/extensions/common/extensionHostProxy';
export class ManagedSocket extends Disposable implements ISocket {
public static connect(
socketId: number,
proxy: IExtensionHostProxy,
path: string, query: string, debugLabel: string,
half: {
onClose: Emitter<SocketCloseEvent>;
onData: Emitter<VSBuffer>;
onEnd: Emitter<void>;
}
): Promise<ManagedSocket> {
const socket = new ManagedSocket(socketId, proxy, debugLabel, half.onClose, half.onData, half.onEnd);
socket.write(VSBuffer.fromString(makeRawSocketHeaders(path, query, debugLabel)));
const d = new DisposableStore();
return new Promise<ManagedSocket>((resolve, reject) => {
d.add(socket.onData(d => {
if (d.indexOf(socketRawEndHeaderSequence) !== -1) {
resolve(socket);
}
}));
d.add(socket.onClose(err => reject(err ?? new Error('socket closed'))));
d.add(socket.onEnd(() => reject(new Error('socket ended'))));
}).finally(() => d.dispose());
}
public onData: Event<VSBuffer>;
public onClose: Event<SocketCloseEvent>;
public onEnd: Event<void>;
private readonly didDisposeEmitter = this._register(new Emitter<void>());
public onDidDispose = this.didDisposeEmitter.event;
private ended = false;
private constructor(
private readonly socketId: number,
private readonly proxy: IExtensionHostProxy,
private readonly debugLabel: string,
onCloseEmitter: Emitter<SocketCloseEvent>,
onDataEmitter: Emitter<VSBuffer>,
onEndEmitter: Emitter<void>,
) {
super();
this.onClose = this._register(onCloseEmitter).event;
this.onData = this._register(onDataEmitter).event;
this.onEnd = this._register(onEndEmitter).event;
}
write(buffer: VSBuffer): void {
this.proxy.remoteSocketWrite(this.socketId, buffer);
}
end(): void {
this.ended = true;
this.proxy.remoteSocketEnd(this.socketId);
}
drain(): Promise<void> {
return this.proxy.remoteSocketDrain(this.socketId);
}
traceSocketEvent(type: SocketDiagnosticsEventType, data?: any): void {
SocketDiagnostics.traceSocketEvent(this, this.debugLabel, type, data);
}
override dispose(): void {
if (!this.ended) {
this.proxy.remoteSocketEnd(this.socketId);
}
this.didDisposeEmitter.fire();
super.dispose();
}
}

View file

@ -8,7 +8,7 @@ import { RemoteAgentConnectionContext, IRemoteAgentEnvironment } from 'vs/platfo
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics';
import { Event } from 'vs/base/common/event';
import { PersistentConnectionEvent, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
import { PersistentConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection';
import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
export const IRemoteAgentService = createDecorator<IRemoteAgentService>('remoteAgentService');
@ -16,8 +16,6 @@ export const IRemoteAgentService = createDecorator<IRemoteAgentService>('remoteA
export interface IRemoteAgentService {
readonly _serviceBrand: undefined;
readonly socketFactory: ISocketFactory;
getConnection(): IRemoteAgentConnection | null;
/**
* Get the remote environment. In case of an error, returns `null`.

View file

@ -621,7 +621,10 @@ export class TunnelModel extends Disposable {
if (!existingTunnel) {
const authority = this.environmentService.remoteAuthority;
const addressProvider: IAddressProvider | undefined = authority ? {
getAddress: async () => { return (await this.remoteAuthorityResolverService.resolveAuthority(authority)).authority; }
getAddress: async () => {
const r = await this.remoteAuthorityResolverService.resolveAuthority(authority);
return { connectTo: r.authority.messaging, connectionToken: r.authority.connectionToken };
}
} : undefined;
const key = makeAddress(tunnelProperties.remote.host, tunnelProperties.remote.port);

View file

@ -5,9 +5,8 @@
import * as nls from 'vs/nls';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRemoteAuthorityResolverService, MessagePassingType, RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IProductService } from 'vs/platform/product/common/productService';
import { BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory';
import { AbstractRemoteAgentService } from 'vs/workbench/services/remote/common/abstractRemoteAgentService';
import { ISignService } from 'vs/platform/sign/common/sign';
import { ILogService } from 'vs/platform/log/common/log';
@ -21,9 +20,11 @@ import { INativeHostService } from 'vs/platform/native/common/native';
import { URI } from 'vs/base/common/uri';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { IRemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection';
export class RemoteAgentService extends AbstractRemoteAgentService implements IRemoteAgentService {
constructor(
@IRemoteSocketFactoryCollection socketFactoryCollection: IRemoteSocketFactoryCollection,
@IUserDataProfileService userDataProfileService: IUserDataProfileService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IProductService productService: IProductService,
@ -31,7 +32,7 @@ export class RemoteAgentService extends AbstractRemoteAgentService implements IR
@ISignService signService: ISignService,
@ILogService logService: ILogService,
) {
super(new BrowserSocketFactory(null), userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService);
super(socketFactoryCollection, userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService);
}
}
@ -79,12 +80,12 @@ class RemoteConnectionFailureNotificationContribution implements IWorkbenchContr
return null;
}
const connectionData = this._remoteAuthorityResolverService.getConnectionData(remoteAgentConnection.remoteAuthority);
if (!connectionData) {
if (!connectionData || connectionData.connectTo.type !== MessagePassingType.WebSocket) {
return null;
}
return URI.from({
scheme: 'http',
authority: `${connectionData.host}:${connectionData.port}`,
authority: `${connectionData.connectTo.host}:${connectionData.connectTo.port}`,
path: `/version`
});
}

View file

@ -167,6 +167,7 @@ import { InstallVSIXOptions, ILocalExtension, IGalleryExtension, InstallOptions,
import { Codicon } from 'vs/base/common/codicons';
import { IHoverOptions, IHoverService, IHoverWidget } from 'vs/workbench/services/hover/browser/hover';
import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner';
import { IRemoteSocketFactoryCollection, RemoteSocketFactoryCollection } from 'vs/platform/remote/common/remoteSocketFactoryCollection';
export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput {
return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined);
@ -325,6 +326,7 @@ export function workbenchInstantiationService(
instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService());
instantiationService.stub(ITerminalInstanceService, new TestTerminalInstanceService());
instantiationService.stub(IElevatedFileService, new BrowserElevatedFileService());
instantiationService.stub(IRemoteSocketFactoryCollection, new RemoteSocketFactoryCollection());
return instantiationService;
}
@ -1937,7 +1939,7 @@ export class TestRemoteAgentService implements IRemoteAgentService {
declare readonly _serviceBrand: undefined;
socketFactory: ISocketFactory = {
socketFactory: ISocketFactory<any> = {
connect() { }
};

View file

@ -26,6 +26,23 @@ declare module 'vscode' {
constructor(host: string, port: number, connectionToken?: string);
}
export interface ManagedMessagePassing {
onDidReceiveMessage: Event<Uint8Array>;
onDidClose: Event<Error | undefined>;
onDidEnd: Event<void>;
dataHandler: (data: Uint8Array) => void;
endHandler: () => void;
drainHandler?: () => void;
}
export class ManagedResolvedAuthority {
readonly makeConnection: () => Thenable<ManagedMessagePassing>;
readonly connectionToken: string | undefined;
constructor(makeConnection: () => Thenable<ManagedMessagePassing>, connectionToken?: string);
}
export interface ResolvedOptions {
extensionHostEnv?: { [key: string]: string | null };
@ -109,7 +126,7 @@ declare module 'vscode' {
Output = 2
}
export type ResolverResult = ResolvedAuthority & ResolvedOptions & TunnelInformation;
export type ResolverResult = (ResolvedAuthority | ManagedResolvedAuthority) & ResolvedOptions & TunnelInformation;
export class RemoteAuthorityResolverError extends Error {
static NotAvailable(message?: string, handled?: boolean): RemoteAuthorityResolverError;