Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Take into account already activated extensions when computing running locations #184239

Merged
merged 3 commits into from
Jun 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

//@ts-check

'use strict';

const withBrowserDefaults = require('../shared.webpack.config').browser;

module.exports = withBrowserDefaults({
context: __dirname,
entry: {
extension: './src/extension.browser.ts'
},
output: {
filename: 'testResolverMain.js'
}
});
4 changes: 2 additions & 2 deletions extensions/vscode-test-resolver/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
"version": "0.0.1",
"publisher": "vscode",
"license": "MIT",
"enableProposedApi": true,
"enabledApiProposals": [
"resolvers",
"tunnels"
"tunnels"
],
"private": true,
"engines": {
Expand All @@ -32,6 +31,7 @@
"onCommand:vscode-testresolver.toggleConnectionPause"
],
"main": "./out/extension",
"browser": "./dist/browser/testResolverMain",
"devDependencies": {
"@types/node": "16.x"
},
Expand Down
133 changes: 133 additions & 0 deletions extensions/vscode-test-resolver/src/extension.browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';

export function activate(_context: vscode.ExtensionContext) {
vscode.workspace.registerRemoteAuthorityResolver('test', {
async resolve(_authority: string): Promise<vscode.ResolverResult> {
console.log(`Resolving ${_authority}`);
console.log(`Activating vscode.github-authentication to simulate auth`);
await vscode.extensions.getExtension('vscode.github-authentication')?.activate();
return new vscode.ManagedResolvedAuthority(async () => {
return new InitialManagedMessagePassing();
});
}
});
}

/**
* The initial message passing is a bit special because we need to
* wait for the HTTP headers to arrive before we can create the
* actual WebSocket.
*/
class InitialManagedMessagePassing implements vscode.ManagedMessagePassing {
private readonly dataEmitter = new vscode.EventEmitter<Uint8Array>();
private readonly closeEmitter = new vscode.EventEmitter<Error | undefined>();
private readonly endEmitter = new vscode.EventEmitter<void>();

public readonly onDidReceiveMessage = this.dataEmitter.event;
public readonly onDidClose = this.closeEmitter.event;
public readonly onDidEnd = this.endEmitter.event;

private _actual: OpeningManagedMessagePassing | null = null;
private _isDisposed = false;

public send(d: Uint8Array): void {
if (this._actual) {
// we already got the HTTP headers
this._actual.send(d);
return;
}

if (this._isDisposed) {
// got disposed in the meantime, ignore
return;
}

// we now received the HTTP headers
const decoder = new TextDecoder();
const str = decoder.decode(d);

// example str GET ws://localhost/oss-dev?reconnectionToken=4354a323-a45a-452c-b5d7-d8d586e1cd5c&reconnection=false&skipWebSocketFrames=true HTTP/1.1
const match = str.match(/GET\s+(\S+)\s+HTTP/);
if (!match) {
console.error(`Coult not parse ${str}`);
this.closeEmitter.fire(new Error(`Coult not parse ${str}`));
return;
}

// example url ws://localhost/oss-dev?reconnectionToken=4354a323-a45a-452c-b5d7-d8d586e1cd5c&reconnection=false&skipWebSocketFrames=true
const url = new URL(match[1]);

// extract path and query from url using browser's URL
const parsedUrl = new URL(url);
this._actual = new OpeningManagedMessagePassing(parsedUrl, this.dataEmitter, this.closeEmitter, this.endEmitter);
}

public end(): void {
if (this._actual) {
this._actual.end();
return;
}
this._isDisposed = true;
}
}

