#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 { 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<boolean | { preReleaseExtension: { id: string, displayName: string } }>;
}
abstract class AbstractExtensionGalleryService implements IExtensionGalleryService {
@ -958,14 +959,23 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
const result = await asJson<IRawExtensionsControlManifest>(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 };
}
}

View file

@ -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 {

View file

@ -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<IWorkbenchContributionsRegistry>(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);

View file

@ -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<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 {
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 {
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<string>): Promise<boolean> {
async syncLocalWithGalleryExtension(gallery: IGalleryExtension, extensionsControlManifest: IExtensionsControlManifest): Promise<boolean> {
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<IGalleryExtension | null> {
@ -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<string>): 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;

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;
}
}
}