#15756 prompt users to migrate from old extension to main prerelease extension

This commit is contained in:
Sandeep Somavarapu 2021-11-26 01:06:49 +01:00
parent 4aad18d229
commit 503a9bcd16
No known key found for this signature in database
GPG key ID: 1FED25EC4646638B
6 changed files with 148 additions and 25 deletions

View file

@ -5,6 +5,7 @@
import { distinct } from 'vs/base/common/arrays'; import { distinct } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { IStringDictionary } from 'vs/base/common/collections';
import { canceled, getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors'; import { canceled, getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors';
import { getOrDefault } from 'vs/base/common/objects'; import { getOrDefault } from 'vs/base/common/objects';
import { IPager } from 'vs/base/common/paging'; import { IPager } from 'vs/base/common/paging';
@ -440,7 +441,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
interface IRawExtensionsControlManifest { interface IRawExtensionsControlManifest {
malicious: string[]; malicious: string[];
slow: string[]; unsupported: IStringDictionary<boolean | { preReleaseExtension: { id: string, displayName: string } }>;
} }
abstract class AbstractExtensionGalleryService implements IExtensionGalleryService { abstract class AbstractExtensionGalleryService implements IExtensionGalleryService {
@ -958,14 +959,23 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
const result = await asJson<IRawExtensionsControlManifest>(context); const result = await asJson<IRawExtensionsControlManifest>(context);
const malicious: IExtensionIdentifier[] = []; const malicious: IExtensionIdentifier[] = [];
const unsupportedPreReleaseExtensions: IStringDictionary<{ id: string, displayName: string }> = {};
if (result) { if (result) {
for (const id of result.malicious) { for (const id of result.malicious) {
malicious.push({ id }); 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 };
} }
} }

View file

@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { IStringDictionary } from 'vs/base/common/collections';
import { Event } from 'vs/base/common/event'; import { Event } from 'vs/base/common/event';
import { FileAccess } from 'vs/base/common/network'; import { FileAccess } from 'vs/base/common/network';
import { IPager } from 'vs/base/common/paging'; import { IPager } from 'vs/base/common/paging';
@ -312,6 +313,7 @@ export const enum StatisticType {
export interface IExtensionsControlManifest { export interface IExtensionsControlManifest {
malicious: IExtensionIdentifier[]; malicious: IExtensionIdentifier[];
unsupportedPreReleaseExtensions?: IStringDictionary<{ id: string, displayName: string }>;
} }
export const enum InstallOperation { export const enum InstallOperation {

View file

@ -74,6 +74,7 @@ import { ExtensionsCompletionItemsProvider } from 'vs/workbench/contrib/extensio
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { Event } from 'vs/base/common/event'; import { Event } from 'vs/base/common/event';
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
import { UnsupportedPreReleaseExtensionsChecker } from 'vs/workbench/contrib/extensions/browser/unsupportedPreReleaseExtensionsChecker';
// Singletons // Singletons
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService);
@ -1461,6 +1462,7 @@ const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(Workbench
workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Starting);
workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Restored);
workbenchRegistry.registerWorkbenchContribution(MaliciousExtensionChecker, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(MaliciousExtensionChecker, LifecyclePhase.Eventually);
workbenchRegistry.registerWorkbenchContribution(UnsupportedPreReleaseExtensionsChecker, LifecyclePhase.Eventually);
workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase.Restored);
workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting);
workbenchRegistry.registerWorkbenchContribution(ExtensionActivationProgress, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(ExtensionActivationProgress, LifecyclePhase.Eventually);

View file

@ -14,7 +14,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
import { dispose } from 'vs/base/common/lifecycle'; 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 { 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 { 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 { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; 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<any> {
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 { export class InstallAnotherVersionAction extends ExtensionAction {
static readonly ID = 'workbench.extensions.action.install.anotherVersion'; static readonly ID = 'workbench.extensions.action.install.anotherVersion';

View file

@ -14,10 +14,10 @@ import { IPager, mapPager, singlePagePager } from 'vs/base/common/paging';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { import {
IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, 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'; } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from 'vs/workbench/services/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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IHostService } from 'vs/workbench/services/host/browser/host';
@ -418,28 +418,32 @@ class Extensions extends Disposable {
return this.local; return this.local;
} }
async syncLocalWithGalleryExtension(gallery: IGalleryExtension, maliciousExtensionSet: Set<string>): Promise<boolean> { async syncLocalWithGalleryExtension(gallery: IGalleryExtension, extensionsControlManifest: IExtensionsControlManifest): Promise<boolean> {
const extension = this.getInstalledExtensionMatchingGallery(gallery); const extension = this.getInstalledExtensionMatchingGallery(gallery);
if (!extension) { if (!extension?.local) {
return false; 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); const compatible = await this.getCompatibleExtension(gallery, !!extension.local?.isPreReleaseVersion);
if (!compatible) { if (compatible) {
return false;
}
// Sync the local extension with gallery extension if local extension doesnot has metadata
if (extension.local) {
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 }); 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.local = local;
extension.gallery = compatible; extension.gallery = compatible;
this._onChange.fire({ extension }); hasChanged = true;
return true;
} }
return false;
if (hasChanged) {
this._onChange.fire({ extension });
}
return hasChanged;
} }
private async getCompatibleExtension(extensionOrIdentifier: IGalleryExtension | IExtensionIdentifier, includePreRelease: boolean): Promise<IGalleryExtension | null> { private async getCompatibleExtension(extensionOrIdentifier: IGalleryExtension | IExtensionIdentifier, includePreRelease: boolean): Promise<IGalleryExtension | null> {
@ -749,11 +753,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
options.text = options.text ? this.resolveQueryText(options.text) : options.text; options.text = options.text ? this.resolveQueryText(options.text) : options.text;
options.includePreRelease = isUndefined(options.includePreRelease) ? this.preferPreReleases : options.includePreRelease; options.includePreRelease = isUndefined(options.includePreRelease) ? this.preferPreReleases : options.includePreRelease;
const report = await this.extensionManagementService.getExtensionsControlManifest(); const extensionsControlManifest = await this.extensionManagementService.getExtensionsControlManifest();
const maliciousSet = getMaliciousExtensionsSet(report);
try { try {
const result = await this.galleryService.query(options, token); 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) { } catch (error) {
if (/No extension gallery service configured/.test(error.message)) { if (/No extension gallery service configured/.test(error.message)) {
return Promise.resolve(singlePagePager([])); return Promise.resolve(singlePagePager([]));
@ -890,11 +893,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
return extension || extensions[0]; return extension || extensions[0];
} }
private fromGallery(gallery: IGalleryExtension, maliciousExtensionSet: Set<string>): IExtension { private fromGallery(gallery: IGalleryExtension, extensionsControlManifest: IExtensionsControlManifest): IExtension {
Promise.all([ Promise.all([
this.localExtensions ? this.localExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet) : Promise.resolve(false), this.localExtensions ? this.localExtensions.syncLocalWithGalleryExtension(gallery, extensionsControlManifest) : Promise.resolve(false),
this.remoteExtensions ? this.remoteExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet) : Promise.resolve(false), this.remoteExtensions ? this.remoteExtensions.syncLocalWithGalleryExtension(gallery, extensionsControlManifest) : Promise.resolve(false),
this.webExtensions ? this.webExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet) : Promise.resolve(false) this.webExtensions ? this.webExtensions.syncLocalWithGalleryExtension(gallery, extensionsControlManifest) : Promise.resolve(false)
]) ])
.then(result => { .then(result => {
if (result[0] || result[1] || result[2]) { if (result[0] || result[1] || result[2]) {
@ -907,7 +910,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
return installed; return installed;
} }
const extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), undefined, undefined, gallery); 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; extension.isMalicious = true;
} }
return extension; return extension;

View file

@ -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<void> {
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;
}
}
}