class OpeningManagedMessagePassing {

private readonly socket: WebSocket;
private isOpen = false;
private bufferedData: Uint8Array[] = [];

constructor(
url: URL,
dataEmitter: vscode.EventEmitter<Uint8Array>,
closeEmitter: vscode.EventEmitter<Error | undefined>,
_endEmitter: vscode.EventEmitter<void>
) {
this.socket = new WebSocket(`ws://localhost:9888${url.pathname}${url.search.replace(/skipWebSocketFrames=true/, 'skipWebSocketFrames=false')}`);
this.socket.addEventListener('close', () => closeEmitter.fire(undefined));
this.socket.addEventListener('error', (e) => closeEmitter.fire(new Error(String(e))));
this.socket.addEventListener('message', async (e) => {
const arrayBuffer = await e.data.arrayBuffer();
dataEmitter.fire(new Uint8Array(arrayBuffer));
});
this.socket.addEventListener('open', () => {
while (this.bufferedData.length > 0) {
const first = this.bufferedData.shift()!;
this.socket.send(first);
}
this.isOpen = true;

// https://tools.ietf.org/html/rfc6455#section-4
// const requestNonce = req.headers['sec-websocket-key'];
// const hash = crypto.createHash('sha1');
// hash.update(requestNonce + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
// const responseNonce = hash.digest('base64');
const responseHeaders = [
`HTTP/1.1 101 Switching Protocols`,
`Upgrade: websocket`,
`Connection: Upgrade`,
`Sec-WebSocket-Accept: TODO`
];
const textEncoder = new TextEncoder();
textEncoder.encode(responseHeaders.join('\r\n') + '\r\n\r\n');
dataEmitter.fire(textEncoder.encode(responseHeaders.join('\r\n') + '\r\n\r\n'));
});
}

public send(d: Uint8Array): void {
if (!this.isOpen) {
this.bufferedData.push(d);
return;
}
this.socket.send(d);
}

public end(): void {
this.socket.close();
}
}
3 changes: 3 additions & 0 deletions extensions/vscode-test-resolver/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"outDir": "./out",
"types": [
"node"
],
"lib": [
"WebWorker"
]
},
"include": [
Expand Down
2 changes: 2 additions & 0 deletions src/vs/server/node/serverEnvironmentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const serverOptions: OptionDescriptions<Required<ServerParsedArgs>> = {

'enable-sync': { type: 'boolean' },
'github-auth': { type: 'string' },
'use-test-resolver': { type: 'boolean' },

/* ----- extension management ----- */

Expand Down Expand Up @@ -165,6 +166,7 @@ export interface ServerParsedArgs {

'enable-sync'?: boolean;
'github-auth'?: string;
'use-test-resolver'?: boolean;

/* ----- extension management ----- */

Expand Down
20 changes: 17 additions & 3 deletions src/vs/server/node/webClientServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { isLinux } from 'vs/base/common/platform';
import { ILogService } from 'vs/platform/log/common/log';
import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService';
import { extname, dirname, join, normalize } from 'vs/base/common/path';
import { FileAccess, connectionTokenCookieName, connectionTokenQueryName, Schemas } from 'vs/base/common/network';
import { FileAccess, connectionTokenCookieName, connectionTokenQueryName, Schemas, builtinExtensionsPath } from 'vs/base/common/network';
import { generateUuid } from 'vs/base/common/uuid';
import { IProductService } from 'vs/platform/product/common/productService';
import { ServerConnectionToken, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken';
Expand All @@ -28,6 +28,7 @@ import { IProductConfiguration } from 'vs/base/common/product';
import { isString } from 'vs/base/common/types';
import { CharCode } from 'vs/base/common/charCode';
import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts';
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';

const textMimeType = {
'.html': 'text/html',
Expand Down Expand Up @@ -272,7 +273,12 @@ export class WebClientServer {
return Array.isArray(val) ? val[0] : val;
};

const remoteAuthority = getFirstHeader('x-original-host') || getFirstHeader('x-forwarded-host') || req.headers.host;
const useTestResolver = (!this._environmentService.isBuilt && this._environmentService.args['use-test-resolver']);
const remoteAuthority = (
useTestResolver
? 'test+test'
: (getFirstHeader('x-original-host') || getFirstHeader('x-forwarded-host') || req.headers.host)
);
if (!remoteAuthority) {
return serveError(req, res, 400, `Bad request.`);
}
Expand Down Expand Up @@ -337,6 +343,14 @@ export class WebClientServer {
WORKBENCH_NLS_BASE_URL: nlsBaseUrl ? `${nlsBaseUrl}${!nlsBaseUrl.endsWith('/') ? '/' : ''}${this._productService.commit}/${this._productService.version}/` : '',
};

if (useTestResolver) {
const bundledExtensions: { extensionPath: string; packageJSON: IExtensionManifest }[] = [];
for (const extensionPath of ['vscode-test-resolver', 'github-authentication']) {
const packageJSON = JSON.parse((await fsp.readFile(FileAccess.asFileUri(`${builtinExtensionsPath}/${extensionPath}/package.json`).fsPath)).toString());
bundledExtensions.push({ extensionPath, packageJSON });
}
values['WORKBENCH_BUILTIN_EXTENSIONS'] = asJSON(bundledExtensions);
}

let data;
try {
Expand All @@ -351,7 +365,7 @@ export class WebClientServer {
'default-src \'self\';',
'img-src \'self\' https: data: blob:;',
'media-src \'self\';',
`script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-fh3TwPMflhsEIpR8g1OYTIMVWhXTLcjQ9kh2tIpmv54=' http://${remoteAuthority};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html
`script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-fh3TwPMflhsEIpR8g1OYTIMVWhXTLcjQ9kh2tIpmv54=' ${useTestResolver ? '' : `http://${remoteAuthority}`};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html
'child-src \'self\';',
`frame-src 'self' https://*.vscode-cdn.net data:;`,
'worker-src \'self\' data: blob:;',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
}

protected _doCreateExtensionHostManager(extensionHost: IExtensionHost, initialActivationEvents: string[]): IExtensionHostManager {
return createExtensionHostManager(this._instantiationService, extensionHost, initialActivationEvents, this._acquireInternalAPI());
return createExtensionHostManager(this._instantiationService, extensionHost, initialActivationEvents, this._acquireInternalAPI(extensionHost));
}

private _onExtensionHostCrashOrExit(extensionHost: IExtensionHostManager, code: number, signal: string | null): void {
Expand Down Expand Up @@ -1070,13 +1070,13 @@ export abstract class AbstractExtensionService extends Disposable implements IEx

//#region Called by extension host

private _acquireInternalAPI(): IInternalExtensionService {
private _acquireInternalAPI(extensionHost: IExtensionHost): IInternalExtensionService {
return {
_activateById: (extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void> => {
return this._activateById(extensionId, reason);
},
_onWillActivateExtension: (extensionId: ExtensionIdentifier): void => {
return this._onWillActivateExtension(extensionId);
return this._onWillActivateExtension(extensionId, extensionHost.runningLocation);
},
_onDidActivateExtension: (extensionId: ExtensionIdentifier, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationReason: ExtensionActivationReason): void => {
return this._onDidActivateExtension(extensionId, codeLoadingTime, activateCallTime, activateResolvedTime, activationReason);
Expand All @@ -1100,7 +1100,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
}
}

private _onWillActivateExtension(extensionId: ExtensionIdentifier): void {
private _onWillActivateExtension(extensionId: ExtensionIdentifier, runningLocation: ExtensionRunningLocation): void {
this._runningLocations.set(extensionId, runningLocation);
const extensionStatus = this._getOrCreateExtensionStatus(extensionId);
extensionStatus.onWillActivate();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export class ExtensionRunningLocationTracker {
@IExtensionManifestPropertiesService private readonly _extensionManifestPropertiesService: IExtensionManifestPropertiesService,
) { }

public set(extensionId: ExtensionIdentifier, runningLocation: ExtensionRunningLocation) {
this._runningLocation.set(extensionId, runningLocation);
}

public readExtensionKinds(extensionDescription: IExtensionDescription): ExtensionKind[] {
if (extensionDescription.isUnderDevelopment && this._environmentService.extensionDevelopmentKind) {
return this._environmentService.extensionDevelopmentKind;
Expand Down Expand Up @@ -196,10 +200,14 @@ export class ExtensionRunningLocationTracker {
}

public computeRunningLocation(localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], isInitialAllocation: boolean): ExtensionIdentifierMap<ExtensionRunningLocation | null> {
return this._doComputeRunningLocation(localExtensions, remoteExtensions, isInitialAllocation).runningLocation;
return this._doComputeRunningLocation(this._runningLocation, localExtensions, remoteExtensions, isInitialAllocation).runningLocation;
}

private _doComputeRunningLocation(localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], isInitialAllocation: boolean): { runningLocation: ExtensionIdentifierMap<ExtensionRunningLocation | null>; maxLocalProcessAffinity: number; maxLocalWebWorkerAffinity: number } {
private _doComputeRunningLocation(existingRunningLocation: ExtensionIdentifierMap<ExtensionRunningLocation | null>, localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], isInitialAllocation: boolean): { runningLocation: ExtensionIdentifierMap<ExtensionRunningLocation | null>; maxLocalProcessAffinity: number; maxLocalWebWorkerAffinity: number } {
// Skip extensions that have an existing running location
localExtensions = localExtensions.filter(extension => !existingRunningLocation.has(extension.identifier));
remoteExtensions = remoteExtensions.filter(extension => !existingRunningLocation.has(extension.identifier));

const extensionHostKinds = determineExtensionHostKinds(
localExtensions,
remoteExtensions,
Expand Down Expand Up @@ -247,11 +255,18 @@ export class ExtensionRunningLocationTracker {
result.set(extension.identifier, new LocalWebWorkerRunningLocation(affinity));
}

// Add extensions that already have an existing running location
for (const [extensionIdKey, runningLocation] of existingRunningLocation) {
if (runningLocation) {
result.set(extensionIdKey, runningLocation);
}
}

return { runningLocation: result, maxLocalProcessAffinity: maxAffinity, maxLocalWebWorkerAffinity: maxLocalWebWorkerAffinity };
}

public initializeRunningLocation(localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[]): void {
const { runningLocation, maxLocalProcessAffinity, maxLocalWebWorkerAffinity } = this._doComputeRunningLocation(localExtensions, remoteExtensions, true);
const { runningLocation, maxLocalProcessAffinity, maxLocalWebWorkerAffinity } = this._doComputeRunningLocation(this._runningLocation, localExtensions, remoteExtensions, true);
this._runningLocation = runningLocation;
this._maxLocalProcessAffinity = maxLocalProcessAffinity;
this._maxLocalWebWorkerAffinity = maxLocalWebWorkerAffinity;
Expand Down