Files
vscode/src/vs/workbench/api/worker/extHostExtensionService.ts
2022-03-29 10:14:42 -07:00

218 lines
7.7 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createApiFactoryAndRegisterActors } from 'vs/workbench/api/common/extHost.api.impl';
import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHostExtensionActivator';
import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService';
import { URI } from 'vs/base/common/uri';
import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes';
import { timeout } from 'vs/base/common/async';
import { MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol';
class WorkerRequireInterceptor extends RequireInterceptor {
_installInterceptor() { }
getModule(request: string, parent: URI): undefined | any {
for (let alternativeModuleName of this._alternatives) {
let alternative = alternativeModuleName(request);
if (alternative) {
request = alternative;
break;
}
}
if (this._factories.has(request)) {
return this._factories.get(request)!.load(request, parent, () => { throw new Error('CANNOT LOAD MODULE from here.'); });
}
return undefined;
}
}
export class ExtHostExtensionService extends AbstractExtHostExtensionService {
readonly extensionRuntime = ExtensionRuntime.Webworker;
private _fakeModules?: WorkerRequireInterceptor;
protected async _beforeAlmostReadyToRunExtensions(): Promise<void> {
const mainThreadConsole = this._extHostContext.getProxy(MainContext.MainThreadConsole);
wrapConsoleMethods(mainThreadConsole, this._initData.environment.isExtensionDevelopmentDebug);
// initialize API and register actors
const apiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors);
this._fakeModules = this._instaService.createInstance(WorkerRequireInterceptor, apiFactory, this._registry);
await this._fakeModules.install();
performance.mark('code/extHost/didInitAPI');
await this._waitForDebuggerAttachment();
}
protected _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined {
return extensionDescription.browser;
}
protected async _loadCommonJSModule<T extends object | undefined>(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
module = module.with({ path: ensureSuffix(module.path, '.js') });
if (extensionId) {
performance.mark(`code/extHost/willFetchExtensionCode/${extensionId.value}`);
}
// First resolve the extension entry point URI to something we can load using `fetch`
// This needs to be done on the main thread due to a potential `resourceUriProvider` (workbench api)
// which is only available in the main thread
const browserUri = URI.revive(await this._mainThreadExtensionsProxy.$asBrowserUri(module));
const response = await fetch(browserUri.toString(true));
if (extensionId) {
performance.mark(`code/extHost/didFetchExtensionCode/${extensionId.value}`);
}
if (response.status !== 200) {
throw new Error(response.statusText);
}
// fetch JS sources as text and create a new function around it
const source = await response.text();
// Here we append #vscode-extension to serve as a marker, such that source maps
// can be adjusted for the extra wrapping function.
const sourceURL = `${module.toString(true)}#vscode-extension`;
const fullSource = `${source}\n//# sourceURL=${sourceURL}`;
let initFn: Function;
try {
initFn = new Function('module', 'exports', 'require', fullSource);
} catch (err) {
if (extensionId) {
console.error(`Loading code for extension ${extensionId.value} failed: ${err.message}`);
} else {
console.error(`Loading code failed: ${err.message}`);
}
console.error(`${module.toString(true)}${typeof err.line === 'number' ? ` line ${err.line}` : ''}${typeof err.column === 'number' ? ` column ${err.column}` : ''}`);
console.error(err);
throw err;
}
// define commonjs globals: `module`, `exports`, and `require`
const _exports = {};
const _module = { exports: _exports };
const _require = (request: string) => {
const result = this._fakeModules!.getModule(request, module);
if (result === undefined) {
throw new Error(`Cannot load module '${request}'`);
}
return result;
};
try {
activationTimesBuilder.codeLoadingStart();
if (extensionId) {
performance.mark(`code/extHost/willLoadExtensionCode/${extensionId.value}`);
}
initFn(_module, _exports, _require);
return <T>(_module.exports !== _exports ? _module.exports : _exports);
} finally {
if (extensionId) {
performance.mark(`code/extHost/didLoadExtensionCode/${extensionId.value}`);
}
activationTimesBuilder.codeLoadingStop();
}
}
async $setRemoteEnvironment(_env: { [key: string]: string | null }): Promise<void> {
return;
}
private async _waitForDebuggerAttachment(waitTimeout = 5000) {
// debugger attaches async, waiting for it fixes #106698 and #99222
if (!this._initData.environment.isExtensionDevelopmentDebug) {
return;
}
const deadline = Date.now() + waitTimeout;
while (Date.now() < deadline && !('__jsDebugIsReady' in globalThis)) {
await timeout(10);
}
}
}
function ensureSuffix(path: string, suffix: string): string {
return path.endsWith(suffix) ? path : path + suffix;
}
// copied from bootstrap-fork.js
function wrapConsoleMethods(service: MainThreadConsoleShape, callToNative: boolean) {
wrap('info', 'log');
wrap('log', 'log');
wrap('warn', 'warn');
wrap('error', 'error');
function wrap(method: 'error' | 'warn' | 'info' | 'log', severity: 'error' | 'warn' | 'log') {
const original = console[method];
console[method] = function () {
service.$logExtensionHostMessage({ type: '__$console', severity, arguments: safeToArray(arguments) });
if (callToNative) {
original.apply(console, arguments as any);
}
};
}
const MAX_LENGTH = 100000;
function safeToArray(args: IArguments) {
const seen: any[] = [];
const argsArray = [];
// Massage some arguments with special treatment
if (args.length) {
for (let i = 0; i < args.length; i++) {
// Any argument of type 'undefined' needs to be specially treated because
// JSON.stringify will simply ignore those. We replace them with the string
// 'undefined' which is not 100% right, but good enough to be logged to console
if (typeof args[i] === 'undefined') {
args[i] = 'undefined';
}
// Any argument that is an Error will be changed to be just the error stack/message
// itself because currently cannot serialize the error over entirely.
else if (args[i] instanceof Error) {
const errorObj = args[i];
if (errorObj.stack) {
args[i] = errorObj.stack;
} else {
args[i] = errorObj.toString();
}
}
argsArray.push(args[i]);
}
}
try {
const res = JSON.stringify(argsArray, function (key, value) {
// Objects get special treatment to prevent circles
if (value && typeof value === 'object') {
if (seen.indexOf(value) !== -1) {
return '[Circular]';
}
seen.push(value);
}
return value;
});
if (res.length > MAX_LENGTH) {
return 'Output omitted for a large object that exceeds the limits';
}
return res;
} catch (error) {
return `Output omitted for an object that cannot be inspected ('${error.toString()}')`;
}
}
}