diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 8585f92a884..6f15f76499f 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -5,6 +5,7 @@ import { distinct } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IStringDictionary } from 'vs/base/common/collections'; import { canceled, getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors'; import { getOrDefault } from 'vs/base/common/objects'; import { IPager } from 'vs/base/common/paging'; @@ -440,7 +441,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller interface IRawExtensionsControlManifest { malicious: string[]; - slow: string[]; + unsupported: IStringDictionary; } abstract class AbstractExtensionGalleryService implements IExtensionGalleryService { @@ -958,14 +959,23 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const result = await asJson(context); const malicious: IExtensionIdentifier[] = []; + const unsupportedPreReleaseExtensions: IStringDictionary<{ id: string, displayName: string }> = {}; if (result) { for (const id of result.malicious) { malicious.push({ id }); } + if (result.unsupported) { + for (const extensionId of Object.keys(result.unsupported)) { + const value = result.unsupported[extensionId]; + if (!isBoolean(value)) { + unsupportedPreReleaseExtensions[extensionId.toLowerCase()] = value.preReleaseExtension; + } + } + } } - return { malicious }; + return { malicious, unsupportedPreReleaseExtensions }; } } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index fe4a211e5a6..15afacc7856 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; +import { IStringDictionary } from 'vs/base/common/collections'; import { Event } from 'vs/base/common/event'; import { FileAccess } from 'vs/base/common/network'; import { IPager } from 'vs/base/common/paging'; @@ -312,6 +313,7 @@ export const enum StatisticType { export interface IExtensionsControlManifest { malicious: IExtensionIdentifier[]; + unsupportedPreReleaseExtensions?: IStringDictionary<{ id: string, displayName: string }>; } export const enum InstallOperation { diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index c114661a85a..24ce51098e4 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -74,6 +74,7 @@ import { ExtensionsCompletionItemsProvider } from 'vs/workbench/contrib/extensio import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { Event } from 'vs/base/common/event'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { UnsupportedPreReleaseExtensionsChecker } from 'vs/workbench/contrib/extensions/browser/unsupportedPreReleaseExtensionsChecker'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -1461,6 +1462,7 @@ const workbenchRegistry = Registry.as(Workbench workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(MaliciousExtensionChecker, LifecyclePhase.Eventually); +workbenchRegistry.registerWorkbenchContribution(UnsupportedPreReleaseExtensionsChecker, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(ExtensionActivationProgress, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 94074876872..cbb2a5fedaf 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -14,7 +14,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { dispose } from 'vs/base/common/lifecycle'; import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; -import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -1125,6 +1125,40 @@ export class SwitchToReleasedVersionAction extends ExtensionAction { } } +export class SwitchUnsupportedExtensionToPreReleaseExtensionAction extends Action { + + private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} hide-when-disabled`; + + constructor( + private readonly local: ILocalExtension, + private readonly gallery: IGalleryExtension, + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IProductService private readonly productService: IProductService, + @IHostService private readonly hostService: IHostService, + @IWorkbenchExtensionEnablementService private readonly workbenchExtensionEnablementService: IWorkbenchExtensionEnablementService, + @INotificationService private readonly notificationService: INotificationService, + ) { + super('workbench.extensions.action.switchUnsupportedExtensionToPreReleaseExtension', localize('switchUnsupportedExtensionToPreReleaseExtension', "Switch to '{0}' Pre-Release version", gallery.displayName), SwitchUnsupportedExtensionToPreReleaseExtensionAction.Class); + } + + override async run(): Promise { + await Promise.all([ + this.extensionManagementService.uninstall(this.local), + this.extensionManagementService.installFromGallery(this.gallery, { installPreReleaseVersion: true, isMachineScoped: this.local.isMachineScoped }) + .then(local => this.workbenchExtensionEnablementService.setEnablement([this.local], EnablementState.EnabledGlobally)), + ]); + this.notificationService.prompt( + Severity.Info, + localize('SwitchToAnotherReleaseExtension.successReload', "Please reload {0} to complete switching to the '{1}' extension.", this.productService.nameLong, this.gallery.displayName), + [{ + label: localize('reloadNow', "Reload Now"), + run: () => this.hostService.reload() + }], + { sticky: true } + ); + } +} + export class InstallAnotherVersionAction extends ExtensionAction { static readonly ID = 'workbench.extensions.action.install.anotherVersion'; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 9d9dff33d8e..aa88093a29b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -14,10 +14,10 @@ import { IPager, mapPager, singlePagePager } from 'vs/base/common/paging'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, - InstallExtensionEvent, DidUninstallExtensionEvent, IExtensionIdentifier, InstallOperation, DefaultIconPath, InstallOptions, WEB_EXTENSION_TAG, InstallExtensionResult, isIExtensionIdentifier + InstallExtensionEvent, DidUninstallExtensionEvent, IExtensionIdentifier, InstallOperation, DefaultIconPath, InstallOptions, WEB_EXTENSION_TAG, InstallExtensionResult, isIExtensionIdentifier, IExtensionsControlManifest } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, getMaliciousExtensionsSet, groupByExtension, ExtensionIdentifierWithVersion, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, ExtensionIdentifierWithVersion, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -418,28 +418,32 @@ class Extensions extends Disposable { return this.local; } - async syncLocalWithGalleryExtension(gallery: IGalleryExtension, maliciousExtensionSet: Set): Promise { + async syncLocalWithGalleryExtension(gallery: IGalleryExtension, extensionsControlManifest: IExtensionsControlManifest): Promise { const extension = this.getInstalledExtensionMatchingGallery(gallery); - if (!extension) { + if (!extension?.local) { return false; } - if (maliciousExtensionSet.has(extension.identifier.id)) { - extension.isMalicious = true; + + let hasChanged: boolean = false; + + const isMalicious = extensionsControlManifest.malicious.some(identifier => areSameExtensions(extension.identifier, identifier)); + if (extension.isMalicious !== isMalicious) { + extension.isMalicious = isMalicious; + hasChanged = true; } const compatible = await this.getCompatibleExtension(gallery, !!extension.local?.isPreReleaseVersion); - if (!compatible) { - return false; - } - // Sync the local extension with gallery extension if local extension doesnot has metadata - if (extension.local) { + if (compatible) { const local = extension.local.identifier.uuid ? extension.local : await this.server.extensionManagementService.updateMetadata(extension.local, { id: compatible.identifier.uuid, publisherDisplayName: compatible.publisherDisplayName, publisherId: compatible.publisherId }); extension.local = local; extension.gallery = compatible; - this._onChange.fire({ extension }); - return true; + hasChanged = true; } - return false; + + if (hasChanged) { + this._onChange.fire({ extension }); + } + return hasChanged; } private async getCompatibleExtension(extensionOrIdentifier: IGalleryExtension | IExtensionIdentifier, includePreRelease: boolean): Promise { @@ -749,11 +753,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension options.text = options.text ? this.resolveQueryText(options.text) : options.text; options.includePreRelease = isUndefined(options.includePreRelease) ? this.preferPreReleases : options.includePreRelease; - const report = await this.extensionManagementService.getExtensionsControlManifest(); - const maliciousSet = getMaliciousExtensionsSet(report); + const extensionsControlManifest = await this.extensionManagementService.getExtensionsControlManifest(); try { const result = await this.galleryService.query(options, token); - return mapPager(result, gallery => this.fromGallery(gallery, maliciousSet)); + return mapPager(result, gallery => this.fromGallery(gallery, extensionsControlManifest)); } catch (error) { if (/No extension gallery service configured/.test(error.message)) { return Promise.resolve(singlePagePager([])); @@ -890,11 +893,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return extension || extensions[0]; } - private fromGallery(gallery: IGalleryExtension, maliciousExtensionSet: Set): IExtension { + private fromGallery(gallery: IGalleryExtension, extensionsControlManifest: IExtensionsControlManifest): IExtension { Promise.all([ - this.localExtensions ? this.localExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet) : Promise.resolve(false), - this.remoteExtensions ? this.remoteExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet) : Promise.resolve(false), - this.webExtensions ? this.webExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet) : Promise.resolve(false) + this.localExtensions ? this.localExtensions.syncLocalWithGalleryExtension(gallery, extensionsControlManifest) : Promise.resolve(false), + this.remoteExtensions ? this.remoteExtensions.syncLocalWithGalleryExtension(gallery, extensionsControlManifest) : Promise.resolve(false), + this.webExtensions ? this.webExtensions.syncLocalWithGalleryExtension(gallery, extensionsControlManifest) : Promise.resolve(false) ]) .then(result => { if (result[0] || result[1] || result[2]) { @@ -907,7 +910,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return installed; } const extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), undefined, undefined, gallery); - if (maliciousExtensionSet.has(extension.identifier.id)) { + if (extensionsControlManifest.malicious.some(identifier => areSameExtensions(extension.identifier, identifier))) { extension.isMalicious = true; } return extension; diff --git a/src/vs/workbench/contrib/extensions/browser/unsupportedPreReleaseExtensionsChecker.ts b/src/vs/workbench/contrib/extensions/browser/unsupportedPreReleaseExtensionsChecker.ts new file mode 100644 index 00000000000..75ee92f4b3a --- /dev/null +++ b/src/vs/workbench/contrib/extensions/browser/unsupportedPreReleaseExtensionsChecker.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { localize } from 'vs/nls'; +import { IExtensionGalleryService, IExtensionManagementService, IGalleryExtension, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { SwitchUnsupportedExtensionToPreReleaseExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; + +export class UnsupportedPreReleaseExtensionsChecker implements IWorkbenchContribution { + + constructor( + @INotificationService private readonly notificationService: INotificationService, + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + this.notifyUnsupportedPreReleaseExtensions(); + } + + private async notifyUnsupportedPreReleaseExtensions(): Promise { + const extensionsControlManifest = await this.extensionManagementService.getExtensionsControlManifest(); + if (!extensionsControlManifest.unsupportedPreReleaseExtensions) { + return; + } + + const installed = await this.extensionManagementService.getInstalled(ExtensionType.User); + const unsupportedLocalExtensionsWithIdentifiers: [ILocalExtension, IExtensionIdentifier][] = []; + for (const extension of installed) { + const preReleaseExtension = extensionsControlManifest.unsupportedPreReleaseExtensions[extension.identifier.id.toLowerCase()]; + if (preReleaseExtension) { + unsupportedLocalExtensionsWithIdentifiers.push([extension, { id: preReleaseExtension.id }]); + } + } + if (!unsupportedLocalExtensionsWithIdentifiers.length) { + return; + } + + const unsupportedPreReleaseExtensions: [ILocalExtension, IGalleryExtension][] = []; + const galleryExensions = await this.extensionGalleryService.getExtensions(unsupportedLocalExtensionsWithIdentifiers.map(([, identifier]) => identifier), true, CancellationToken.None); + for (const gallery of galleryExensions) { + const unsupportedLocalExtension = unsupportedLocalExtensionsWithIdentifiers.find(([, identifier]) => areSameExtensions(identifier, gallery.identifier)); + if (unsupportedLocalExtension) { + unsupportedPreReleaseExtensions.push([unsupportedLocalExtension[0], gallery]); + } + } + if (!unsupportedPreReleaseExtensions.length) { + return; + } + + if (unsupportedPreReleaseExtensions.length === 1) { + const [local, gallery] = unsupportedPreReleaseExtensions[0]; + const action = this.instantiationService.createInstance(SwitchUnsupportedExtensionToPreReleaseExtensionAction, unsupportedPreReleaseExtensions[0][0], unsupportedPreReleaseExtensions[0][1]); + this.notificationService.notify({ + severity: Severity.Info, + message: localize('unsupported prerelease message', "'{0}' extension is now part of the '{1}' extension as a pre-release version and it is no longer supported. Would you like to switch to '{2}' extension?", local.manifest.displayName || local.identifier.id, gallery.displayName, gallery.displayName), + actions: { + primary: [action] + }, + sticky: true + }); + return; + } + } + +}