From d6058d77c024eb64f29399d2c9385dd9549814fb Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 15 Mar 2019 10:19:21 +0100 Subject: [PATCH] Improve extension activation failure messages --- .../mainThreadExtensionService.ts | 80 ++++++++++++++++++- src/vs/workbench/api/node/extHost.protocol.ts | 4 +- .../api/node/extHostExtensionActivator.ts | 11 ++- .../api/node/extHostExtensionService.ts | 19 +---- .../services/extensions/common/extensions.ts | 5 ++ 5 files changed, 92 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/api/electron-browser/mainThreadExtensionService.ts b/src/vs/workbench/api/electron-browser/mainThreadExtensionService.ts index 21034b7ddc6..c6c87ec98b8 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadExtensionService.ts @@ -7,22 +7,38 @@ import { SerializedError } from 'vs/base/common/errors'; import Severity from 'vs/base/common/severity'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { IExtHostContext, MainContext, MainThreadExtensionServiceShape } from 'vs/workbench/api/node/extHost.protocol'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, ExtensionActivationError, IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionService } from 'vs/workbench/services/extensions/electron-browser/extensionService'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { localize } from 'vs/nls'; +import { Action } from 'vs/base/common/actions'; +import { EnablementState } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IWindowService } from 'vs/platform/windows/common/windows'; +import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; @extHostNamedCustomer(MainContext.MainThreadExtensionService) export class MainThreadExtensionService implements MainThreadExtensionServiceShape { private readonly _extensionService: ExtensionService; + private readonly _notificationService: INotificationService; + private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService; + private readonly _windowService: IWindowService; constructor( extHostContext: IExtHostContext, - @IExtensionService extensionService: IExtensionService + @IExtensionService extensionService: IExtensionService, + @INotificationService notificationService: INotificationService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @IWindowService windowService: IWindowService ) { if (extensionService instanceof ExtensionService) { this._extensionService = extensionService; } + this._notificationService = notificationService; + this._extensionsWorkbenchService = extensionsWorkbenchService; + this._windowService = windowService; } public dispose(): void { @@ -49,6 +65,64 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha console.error(`[${extensionId}]${error.message}`); console.error(error.stack); } - $onExtensionActivationFailed(extensionId: ExtensionIdentifier): void { + async $onExtensionActivationError(extensionId: ExtensionIdentifier, activationError: ExtensionActivationError): Promise { + if (typeof activationError === 'string') { + this._extensionService._logOrShowMessage(Severity.Error, activationError); + } else { + this._handleMissingDependency(extensionId, activationError.dependencies[0]); + } } + + private async _handleMissingDependency(extensionId: ExtensionIdentifier, missingDependency: string): Promise { + const extension = await this._extensionService.getExtension(extensionId.value); + const local = await this._extensionsWorkbenchService.queryLocal(); + const installedDependency = local.filter(i => areSameExtensions(i.identifier, { id: missingDependency }))[0]; + if (installedDependency) { + await this._handleMissingInstalledDependency(extension, installedDependency); + } else { + await this._handleMissingNotInstalledDependency(extension, missingDependency); + } + } + + private async _handleMissingInstalledDependency(extension: IExtensionDescription, missingInstalledDependency: IExtension): Promise { + const extName = extension.displayName || extension.name; + if (missingInstalledDependency.enablementState === EnablementState.Enabled || missingInstalledDependency.enablementState === EnablementState.WorkspaceEnabled) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('reload window', "Cannot activate extension '{0}' because it depends on extension '{1}', which is not loaded. Would you like to reload the window to load the extension?", extName, missingInstalledDependency.displayName), + actions: { + primary: [new Action('reload', localize('reload', "Reload Window"), '', true, () => this._windowService.reloadWindow())] + } + }); + } else { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('disabledDep', "Cannot activate extension '{0}' because it depends on extension '{1}', which is disabled. Would you like to enable the extension and reload the window?", extName, missingInstalledDependency.displayName), + actions: { + primary: [new Action('enable', localize('enable dep', "Enable and Reload"), '', true, + () => this._extensionsWorkbenchService.setEnablement([missingInstalledDependency], missingInstalledDependency.enablementState === EnablementState.Disabled ? EnablementState.Enabled : EnablementState.WorkspaceEnabled) + .then(() => this._windowService.reloadWindow(), e => this._notificationService.error(e)))] + } + }); + } + } + + private async _handleMissingNotInstalledDependency(extension: IExtensionDescription, missingDependency: string): Promise { + const extName = extension.displayName || extension.name; + const dependencyExtension = (await this._extensionsWorkbenchService.queryGallery({ names: [missingDependency] })).firstPage[0]; + if (dependencyExtension) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('uninstalledDep', "Cannot activate extension '{0}' because it depends on extension '{1}', which is not installed. Would you like to install the extension and reload the window?", extName, dependencyExtension.displayName), + actions: { + primary: [new Action('install', localize('install missing dep', "Install and Reload"), '', true, + () => this._extensionsWorkbenchService.install(dependencyExtension) + .then(() => this._windowService.reloadWindow(), e => this._notificationService.error(e)))] + } + }); + } else { + this._notificationService.error(localize('unknownDep', "Cannot activate extension '{0}' because it depends on an unknown extension '{1}'.", extName, missingDependency)); + } + } + } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 2894f86134d..7971709e39a 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -36,7 +36,7 @@ import { ITreeItem, IRevealOptions } from 'vs/workbench/common/views'; import { IAdapterDescriptor, IConfig, ITerminalSettings } from 'vs/workbench/contrib/debug/common/debug'; import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; import { ITerminalDimensions } from 'vs/workbench/contrib/terminal/common/terminal'; -import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionDescription, ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; import { IRPCProtocol, createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId } from 'vs/workbench/services/extensions/node/proxyIdentifier'; import { IProgressOptions, IProgressStep } from 'vs/platform/progress/common/progress'; import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; @@ -575,7 +575,7 @@ export interface MainThreadExtensionServiceShape extends IDisposable { $activateExtension(extensionId: ExtensionIdentifier, activationEvent: string | null): Promise; $onWillActivateExtension(extensionId: ExtensionIdentifier): void; $onDidActivateExtension(extensionId: ExtensionIdentifier, startup: boolean, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationEvent: string | null): void; - $onExtensionActivationFailed(extensionId: ExtensionIdentifier): void; + $onExtensionActivationError(extensionId: ExtensionIdentifier, error: ExtensionActivationError): Promise; $onExtensionRuntimeError(extensionId: ExtensionIdentifier, error: SerializedError): void; } diff --git a/src/vs/workbench/api/node/extHostExtensionActivator.ts b/src/vs/workbench/api/node/extHostExtensionActivator.ts index a248e0e9f69..15cac8c86b9 100644 --- a/src/vs/workbench/api/node/extHostExtensionActivator.ts +++ b/src/vs/workbench/api/node/extHostExtensionActivator.ts @@ -5,9 +5,9 @@ import * as nls from 'vs/nls'; import { IDisposable } from 'vs/base/common/lifecycle'; -import Severity from 'vs/base/common/severity'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ExtensionActivationError, MissingDependenciesError } from 'vs/workbench/services/extensions/common/extensions'; const NO_OP_VOID_PROMISE = Promise.resolve(undefined); @@ -173,8 +173,7 @@ export class FailedExtension extends ActivatedExtension { } export interface IExtensionsActivatorHost { - showMessage(severity: Severity, message: string): void; - + onExtensionActivationError(extensionId: ExtensionIdentifier, error: ExtensionActivationError): void; actualActivateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise; } @@ -283,7 +282,7 @@ export class ExtensionsActivator { if (dep && dep.activationFailed) { // Error condition 2: a dependency has already failed activation - this._host.showMessage(Severity.Error, nls.localize('failedDep1', "Cannot activate extension '{0}' because it depends on extension '{1}', which failed to activate.", currentExtension.displayName || currentExtension.identifier.value, depId)); + this._host.onExtensionActivationError(currentExtension.identifier, nls.localize('failedDep1', "Cannot activate extension '{0}' because it depends on extension '{1}', which failed to activate.", currentExtension.displayName || currentExtension.identifier.value, depId)); const error = new Error(`Dependency ${depId} failed to activate`); (error).detail = dep.activationFailedError; this._activatedExtensions.set(ExtensionIdentifier.toKey(currentExtension.identifier), new FailedExtension(error)); @@ -306,7 +305,7 @@ export class ExtensionsActivator { } // Error condition 1: unknown dependency - this._host.showMessage(Severity.Error, nls.localize('unknownDep', "Cannot activate extension '{0}' because it depends on extension '{1}', which is not installed or disabled. Please install or enable '{1}' and reload the window.", currentExtension.displayName || currentExtension.identifier.value, depId)); + this._host.onExtensionActivationError(currentExtension.identifier, new MissingDependenciesError([depId])); const error = new Error(`Unknown dependency '${depId}'`); this._activatedExtensions.set(ExtensionIdentifier.toKey(currentExtension.identifier), new FailedExtension(error)); return; @@ -373,7 +372,7 @@ export class ExtensionsActivator { } const newlyActivatingExtension = this._host.actualActivateExtension(extensionId, reason).then(undefined, (err) => { - this._host.showMessage(Severity.Error, nls.localize('activationError', "Activating extension '{0}' failed: {1}.", extensionId.value, err.message)); + this._host.onExtensionActivationError(extensionId, nls.localize('activationError', "Activating extension '{0}' failed: {1}.", extensionId.value, err.message)); console.error('Activating extension `' + extensionId.value + '` failed: ', err.message); console.log('Here is the error stack: ', err.stack); // Treat the extension as being empty diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 9510f14cca7..b4218211644 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -9,7 +9,6 @@ import { originalFSPath } from 'vs/base/common/resources'; import { Barrier } from 'vs/base/common/async'; import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; -import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import * as pfs from 'vs/base/node/pfs'; import { ILogService } from 'vs/platform/log/common/log'; @@ -20,7 +19,7 @@ import { ActivatedExtension, EmptyExtension, ExtensionActivatedByAPI, ExtensionA import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; import { ExtHostStorage } from 'vs/workbench/api/node/extHostStorage'; import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; -import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionDescription, ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry'; import { connectProxyResolver } from 'vs/workbench/services/extensions/node/proxyResolver'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -208,19 +207,8 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { initData.hostExtensions.forEach((extensionId) => hostExtensions.add(ExtensionIdentifier.toKey(extensionId))); this._activator = new ExtensionsActivator(this._registry, initData.resolvedExtensions, initData.hostExtensions, { - showMessage: (severity: Severity, message: string): void => { - this._mainThreadExtensionsProxy.$localShowMessage(severity, message); - - switch (severity) { - case Severity.Error: - console.error(message); - break; - case Severity.Warning: - console.warn(message); - break; - default: - console.log(message); - } + onExtensionActivationError: (extensionId: ExtensionIdentifier, error: ExtensionActivationError): void => { + this._mainThreadExtensionsProxy.$onExtensionActivationError(extensionId, error); }, actualActivateExtension: async (extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise => { @@ -383,7 +371,6 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { this._logExtensionActivationTimes(extensionDescription, reason, 'success', activationTimes); return activatedExtension; }, (err) => { - this._mainThreadExtensionsProxy.$onExtensionActivationFailed(extensionDescription.identifier); this._logExtensionActivationTimes(extensionDescription, reason, 'failure'); throw err; }); diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index e06bea02144..7c864520b92 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -46,6 +46,11 @@ export interface IExtensionsStatus { runtimeErrors: Error[]; } +export type ExtensionActivationError = string | MissingDependenciesError; +export class MissingDependenciesError { + constructor(readonly dependencies: string[]) { } +} + /** * e.g. * ```