From ae4ffb1b81e706f286467a63af3aaa2c129b45be Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Thu, 11 Mar 2021 20:19:28 +0100 Subject: [PATCH] Handle extension test execution on the UI --- .../api/browser/mainThreadExtensionService.ts | 4 - .../workbench/api/common/extHost.protocol.ts | 3 +- .../api/common/extHostExtensionService.ts | 86 +++++++------------ .../common/abstractExtensionService.ts | 37 ++++++++ .../extensions/common/extensionHostManager.ts | 23 +++++ .../services/extensions/common/extensions.ts | 1 - 6 files changed, 92 insertions(+), 62 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 11cfcc3db8f..8bbedafd7ae 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -122,10 +122,6 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha } } - async $onExtensionHostExit(code: number): Promise { - this._extensionService._onExtensionHostExit(code); - } - async $setPerformanceMarks(marks: PerformanceMark[]): Promise { if (this._extensionHostKind === ExtensionHostKind.LocalProcess) { this._timerService.setPerformanceMarks('localExtHost', marks); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 1ee67c1484c..aaa24b06bd7 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -938,7 +938,6 @@ export interface MainThreadExtensionServiceShape extends IDisposable { $onDidActivateExtension(extensionId: ExtensionIdentifier, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationReason: ExtensionActivationReason): void; $onExtensionActivationError(extensionId: ExtensionIdentifier, error: ExtensionActivationError): Promise; $onExtensionRuntimeError(extensionId: ExtensionIdentifier, error: SerializedError): void; - $onExtensionHostExit(code: number): Promise; $setPerformanceMarks(marks: performance.PerformanceMark[]): Promise; } @@ -1228,6 +1227,8 @@ export type IResolveAuthorityResult = IResolveAuthorityErrorResult | IResolveAut export interface ExtHostExtensionServiceShape { $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise; $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise; + $extensionTestsExecute(): Promise; + $extensionTestsExit(code: number): Promise; $activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise; $activate(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise; $setRemoteEnvironment(env: { [key: string]: string | null; }): Promise; diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 547043aba3c..fcf05eadee2 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -547,7 +547,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme ); } - private _handleExtensionTests(): Promise { + public $extensionTestsExecute(): Promise { return this._doHandleExtensionTests().then(undefined, error => { console.error(error); // ensure any error message makes it onto the console @@ -555,76 +555,51 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme }); } - private async _doHandleExtensionTests(): Promise { + private async _doHandleExtensionTests(): Promise { const { extensionDevelopmentLocationURI, extensionTestsLocationURI } = this._initData.environment; - if (!(extensionDevelopmentLocationURI && extensionTestsLocationURI && extensionTestsLocationURI.scheme === Schemas.file)) { - return Promise.resolve(undefined); + if (!extensionDevelopmentLocationURI || !extensionTestsLocationURI || extensionTestsLocationURI.scheme !== Schemas.file) { + throw new Error(nls.localize('extensionTestError1', "Cannot load test runner.")); } const extensionTestsPath = originalFSPath(extensionTestsLocationURI); // Require the test runner via node require from the provided path - let testRunner: ITestRunner | INewTestRunner | undefined; - let requireError: Error | undefined; - try { - testRunner = await this._loadCommonJSModule(null, URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false)); - } catch (error) { - requireError = error; + const testRunner: ITestRunner | INewTestRunner | undefined = await this._loadCommonJSModule(null, URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false)); + + if (!testRunner || typeof testRunner.run !== 'function') { + throw new Error(nls.localize('extensionTestError', "Path {0} does not point to a valid extension test runner.", extensionTestsPath)); } // Execute the runner if it follows the old `run` spec - if (testRunner && typeof testRunner.run === 'function') { - return new Promise((c, e) => { - const oldTestRunnerCallback = (error: Error, failures: number | undefined) => { - if (error) { - e(error.toString()); - } else { - c(undefined); - } - - // after tests have run, we shutdown the host - this._testRunnerExit(error || (typeof failures === 'number' && failures > 0) ? 1 /* ERROR */ : 0 /* OK */); - }; - - const runResult = testRunner!.run(extensionTestsPath, oldTestRunnerCallback); - - // Using the new API `run(): Promise` - if (runResult && runResult.then) { - runResult - .then(() => { - c(); - this._testRunnerExit(0); - }) - .catch((err: Error) => { - e(err.toString()); - this._testRunnerExit(1); - }); + return new Promise((resolve, reject) => { + const oldTestRunnerCallback = (error: Error, failures: number | undefined) => { + if (error) { + reject(error); + } else { + resolve((typeof failures === 'number' && failures > 0) ? 1 /* ERROR */ : 0 /* OK */); } - }); - } + }; - // Otherwise make sure to shutdown anyway even in case of an error - else { - this._testRunnerExit(1 /* ERROR */); - } + const runResult = testRunner.run(extensionTestsPath, oldTestRunnerCallback); - return Promise.reject(new Error(requireError ? requireError.toString() : nls.localize('extensionTestError', "Path {0} does not point to a valid extension test runner.", extensionTestsPath))); + // Using the new API `run(): Promise` + if (runResult && runResult.then) { + runResult + .then(() => { + resolve(0); + }) + .catch((err: Error) => { + reject(err.toString()); + }); + } + }); } - private _testRunnerExit(code: number): void { + public async $extensionTestsExit(code: number): Promise { this._logService.info(`extension host terminating: test runner requested exit with code ${code}`); + this._logService.info(`exiting with code ${code}`); this._logService.flush(); - - // wait at most 5000ms for the renderer to confirm our exit request and for the renderer socket to drain - // (this is to ensure all outstanding messages reach the renderer) - const exitPromise = this._mainThreadExtensionsProxy.$onExtensionHostExit(code); - const drainPromise = this._extHostContext.drain(); - Promise.race([Promise.all([exitPromise, drainPromise]), timeout(5000)]).then(() => { - this._logService.info(`exiting with code ${code}`); - this._logService.flush(); - - this._hostUtils.exit(code); - }); + this._hostUtils.exit(code); } private _startExtensionHost(): Promise { @@ -636,7 +611,6 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return this._readyToStartExtensionHost.wait() .then(() => this._readyToRunExtensions.open()) .then(() => this._handleEagerExtensions()) - .then(() => this._handleExtensionTests()) .then(() => { this._logService.info(`eager extensions activated`); }); diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 1f47dc9a37b..33aaf286b57 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -420,6 +420,43 @@ export abstract class AbstractExtensionService extends Disposable implements IEx await this._scanAndHandleExtensions(); this._releaseBarrier(); perf.mark('code/didLoadExtensions'); + await this._handleExtensionTests(); + } + + private async _handleExtensionTests(): Promise { + if (!this._environmentService.extensionDevelopmentLocationURI || !this._environmentService.extensionTestsLocationURI) { + return; + } + + // TODO: Use `this._registry` and `this._runningLocation` to better determine which extension host should launch the test runner + let extensionHostManager: ExtensionHostManager | null = null; + if (this._environmentService.extensionTestsLocationURI.scheme === Schemas.vscodeRemote) { + extensionHostManager = this._getExtensionHostManager(ExtensionHostKind.Remote); + } + if (!extensionHostManager) { + // When a debugger attaches to the extension host, it will surface all console.log messages from the extension host, + // but not necessarily from the window. So it would be best if any errors get printed to the console of the extension host. + // That is why here we use the local process extension host even for non-file URIs + extensionHostManager = this._getExtensionHostManager(ExtensionHostKind.LocalProcess); + } + + if (!extensionHostManager) { + const msg = nls.localize('extensionTestError', "No extension host found that can launch the test runner at {0}.", this._environmentService.extensionTestsLocationURI.toString()); + console.error(msg); + this._notificationService.error(msg); + return; + } + + let exitCode: number; + try { + exitCode = await extensionHostManager.extensionTestsExecute(); + } catch (err) { + console.error(err); + exitCode = 1 /* ERROR */; + } + + await extensionHostManager.extensionTestsSendExit(exitCode); + this._onExtensionHostExit(exitCode); } private _releaseBarrier(): void { diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 51d14fbcf4b..41e5a222880 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -23,6 +23,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { IExtensionHost, ExtensionHostKind, ActivationKind } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { CATEGORIES } from 'vs/workbench/common/actions'; +import { timeout } from 'vs/base/common/async'; // Enable to see detailed message communication between window and extension host const LOG_EXTENSION_HOST_COMMUNICATION = false; @@ -292,6 +293,28 @@ export class ExtensionHostManager extends Disposable { return proxy.$startExtensionHost(enabledExtensionIds); } + public async extensionTestsExecute(): Promise { + const proxy = await this._getProxy(); + if (!proxy) { + throw new Error('Could not obtain Extension Host Proxy'); + } + return proxy.$extensionTestsExecute(); + } + + public async extensionTestsSendExit(exitCode: number): Promise { + const proxy = await this._getProxy(); + if (!proxy) { + return; + } + // This method does not wait for the actual RPC to be confirmed + // It waits for the socket to drain (i.e. the message has been sent) + // It also times out after 5s in case drain takes too long + proxy.$extensionTestsExit(exitCode); + if (this._rpcProtocol) { + await Promise.race([this._rpcProtocol.drain(), timeout(5000)]); + } + } + public async deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise { const proxy = await this._getProxy(); if (!proxy) { diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index ed224cb6eca..f08798dd813 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -253,7 +253,6 @@ export interface IExtensionService { _onWillActivateExtension(extensionId: ExtensionIdentifier): void; _onDidActivateExtension(extensionId: ExtensionIdentifier, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationReason: ExtensionActivationReason): void; _onExtensionRuntimeError(extensionId: ExtensionIdentifier, err: Error): void; - _onExtensionHostExit(code: number): void; } export interface ProfileSession {