From 82b4d9ae4e6c48793d40b3dab15ec721d02a9e77 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 22 Jun 2021 15:26:42 +0200 Subject: [PATCH] FIx #122448 --- .../extensions/browser/extensionsActions.ts | 5 + .../extensionsWorkbenchService.test.ts | 30 ++- .../browser/extensionEnablementService.ts | 254 +++++++++++++----- .../common/extensionManagement.ts | 26 +- .../common/extensionManagementService.ts | 20 +- .../extensionEnablementService.test.ts | 122 +++++++-- .../common/abstractExtensionService.ts | 56 ++-- 7 files changed, 368 insertions(+), 145 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 94a81c4d96574..57febf2ea9610 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -2129,6 +2129,11 @@ export class SystemDisabledWarningAction extends ExtensionAction { return; } } + if (this.extension.enablementState === EnablementState.DisabledByExtensionDependency) { + this.class = `${SystemDisabledWarningAction.WARNING_CLASS}`; + this.tooltip = localize('extension disabled because of dependency', "This extension has been disabled because it depends on an extension that is disabled."); + return; + } if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { if (isLanguagePackExtension(this.extension.local.manifest)) { if (!this.extensionsWorkbenchService.installed.some(e => areSameExtensions(e.identifier, this.extension!.identifier) && e.server !== this.extension!.server)) { diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 33cd340d10fba..5bbe38465028a 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -101,11 +101,11 @@ suite('ExtensionsWorkbenchServiceTest', () => { } }); - instantiationService.stub(IExtensionManagementServerService, { - localExtensionManagementServer: { - extensionManagementService: instantiationService.get(IExtensionManagementService) - } - }); + instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService({ + id: 'local', + label: 'local', + extensionManagementService: instantiationService.get(IExtensionManagementService) + }, null, null)); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); @@ -121,7 +121,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', []); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage()); instantiationService.stubPromise(INotificationService, 'prompt', 0); - await (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); + (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); }); teardown(() => { @@ -1449,6 +1449,24 @@ suite('ExtensionsWorkbenchServiceTest', () => { }); } + function anExtensionManagementServerService(localExtensionManagementServer: IExtensionManagementServer | null, remoteExtensionManagementServer: IExtensionManagementServer | null, webExtensionManagementServer: IExtensionManagementServer | null): IExtensionManagementServerService { + return { + _serviceBrand: undefined, + localExtensionManagementServer, + remoteExtensionManagementServer, + webExtensionManagementServer, + getExtensionManagementServer: (extension: IExtension) => { + if (extension.location.scheme === Schemas.file) { + return localExtensionManagementServer; + } + if (extension.location.scheme === Schemas.vscodeRemote) { + return remoteExtensionManagementServer; + } + return webExtensionManagementServer; + } + }; + } + function aMultiExtensionManagementServerService(instantiationService: TestInstantiationService, localExtensionManagementService?: IExtensionManagementService, remoteExtensionManagementService?: IExtensionManagementService): IExtensionManagementServerService { const localExtensionManagementServer: IExtensionManagementServer = { id: 'vscode-local', diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 521ab664f825d..d85d771cc70a0 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -5,10 +5,10 @@ import { localize } from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { IExtensionManagementService, DidUninstallExtensionEvent, IExtensionIdentifier, IGlobalExtensionEnablementService, ENABLED_EXTENSIONS_STORAGE_PATH, DISABLED_EXTENSIONS_STORAGE_PATH, DidInstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IExtensionManagementService, IExtensionIdentifier, IGlobalExtensionEnablementService, ENABLED_EXTENSIONS_STORAGE_PATH, DISABLED_EXTENSIONS_STORAGE_PATH } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IWorkbenchExtensionManagementService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { areSameExtensions, BetterMergeId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -27,6 +27,8 @@ import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from import { Promises } from 'vs/base/common/async'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; const SOURCE = 'IWorkbenchExtensionEnablementService'; @@ -37,6 +39,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench private readonly _onEnablementChanged = new Emitter(); public readonly onEnablementChanged: Event = this._onEnablementChanged.event; + protected readonly extensionsManager: ExtensionsManager; private readonly storageManger: StorageManager; constructor( @@ -44,7 +47,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench @IGlobalExtensionEnablementService protected readonly globalExtensionEnablementService: IGlobalExtensionEnablementService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, @@ -56,12 +59,23 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, + @IInstantiationService instantiationService: IInstantiationService, ) { super(); this.storageManger = this._register(new StorageManager(storageService)); - this._register(this.globalExtensionEnablementService.onDidChangeEnablement(({ extensions, source }) => this.onDidChangeExtensions(extensions, source))); - this._register(extensionManagementService.onDidInstallExtension(this._onDidInstallExtension, this)); - this._register(extensionManagementService.onDidUninstallExtension(this._onDidUninstallExtension, this)); + + const uninstallDisposable = this._register(Event.filter(extensionManagementService.onDidUninstallExtension, e => !e.error)(({ identifier }) => this._reset(identifier))); + let isDisposed = false; + this._register(toDisposable(() => isDisposed = true)); + this.extensionsManager = this._register(instantiationService.createInstance(ExtensionsManager)); + this.extensionsManager.whenInitialized().then(() => { + if (!isDisposed) { + this._register(this.extensionsManager.onDidChangeExtensions(({ added, removed }) => this._onDidChangeExtensions(added, removed))); + uninstallDisposable.dispose(); + } + }); + + this._register(this.globalExtensionEnablementService.onDidChangeEnablement(({ extensions, source }) => this._onDidChangeGloballyDisabledExtensions(extensions, source))); // delay notification for extensions disabled until workbench restored if (this.allUserExtensionsDisabled) { @@ -83,22 +97,12 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } getEnablementState(extension: IExtension): EnablementState { - if (this.extensionBisectService.isDisabledByBisect(extension)) { - return EnablementState.DisabledByEnvironment; - } - if (this._isDisabledInEnv(extension)) { - return EnablementState.DisabledByEnvironment; - } - if (this._isDisabledByVirtualWorkspace(extension)) { - return EnablementState.DisabledByVirtualWorkspace; - } - if (this._isDisabledByExtensionKind(extension)) { - return EnablementState.DisabledByExtensionKind; - } - if (this._isEnabled(extension) && this._isDisabledByWorkspaceTrust(extension)) { - return EnablementState.DisabledByTrustRequirement; - } - return this._getEnablementState(extension.identifier); + return this._computeEnablementState(extension, this.extensionsManager.extensions); + } + + getEnablementStates(extensions: IExtension[]): EnablementState[] { + const extensionsEnablements = new Map(); + return extensions.map(extension => this._computeEnablementState(extension, extensions, extensionsEnablements)); } canChangeEnablement(extension: IExtension): boolean { @@ -110,23 +114,13 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench const enablementState = this.getEnablementState(extension); if (enablementState === EnablementState.DisabledByEnvironment || enablementState === EnablementState.DisabledByVirtualWorkspace + || enablementState === EnablementState.DisabledByExtensionDependency || enablementState === EnablementState.DisabledByExtensionKind) { return false; } return true; } - private throwErrorIfCannotChangeEnablement(extension: IExtension): void { - if (isLanguagePackExtension(extension.manifest)) { - throw new Error(localize('cannot disable language pack extension', "Cannot change enablement of {0} extension because it contributes language packs.", extension.manifest.displayName || extension.identifier.id)); - } - - if (this.userDataAutoSyncEnablementService.isEnabled() && this.userDataSyncAccountService.account && - isAuthenticaionProviderExtension(extension.manifest) && extension.manifest.contributes!.authentication!.some(a => a.id === this.userDataSyncAccountService.account!.authenticationProviderId)) { - throw new Error(localize('cannot disable auth extension', "Cannot change enablement {0} extension because Settings Sync depends on it.", extension.manifest.displayName || extension.identifier.id)); - } - } - canChangeWorkspaceEnablement(extension: IExtension): boolean { if (!this.canChangeEnablement(extension)) { return false; @@ -139,6 +133,17 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return true; } + private throwErrorIfCannotChangeEnablement(extension: IExtension): void { + if (isLanguagePackExtension(extension.manifest)) { + throw new Error(localize('cannot disable language pack extension', "Cannot change enablement of {0} extension because it contributes language packs.", extension.manifest.displayName || extension.identifier.id)); + } + + if (this.userDataAutoSyncEnablementService.isEnabled() && this.userDataSyncAccountService.account && + isAuthenticaionProviderExtension(extension.manifest) && extension.manifest.contributes!.authentication!.some(a => a.id === this.userDataSyncAccountService.account!.authenticationProviderId)) { + throw new Error(localize('cannot disable auth extension', "Cannot change enablement {0} extension because Settings Sync depends on it.", extension.manifest.displayName || extension.identifier.id)); + } + } + private throwErrorIfCannotChangeWorkspaceEnablement(extension: IExtension): void { if (!this.hasWorkspace) { throw new Error(localize('noWorkspace', "No workspace.")); @@ -159,14 +164,12 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } - const result = await Promises.settled(extensions.map(e => { - if (this._isDisabledByWorkspaceTrust(e)) { - return this.workspaceTrustRequestService.requestWorkspaceTrust() - .then(trustState => { - return Promise.resolve(trustState ?? false); - }); + const result = await Promises.settled(extensions.map(async e => { + if (this.getEnablementState(e) === EnablementState.DisabledByTrustRequirement) { + const trustState = await this.workspaceTrustRequestService.requestWorkspaceTrust(); + return trustState ?? false; } else { - return this._setEnablement(e, newState); + return this._setUserEnablementState(e, newState); } })); @@ -177,9 +180,9 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return result; } - private _setEnablement(extension: IExtension, newState: EnablementState): Promise { + private _setUserEnablementState(extension: IExtension, newState: EnablementState): Promise { - const currentState = this._getEnablementState(extension.identifier); + const currentState = this._getUserEnablementState(extension.identifier); if (currentState === newState) { return Promise.resolve(false); @@ -205,11 +208,10 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench isEnabled(extension: IExtension): boolean { const enablementState = this.getEnablementState(extension); - return enablementState === EnablementState.EnabledWorkspace || enablementState === EnablementState.EnabledGlobally; + return this._isEnabledEnablementState(enablementState); } - private _isEnabled(extension: IExtension): boolean { - const enablementState = this._getEnablementState(extension.identifier); + private _isEnabledEnablementState(enablementState: EnablementState): boolean { return enablementState === EnablementState.EnabledWorkspace || enablementState === EnablementState.EnabledGlobally; } @@ -217,14 +219,60 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return this._isDisabledGlobally(extension.identifier); } + private _computeEnablementState(extension: IExtension, extensions: ReadonlyArray, computedEnablementStates?: Map): EnablementState { + computedEnablementStates = computedEnablementStates ?? new Map(); + let enablementState = computedEnablementStates.get(extension); + if (enablementState !== undefined) { + return enablementState; + } + + if (this.extensionBisectService.isDisabledByBisect(extension)) { + enablementState = EnablementState.DisabledByEnvironment; + } + + else if (this._isDisabledInEnv(extension)) { + enablementState = EnablementState.DisabledByEnvironment; + } + + else if (this._isDisabledByVirtualWorkspace(extension)) { + enablementState = EnablementState.DisabledByVirtualWorkspace; + } + + else if (this._isDisabledByExtensionKind(extension)) { + enablementState = EnablementState.DisabledByExtensionKind; + } + + else { + enablementState = this._getUserEnablementState(extension.identifier); + if (this._isEnabledEnablementState(enablementState)) { + if (this._isDisabledByWorkspaceTrust(extension)) { + enablementState = EnablementState.DisabledByTrustRequirement; + } + if (this._isDisabledByExtensionDependency(extension, extensions, computedEnablementStates)) { + enablementState = EnablementState.DisabledByExtensionDependency; + } + } + } + + computedEnablementStates.set(extension, enablementState); + return enablementState; + } + private _isDisabledInEnv(extension: IExtension): boolean { if (this.allUserExtensionsDisabled) { return !extension.isBuiltin; } + const disabledExtensions = this.environmentService.disableExtensions; if (Array.isArray(disabledExtensions)) { return disabledExtensions.some(id => areSameExtensions({ id }, extension.identifier)); } + + // Check if this is the better merge extension which was migrated to a built-in extension + if (areSameExtensions({ id: BetterMergeId.value }, extension.identifier)) { + return true; + } + return false; } @@ -268,19 +316,47 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return false; } - isDisabledByWorkspaceTrust(extension: IExtension): boolean { - return this._isEnabled(extension) && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.manifest) === false; - } - private _isDisabledByWorkspaceTrust(extension: IExtension): boolean { if (this.workspaceTrustManagementService.isWorkpaceTrusted()) { return false; } - return this.isDisabledByWorkspaceTrust(extension); + return this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.manifest) === false; } - private _getEnablementState(identifier: IExtensionIdentifier): EnablementState { + private _isDisabledByExtensionDependency(extension: IExtension, extensions: ReadonlyArray, computedEnablementStates: Map): boolean { + // Find dependencies from the same server as of the extension + const dependencyExtensions = extension.manifest.extensionDependencies + ? extensions.filter(e => + extension.manifest.extensionDependencies!.some(id => areSameExtensions(e.identifier, { id }) && this.extensionManagementServerService.getExtensionManagementServer(e) === this.extensionManagementServerService.getExtensionManagementServer(extension))) + : []; + + if (!dependencyExtensions.length) { + return false; + } + + const hasEnablementState = computedEnablementStates.has(extension); + if (!hasEnablementState) { + // Placeholder to handle cyclic deps + computedEnablementStates.set(extension, EnablementState.EnabledGlobally); + } + try { + for (const dependencyExtension of dependencyExtensions) { + if (!this._isEnabledEnablementState(this._computeEnablementState(dependencyExtension, extensions, computedEnablementStates))) { + return true; + } + } + } finally { + if (!hasEnablementState) { + // remove the placeholder + computedEnablementStates.delete(extension); + } + } + + return false; + } + + private _getUserEnablementState(identifier: IExtensionIdentifier): EnablementState { if (this.hasWorkspace) { if (this._getWorkspaceEnabledExtensions().filter(e => areSameExtensions(e, identifier))[0]) { return EnablementState.EnabledWorkspace; @@ -407,29 +483,25 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench this.storageManger.set(storageId, extensions, StorageScope.WORKSPACE); } - private async onDidChangeExtensions(extensionIdentifiers: ReadonlyArray, source?: string): Promise { + private async _onDidChangeGloballyDisabledExtensions(extensionIdentifiers: ReadonlyArray, source?: string): Promise { if (source !== SOURCE) { - const installedExtensions = await this.extensionManagementService.getInstalled(); - const extensions = installedExtensions.filter(installedExtension => extensionIdentifiers.some(identifier => areSameExtensions(identifier, installedExtension.identifier))); + await this.extensionsManager.whenInitialized(); + const extensions = this.extensionsManager.extensions.filter(installedExtension => extensionIdentifiers.some(identifier => areSameExtensions(identifier, installedExtension.identifier))); this._onEnablementChanged.fire(extensions); } } - private _onDidInstallExtension({ local, error }: DidInstallExtensionEvent): void { - if (local && !error && this._isDisabledByWorkspaceTrust(local)) { - this._onEnablementChanged.fire([local]); - } - } - - private _onDidUninstallExtension({ identifier, error }: DidUninstallExtensionEvent): void { - if (!error) { - this._reset(identifier); + private _onDidChangeExtensions(added: ReadonlyArray, removed: ReadonlyArray): void { + const disabledByTrustExtensions = added.filter(e => this.getEnablementState(e) === EnablementState.DisabledByTrustRequirement); + if (disabledByTrustExtensions.length) { + this._onEnablementChanged.fire(disabledByTrustExtensions); } + removed.forEach(({ identifier }) => this._reset(identifier)); } public async updateEnablementByWorkspaceTrustRequirement(): Promise { - const installedExtensions = await this.extensionManagementService.getInstalled(); - const disabledExtensions = installedExtensions.filter(e => this.isDisabledByWorkspaceTrust(e)); + await this.extensionsManager.whenInitialized(); + const disabledExtensions = this.extensionsManager.extensions.filter(e => this.getEnablementState(e) === EnablementState.DisabledByTrustRequirement); if (disabledExtensions.length) { this._onEnablementChanged.fire(disabledExtensions); @@ -443,4 +515,52 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } +class ExtensionsManager extends Disposable { + + private _extensions: IExtension[] = []; + get extensions(): readonly IExtension[] { return this._extensions; } + + private _onDidChangeExtensions = this._register(new Emitter<{ added: readonly IExtension[], removed: readonly IExtension[] }>()); + readonly onDidChangeExtensions = this._onDidChangeExtensions.event; + + private readonly initializePromise; + + constructor( + @IWorkbenchExtensionManagementService private readonly extensionManagementService: IWorkbenchExtensionManagementService, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, + @ILogService private readonly logService: ILogService + ) { + super(); + this.initializePromise = this.initialize(); + } + + whenInitialized(): Promise { + return this.initializePromise; + } + + private async initialize(): Promise { + try { + this._extensions = await this.extensionManagementService.getInstalled(); + this._onDidChangeExtensions.fire({ added: this.extensions, removed: [] }); + } catch (error) { + this.logService.error(error); + } + Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.local))(e => this.onDidInstallExtension(e.local!)); + Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error))(e => this.onDidUninstallExtension(e.identifier, e.server)); + } + + private onDidInstallExtension(extension: IExtension): void { + this._extensions.push(extension); + this._onDidChangeExtensions.fire({ added: [extension], removed: [] }); + } + + private onDidUninstallExtension(identifier: IExtensionIdentifier, server: IExtensionManagementServer): void { + const index = this._extensions.findIndex(e => areSameExtensions(e.identifier, identifier) && this.extensionManagementServerService.getExtensionManagementServer(e) === server); + if (index !== -1) { + const removed = this._extensions.splice(index, 1); + this._onDidChangeExtensions.fire({ added: [], removed }); + } + } +} + registerSingleton(IWorkbenchExtensionEnablementService, ExtensionEnablementService); diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index be964909da973..4ef42e3b18649 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { createDecorator, refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtension, ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier, ILocalExtension, InstallOptions, InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; export interface IExtensionManagementServer { @@ -24,9 +24,20 @@ export interface IExtensionManagementServerService { getExtensionManagementServer(extension: IExtension): IExtensionManagementServer | null; } +export type InstallExtensionOnServerEvent = InstallExtensionEvent & { server: IExtensionManagementServer }; +export type DidInstallExtensionOnServerEvent = DidInstallExtensionEvent & { server: IExtensionManagementServer }; +export type UninstallExtensionOnServerEvent = IExtensionIdentifier & { server: IExtensionManagementServer }; +export type DidUninstallExtensionOnServerEvent = DidUninstallExtensionEvent & { server: IExtensionManagementServer }; + export const IWorkbenchExtensionManagementService = refineServiceDecorator(IExtensionManagementService); export interface IWorkbenchExtensionManagementService extends IExtensionManagementService { readonly _serviceBrand: undefined; + + onInstallExtension: Event; + onDidInstallExtension: Event; + onUninstallExtension: Event; + onDidUninstallExtension: Event; + installWebExtension(location: URI): Promise; installExtensions(extensions: IGalleryExtension[], installOptions?: InstallOptions): Promise; updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension): Promise; @@ -38,6 +49,7 @@ export const enum EnablementState { DisabledByExtensionKind, DisabledByEnvironment, DisabledByVirtualWorkspace, + DisabledByExtensionDependency, DisabledGlobally, DisabledWorkspace, EnabledGlobally, @@ -59,6 +71,12 @@ export interface IWorkbenchExtensionEnablementService { */ getEnablementState(extension: IExtension): EnablementState; + /** + * Returns the enablement states for the given extensions + * @param extensions list of extensions + */ + getEnablementStates(extensions: IExtension[]): EnablementState[]; + /** * Returns `true` if the enablement can be changed. */ @@ -82,12 +100,6 @@ export interface IWorkbenchExtensionEnablementService { */ isDisabledGlobally(extension: IExtension): boolean; - /** - * Returns `true` if the given extension identifier is enabled by the user but it it - * disabled due to the fact that the current window/folder/workspace is not trusted. - */ - isDisabledByWorkspaceTrust(extension: IExtension): boolean; - /** * Enable or disable the given extension. * if `workspace` is `true` then enablement is done for workspace, otherwise globally. diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 785cc92b06271..7ea6c41e16901 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -5,9 +5,9 @@ import { Event, EventMultiplexer } from 'vs/base/common/event'; import { - ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, INSTALL_ERROR_NOT_SUPPORTED, InstallVSIXOptions + ILocalExtension, IGalleryExtension, IExtensionIdentifier, IReportedExtension, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, INSTALL_ERROR_NOT_SUPPORTED, InstallVSIXOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IExtensionManagementServer, IExtensionManagementServerService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { DidInstallExtensionOnServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -31,10 +31,10 @@ export class ExtensionManagementService extends Disposable implements IWorkbench declare readonly _serviceBrand: undefined; - readonly onInstallExtension: Event; - readonly onDidInstallExtension: Event; - readonly onUninstallExtension: Event; - readonly onDidUninstallExtension: Event; + readonly onInstallExtension: Event; + readonly onDidInstallExtension: Event; + readonly onUninstallExtension: Event; + readonly onDidUninstallExtension: Event; protected readonly servers: IExtensionManagementServer[] = []; @@ -61,10 +61,10 @@ export class ExtensionManagementService extends Disposable implements IWorkbench this.servers.push(this.extensionManagementServerService.webExtensionManagementServer); } - this.onInstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onInstallExtension); return emitter; }, new EventMultiplexer())).event; - this.onDidInstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onDidInstallExtension); return emitter; }, new EventMultiplexer())).event; - this.onUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onUninstallExtension); return emitter; }, new EventMultiplexer())).event; - this.onDidUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onDidUninstallExtension); return emitter; }, new EventMultiplexer())).event; + this.onInstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(Event.map(server.extensionManagementService.onInstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer())).event; + this.onDidInstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(Event.map(server.extensionManagementService.onDidInstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer())).event; + this.onUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(Event.map(server.extensionManagementService.onUninstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer())).event; + this.onDidUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(Event.map(server.extensionManagementService.onDidUninstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer())).event; } async getInstalled(type?: ExtensionType): Promise { diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 1318c3f2d1aa7..1f138c595e537 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -4,15 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtension, DidInstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtension, DidInstallExtensionEvent, InstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionEnablementService } from 'vs/workbench/services/extensionManagement/browser/extensionEnablementService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { Emitter } from 'vs/base/common/event'; import { IWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; -import { IExtensionContributions, ExtensionType, IExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { IExtensionContributions, ExtensionType, IExtension, IExtensionManifest, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -34,6 +34,7 @@ import { TestWorkspaceTrustManagementService } from 'vs/workbench/services/works import { ExtensionManifestPropertiesService, IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { TestContextService, TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { ExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagementService'; function createStorageService(instantiationService: TestInstantiationService): IStorageService { let service = instantiationService.get(IStorageService); @@ -53,15 +54,25 @@ function createStorageService(instantiationService: TestInstantiationService): I export class TestExtensionEnablementService extends ExtensionEnablementService { constructor(instantiationService: TestInstantiationService) { const storageService = createStorageService(instantiationService); - const extensionManagementService = instantiationService.get(IExtensionManagementService) || instantiationService.stub(IExtensionManagementService, { onDidInstallExtension: new Emitter().event, onDidUninstallExtension: new Emitter().event } as IExtensionManagementService); - const extensionManagementServerService = instantiationService.get(IExtensionManagementServerService) || instantiationService.stub(IExtensionManagementServerService, { localExtensionManagementServer: { extensionManagementService } }); + const extensionManagementServerService = instantiationService.get(IExtensionManagementServerService) || + instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService({ + id: 'local', + label: 'local', + extensionManagementService: { + onInstallExtension: new Emitter().event, + onDidInstallExtension: new Emitter().event, + onUninstallExtension: new Emitter().event, + onDidUninstallExtension: new Emitter().event, + } + }, null, null)); + const workbenchExtensionManagementService = instantiationService.get(IWorkbenchExtensionManagementService) || instantiationService.stub(IWorkbenchExtensionManagementService, instantiationService.createInstance(ExtensionManagementService)); const workspaceTrustManagementService = instantiationService.get(IWorkspaceTrustManagementService) || instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); super( storageService, new GlobalExtensionEnablementService(storageService), instantiationService.get(IWorkspaceContextService) || new TestContextService(), instantiationService.get(IWorkbenchEnvironmentService) || instantiationService.stub(IWorkbenchEnvironmentService, { configuration: Object.create(null) } as IWorkbenchEnvironmentService), - extensionManagementService, + workbenchExtensionManagementService, instantiationService.get(IConfigurationService), extensionManagementServerService, instantiationService.get(IUserDataAutoSyncEnablementService) || instantiationService.stub(IUserDataAutoSyncEnablementService, >{ isEnabled() { return false; } }), @@ -72,10 +83,15 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { new class extends mock() { override isDisabledByBisect() { return false; } }, workspaceTrustManagementService, new class extends mock() { override requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise { return Promise.resolve(true); } }, - instantiationService.get(IExtensionManifestPropertiesService) || instantiationService.stub(IExtensionManifestPropertiesService, new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService(), workspaceTrustManagementService)) + instantiationService.get(IExtensionManifestPropertiesService) || instantiationService.stub(IExtensionManifestPropertiesService, new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService(), workspaceTrustManagementService)), + instantiationService ); } + public async waitUntilInitialized(): Promise { + await this.extensionsManager.whenInitialized(); + } + public reset(): void { let extensions = this.globalExtensionEnablementService.getDisabledExtensions(); for (const e of this._getWorkspaceDisabledExtensions()) { @@ -98,20 +114,22 @@ suite('ExtensionEnablementService Test', () => { const didInstallEvent = new Emitter(); const didUninstallEvent = new Emitter(); + const installed: ILocalExtension[] = []; setup(() => { + installed.splice(0, installed.length); instantiationService = new TestInstantiationService(); instantiationService.stub(IConfigurationService, new TestConfigurationService()); - instantiationService.stub(IExtensionManagementService, >{ - onDidInstallExtension: didInstallEvent.event, - onDidUninstallExtension: didUninstallEvent.event, - getInstalled: () => Promise.resolve([] as ILocalExtension[]) - }); - instantiationService.stub(IExtensionManagementServerService, { - localExtensionManagementServer: { - extensionManagementService: instantiationService.get(IExtensionManagementService) + instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService({ + id: 'local', + label: 'local', + extensionManagementService: { + onDidInstallExtension: didInstallEvent.event, + onDidUninstallExtension: didUninstallEvent.event, + getInstalled: () => Promise.resolve(installed) } - }); + }, null, null)); + instantiationService.stub(IWorkbenchExtensionManagementService, instantiationService.createInstance(ExtensionManagementService)); testObject = new TestExtensionEnablementService(instantiationService); }); @@ -131,14 +149,12 @@ suite('ExtensionEnablementService Test', () => { .then(value => assert.ok(value)); }); - test('test disable an extension globally triggers the change event', () => { + test('test disable an extension globally triggers the change event', async () => { const target = sinon.spy(); testObject.onEnablementChanged(target); - return testObject.setEnablement([aLocalExtension('pub.a')], EnablementState.DisabledGlobally) - .then(() => { - assert.ok(target.calledOnce); - assert.deepStrictEqual((target.args[0][0][0]).identifier, { id: 'pub.a' }); - }); + await testObject.setEnablement([aLocalExtension('pub.a')], EnablementState.DisabledGlobally); + assert.ok(target.calledOnce); + assert.deepStrictEqual((target.args[0][0][0]).identifier, { id: 'pub.a' }); }); test('test disable an extension globally again should return a falsy promise', () => { @@ -370,9 +386,13 @@ suite('ExtensionEnablementService Test', () => { test('test remove an extension from disablement list when uninstalled', async () => { const extension = aLocalExtension('pub.a'); + installed.push(extension); + testObject = new TestExtensionEnablementService(instantiationService); + await testObject.setEnablement([extension], EnablementState.DisabledWorkspace); await testObject.setEnablement([extension], EnablementState.DisabledGlobally); didUninstallEvent.fire({ identifier: { id: 'pub.a' } }); + assert.ok(testObject.isEnabled(extension)); assert.strictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); }); @@ -467,13 +487,11 @@ suite('ExtensionEnablementService Test', () => { test('test extension is disabled when disabled in environment', async () => { const extension = aLocalExtension('pub.a'); + installed.push(extension); + instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService); - instantiationService.stub(IExtensionManagementService, >{ - onDidInstallExtension: didInstallEvent.event, - onDidUninstallExtension: didUninstallEvent.event, - getInstalled: () => Promise.resolve([extension, aLocalExtension('pub.b')]) - }); testObject = new TestExtensionEnablementService(instantiationService); + assert.ok(!testObject.isEnabled(extension)); assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.DisabledByEnvironment); }); @@ -645,6 +663,55 @@ suite('ExtensionEnablementService Test', () => { assert.deepStrictEqual(testObject.getEnablementState(webExtension), EnablementState.EnabledGlobally); }); + test('test state of multipe extensions', async () => { + installed.push(...[aLocalExtension('pub.a'), aLocalExtension('pub.b'), aLocalExtension('pub.c'), aLocalExtension('pub.d'), aLocalExtension('pub.e')]); + testObject = new TestExtensionEnablementService(instantiationService); + await (testObject).waitUntilInitialized(); + + await testObject.setEnablement([installed[0]], EnablementState.DisabledGlobally); + await testObject.setEnablement([installed[1]], EnablementState.DisabledWorkspace); + await testObject.setEnablement([installed[2]], EnablementState.EnabledWorkspace); + await testObject.setEnablement([installed[3]], EnablementState.EnabledGlobally); + + assert.deepStrictEqual(testObject.getEnablementStates(installed), [EnablementState.DisabledGlobally, EnablementState.DisabledWorkspace, EnablementState.EnabledWorkspace, EnablementState.EnabledGlobally, EnablementState.EnabledGlobally]); + }); + + test('test extension is disabled by dependency if it has a dependency that is disabled', async () => { + installed.push(...[aLocalExtension2('pub.a'), aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'] })]); + testObject = new TestExtensionEnablementService(instantiationService); + await (testObject).waitUntilInitialized(); + + await testObject.setEnablement([installed[0]], EnablementState.DisabledGlobally); + + assert.strictEqual(testObject.getEnablementState(installed[1]), EnablementState.DisabledByExtensionDependency); + }); + + test('test extension is disabled by dependency if it has a dependency that is disabled by virtual workspace', async () => { + installed.push(...[aLocalExtension2('pub.a', { capabilities: { virtualWorkspaces: false } }), aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'], capabilities: { virtualWorkspaces: true } })]); + instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] }); + testObject = new TestExtensionEnablementService(instantiationService); + await (testObject).waitUntilInitialized(); + + assert.strictEqual(testObject.getEnablementState(installed[0]), EnablementState.DisabledByVirtualWorkspace); + assert.strictEqual(testObject.getEnablementState(installed[1]), EnablementState.DisabledByExtensionDependency); + }); + + test('test extension is not disabled by dependency even if it has a dependency that is disabled when installed extensions are not set', async () => { + await testObject.setEnablement([aLocalExtension2('pub.a')], EnablementState.DisabledGlobally); + + assert.strictEqual(testObject.getEnablementState(aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'] })), EnablementState.EnabledGlobally); + }); + + test('test extension is disabled by dependency if it has a dependency that is disabled when all extensions are passed', async () => { + installed.push(...[aLocalExtension2('pub.a'), aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'] })]); + testObject = new TestExtensionEnablementService(instantiationService); + await (testObject).waitUntilInitialized(); + + await testObject.setEnablement([installed[0]], EnablementState.DisabledGlobally); + + assert.deepStrictEqual(testObject.getEnablementStates(installed), [EnablementState.DisabledGlobally, EnablementState.DisabledByExtensionDependency]); + }); + }); function anExtensionManagementServer(authority: string, instantiationService: TestInstantiationService): IExtensionManagementServer { @@ -688,6 +755,7 @@ function aLocalExtension2(id: string, manifest: Partial = {} manifest = { name, publisher, ...manifest }; properties = { identifier: { id }, + location: URI.file(`pub.${name}`), galleryIdentifier: { id, uuid: undefined }, type: ExtensionType.User, ...properties diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index ac46cba0a9bc2..c1c71366213bf 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -11,8 +11,7 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import * as perf from 'vs/base/common/performance'; import { isEqualOrParent } from 'vs/base/common/resources'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IWebExtensionsScannerService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { BetterMergeId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { EnablementState, IWebExtensionsScannerService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -763,7 +762,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._checkEnableProposedApi(extensions); // keep only enabled extensions - return extensions.filter(extension => this._isEnabled(extension, ignoreWorkspaceTrust)); + return this._filterEnabledExtensions(extensions, ignoreWorkspaceTrust); } /** @@ -771,30 +770,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx * @argument ignoreWorkspaceTrust Do not take workspace trust into account. */ protected _isEnabled(extension: IExtensionDescription, ignoreWorkspaceTrust: boolean): boolean { - if (extension.isUnderDevelopment) { - // Never disable extensions under development - return true; - } - - if (ExtensionIdentifier.equals(extension.identifier, BetterMergeId)) { - // Check if this is the better merge extension which was migrated to a built-in extension - return false; - } - - const ext = toExtension(extension); - - const isEnabled = this._safeInvokeIsEnabled(ext); - if (isEnabled) { - return true; - } - - if (ignoreWorkspaceTrust && this._safeInvokeIsDisabledByWorkspaceTrust(ext)) { - // This extension is disabled, but the reason for it being disabled - // is workspace trust, so we will consider it enabled - return true; - } - - return false; + return this._filterEnabledExtensions([extension], ignoreWorkspaceTrust).includes(extension); } protected _safeInvokeIsEnabled(extension: IExtension): boolean { @@ -807,12 +783,36 @@ export abstract class AbstractExtensionService extends Disposable implements IEx protected _safeInvokeIsDisabledByWorkspaceTrust(extension: IExtension): boolean { try { - return this._extensionEnablementService.isDisabledByWorkspaceTrust(extension); + const enablementState = this._extensionEnablementService.getEnablementState(extension); + return enablementState === EnablementState.DisabledByTrustRequirement; } catch (err) { return false; } } + private _filterEnabledExtensions(extensions: IExtensionDescription[], ignoreWorkspaceTrust: boolean): IExtensionDescription[] { + const enabledExtensions: IExtensionDescription[] = [], extensionsToCheck: IExtensionDescription[] = [], mappedExtensions: IExtension[] = []; + for (const extension of extensions) { + if (extension.isUnderDevelopment) { + // Never disable extensions under development + enabledExtensions.push(extension); + } + else { + extensionsToCheck.push(extension); + mappedExtensions.push(toExtension(extension)); + } + } + + const enablementStates = this._extensionEnablementService.getEnablementStates(mappedExtensions); + for (let index = 0; index < enablementStates.length; index++) { + if (enablementStates[index] === EnablementState.EnabledGlobally || enablementStates[index] === EnablementState.EnabledWorkspace || (ignoreWorkspaceTrust && enablementStates[index] === EnablementState.DisabledByTrustRequirement)) { + enabledExtensions.push(extensionsToCheck[index]); + } + } + + return enabledExtensions; + } + protected _doHandleExtensionPoints(affectedExtensions: IExtensionDescription[]): void { const affectedExtensionPoints: { [extPointName: string]: boolean; } = Object.create(null); for (let extensionDescription of affectedExtensions) {