From 94459e758e229b13d882cc8298ad98c67586a52f Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sun, 26 Jun 2022 00:06:27 +0200 Subject: [PATCH 1/4] Improve extensions management in profiles - Make version property mandatory in extension profiles (like in web) - Extend extensions clean up to profiles - Add necessay changes in other services to support extension cleanup: - Introduce INativeServerExtensionManagementService - Extend profile change event to provide added and removed profiles --- .../contrib/extensionsCleaner.ts | 144 +++++++++++++++++- .../sharedProcess/sharedProcessMain.ts | 4 +- src/vs/code/node/cliProcessMain.ts | 6 +- .../abstractExtensionManagementService.ts | 52 ++++--- .../common/extensionManagement.ts | 7 +- .../common/extensionsProfileScannerService.ts | 61 +++++++- .../node/extensionManagementService.ts | 34 +++-- .../node/extensionsScannerService.test.ts | 5 +- .../userDataProfile/common/userDataProfile.ts | 6 +- .../electron-main/userDataProfile.ts | 8 +- .../electron-sandbox/userDataProfile.ts | 10 +- .../node/remoteExtensionHostAgentCli.ts | 6 +- src/vs/server/node/serverServices.ts | 10 +- .../browser/extensions.contribution.ts | 15 +- .../extensions/browser/extensionsCleaner.ts | 19 --- .../common/webExtensionManagementService.ts | 8 +- 16 files changed, 301 insertions(+), 94 deletions(-) delete mode 100644 src/vs/workbench/contrib/extensions/browser/extensionsCleaner.ts diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts index a7ebe66abcae7..3ac907be4588a 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts @@ -4,26 +4,162 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { IExtensionGalleryService, IGlobalExtensionEnablementService, IServerExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { URI } from 'vs/base/common/uri'; +import { IExtensionGalleryService, IExtensionIdentifier, IGlobalExtensionEnablementService, ServerDidUninstallExtensionEvent, ServerInstallExtensionResult, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { migrateUnsupportedExtensions } from 'vs/platform/extensionManagement/common/unsupportedExtensionsMigration'; -import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { DidChangeProfilesEvent, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; + +const uninstalOptions: UninstallOptions = { versionOnly: true, donotIncludePack: true, donotCheckDependents: true }; export class ExtensionsCleaner extends Disposable { constructor( - @IServerExtensionManagementService extensionManagementService: ExtensionManagementService, + @INativeServerExtensionManagementService extensionManagementService: INativeServerExtensionManagementService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @IExtensionStorageService extensionStorageService: IExtensionStorageService, @IGlobalExtensionEnablementService extensionEnablementService: IGlobalExtensionEnablementService, + @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @ILogService logService: ILogService, ) { super(); - extensionManagementService.removeDeprecatedExtensions(); + + extensionManagementService.removeUninstalledExtensions(this.userDataProfilesService.profiles.length > 1); migrateUnsupportedExtensions(extensionManagementService, extensionGalleryService, extensionStorageService, extensionEnablementService, logService); ExtensionStorageService.removeOutdatedExtensionVersions(extensionManagementService, storageService); + this._register(instantiationService.createInstance(ProfileExtensionsCleaner)); } + +} + +class ProfileExtensionsCleaner extends Disposable { + + private profileExtensionsLocations = new Map; + private readonly initPromise: Promise; + + constructor( + @INativeServerExtensionManagementService private readonly extensionManagementService: INativeServerExtensionManagementService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @ILogService private readonly logService: ILogService, + ) { + super(); + + this.initPromise = this.initialize(); + this._register(this.userDataProfilesService.onDidChangeProfiles(e => this.onDidChangeProfiles(e))); + this._register(this.extensionManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e))); + this._register(this.extensionManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e))); + } + + private async initialize(): Promise { + if (this.userDataProfilesService.profiles.length === 1) { + return true; + } + try { + const installed = await this.extensionManagementService.getAllUserInstalled(); + await Promise.all(this.userDataProfilesService.profiles.map(profile => profile.extensionsResource ? this.populateExtensionsFromProfile(profile.extensionsResource) : Promise.resolve())); + const toUninstall = installed.filter(installedExtension => !this.profileExtensionsLocations.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version))); + if (toUninstall.length) { + await Promise.all(toUninstall.map(extension => this.extensionManagementService.uninstall(extension, uninstalOptions))); + } + return true; + } catch (error) { + this.logService.error('ExtensionsCleaner: Failed to initialize'); + this.logService.error(error); + return false; + } + } + + private async onDidChangeProfiles({ added, removed }: DidChangeProfilesEvent): Promise { + if (!(await this.initPromise)) { + return; + } + await Promise.all(added.map(profile => profile.extensionsResource ? this.populateExtensionsFromProfile(profile.extensionsResource) : Promise.resolve())); + await Promise.all(removed.map(profile => profile.extensionsResource ? this.removeExtensionsFromProfile(profile.extensionsResource) : Promise.resolve())); + } + + private async onDidInstallExtensions(installedExtensions: readonly ServerInstallExtensionResult[]): Promise { + if (!(await this.initPromise)) { + return; + } + for (const { local, profileLocation } of installedExtensions) { + if (!local || !profileLocation) { + continue; + } + this.addExtensionWithKey(this.getKey(local.identifier, local.manifest.version), profileLocation); + } + } + + private async onDidUninstallExtension(e: ServerDidUninstallExtensionEvent): Promise { + if (!e.profileLocation || !e.version) { + return; + } + if (!(await this.initPromise)) { + return; + } + if (this.removeExtensionWithKey(this.getKey(e.identifier, e.version), e.profileLocation)) { + await this.uninstallExtensions([{ identifier: e.identifier, version: e.version }]); + } + } + + private async populateExtensionsFromProfile(extensionsProfileLocation: URI): Promise { + const extensions = await this.extensionsProfileScannerService.scanProfileExtensions(extensionsProfileLocation); + for (const extension of extensions) { + this.addExtensionWithKey(this.getKey(extension.identifier, extension.version), extensionsProfileLocation); + } + } + + private async removeExtensionsFromProfile(removedProfile: URI): Promise { + const profileExtensions = await this.extensionsProfileScannerService.scanProfileExtensions(removedProfile); + const extensionsToRemove = profileExtensions.filter(profileExtension => this.removeExtensionWithKey(this.getKey(profileExtension.identifier, profileExtension.version), removedProfile)); + if (extensionsToRemove.length) { + await this.uninstallExtensions(extensionsToRemove); + } + } + + private addExtensionWithKey(key: string, extensionsProfileLocation: URI): void { + let locations = this.profileExtensionsLocations.get(key); + if (!locations) { + locations = []; + this.profileExtensionsLocations.set(key, locations); + } + locations.push(extensionsProfileLocation); + } + + private removeExtensionWithKey(key: string, profileLocation: URI): boolean { + const profiles = this.profileExtensionsLocations.get(key); + if (profiles) { + const index = profiles.findIndex(profile => this.uriIdentityService.extUri.isEqual(profile, profileLocation)); + if (index > -1) { + profiles.splice(index, 1); + } + } + if (!profiles?.length) { + this.profileExtensionsLocations.delete(key); + } + return !profiles?.length; + } + + private async uninstallExtensions(extensionsToRemove: { identifier: IExtensionIdentifier; version: string }[]): Promise { + const installed = await this.extensionManagementService.getAllUserInstalled(); + const toUninstall = installed.filter(installedExtension => extensionsToRemove.some(e => this.getKey(installedExtension.identifier, installedExtension.manifest.version) === this.getKey(e.identifier, e.version))); + if (toUninstall.length) { + await Promise.all(toUninstall.map(extension => this.extensionManagementService.uninstall(extension, uninstalOptions))); + } + } + + private getKey(identifier: IExtensionIdentifier, version: string): string { + return `${ExtensionIdentifier.toKey(identifier.id)}@${version}`; + } + } diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 862a359dfd9fc..11116cc4cce85 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -34,7 +34,7 @@ import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/ import { IExtensionGalleryService, IExtensionManagementService, IExtensionTipsService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionManagementChannel, ExtensionTipsChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService'; -import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; import { ExtensionRecommendationNotificationServiceChannelClient } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc'; import { IFileService } from 'vs/platform/files/common/files'; @@ -315,7 +315,7 @@ class SharedProcessMain extends Disposable { // Extension Management services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); - services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); // Extension Gallery services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 6ffc074614c5a..56ee36e7f1b10 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -23,11 +23,11 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; -import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IExtensionManagementCLIService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; import { ExtensionsProfileScannerService, IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; -import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -178,7 +178,7 @@ class CliMain extends Disposable { // Extensions services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); - services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService)); diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 99d049079edb7..c31a9e3c4270a 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -14,7 +14,8 @@ import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, - IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, IServerExtensionManagementService, ServerInstallOptions, ServerInstallVSIXOptions, ServerUninstallOptions, Metadata, ServerInstallExtensionEvent, ServerInstallExtensionResult, ServerUninstallExtensionEvent, ServerDidUninstallExtensionEvent + IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, IServerExtensionManagementService, + ServerInstallOptions, ServerInstallVSIXOptions, ServerUninstallOptions, Metadata, ServerInstallExtensionEvent, ServerInstallExtensionResult, ServerUninstallExtensionEvent, ServerDidUninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionKey, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; @@ -34,8 +35,6 @@ export interface IInstallExtensionTask { cancel(): void; } -export type UninstallExtensionTaskOptions = { readonly remove?: boolean; readonly versionOnly?: boolean }; - export interface IUninstallExtensionTask { readonly extension: ILocalExtension; run(): Promise; @@ -426,47 +425,58 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } private async unininstallExtension(extension: ILocalExtension, options: ServerUninstallOptions): Promise { - if (!options.profileLocation) { - const uninstallExtensionTask = this.uninstallingExtensions.get(extension.identifier.id.toLowerCase()); - if (uninstallExtensionTask) { - this.logService.info('Extensions is already requested to uninstall', extension.identifier.id); - return uninstallExtensionTask.waitUntilTaskIsFinished(); - } + const getUninstallExtensionTaskKey = (identifier: IExtensionIdentifier) => `${identifier.id.toLowerCase()}${options.profileLocation ? `@${options.profileLocation.toString()}` : ''}`; + const uninstallExtensionTask = this.uninstallingExtensions.get(getUninstallExtensionTaskKey(extension.identifier)); + if (uninstallExtensionTask) { + this.logService.info('Extensions is already requested to uninstall', extension.identifier.id); + return uninstallExtensionTask.waitUntilTaskIsFinished(); } - const createUninstallExtensionTask = (extension: ILocalExtension, uninstallOptions: UninstallExtensionTaskOptions): IUninstallExtensionTask => { + const createUninstallExtensionTask = (extension: ILocalExtension, uninstallOptions: ServerUninstallOptions): IUninstallExtensionTask => { const uninstallExtensionTask = this.createUninstallExtensionTask(extension, uninstallOptions, options.profileLocation); - this.uninstallingExtensions.set(uninstallExtensionTask.extension.identifier.id.toLowerCase(), uninstallExtensionTask); - this.logService.info('Uninstalling extension:', extension.identifier.id); + this.uninstallingExtensions.set(getUninstallExtensionTaskKey(uninstallExtensionTask.extension.identifier), uninstallExtensionTask); + if (options.profileLocation) { + this.logService.info('Uninstalling extension from the profile:', extension.identifier.id, options.profileLocation.toString()); + } else { + this.logService.info('Uninstalling extension:', extension.identifier.id); + } this._onUninstallExtension.fire({ identifier: extension.identifier, profileLocation: options.profileLocation, applicationScoped: extension.isApplicationScoped }); return uninstallExtensionTask; }; const postUninstallExtension = (extension: ILocalExtension, error?: ExtensionManagementError): void => { if (error) { - this.logService.error('Failed to uninstall extension:', extension.identifier.id, error.message); + if (options.profileLocation) { + this.logService.error('Failed to uninstall extension from the profile:', extension.identifier.id, options.profileLocation.toString(), error.message); + } else { + this.logService.error('Failed to uninstall extension:', extension.identifier.id, error.message); + } } else { - this.logService.info('Successfully uninstalled extension:', extension.identifier.id); + if (options.profileLocation) { + this.logService.info('Successfully uninstalled extension from the profile', extension.identifier.id, options.profileLocation.toString()); + } else { + this.logService.info('Successfully uninstalled extension:', extension.identifier.id); + } } reportTelemetry(this.telemetryService, 'extensionGallery:uninstall', { extensionData: getLocalExtensionTelemetryData(extension), error }); - this._onDidUninstallExtension.fire({ identifier: extension.identifier, error: error?.code, profileLocation: options.profileLocation, applicationScoped: extension.isApplicationScoped }); + this._onDidUninstallExtension.fire({ identifier: extension.identifier, version: extension.manifest.version, error: error?.code, profileLocation: options.profileLocation, applicationScoped: extension.isApplicationScoped }); }; const allTasks: IUninstallExtensionTask[] = []; const processedTasks: IUninstallExtensionTask[] = []; try { - allTasks.push(createUninstallExtensionTask(extension, {})); + allTasks.push(createUninstallExtensionTask(extension, options)); const installed = await this.getInstalled(ExtensionType.User, options.profileLocation); if (options.donotIncludePack) { this.logService.info('Uninstalling the extension without including packed extension', extension.identifier.id); } else { const packedExtensions = this.getAllPackExtensionsToUninstall(extension, installed); for (const packedExtension of packedExtensions) { - if (this.uninstallingExtensions.has(packedExtension.identifier.id.toLowerCase())) { + if (this.uninstallingExtensions.has(getUninstallExtensionTaskKey(packedExtension.identifier))) { this.logService.info('Extensions is already requested to uninstall', packedExtension.identifier.id); } else { - allTasks.push(createUninstallExtensionTask(packedExtension, {})); + allTasks.push(createUninstallExtensionTask(packedExtension, options)); } } } @@ -511,7 +521,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } finally { // Remove tasks from cache for (const task of allTasks) { - if (!this.uninstallingExtensions.delete(task.extension.identifier.id.toLowerCase())) { + if (!this.uninstallingExtensions.delete(getUninstallExtensionTaskKey(task.extension.identifier))) { this.logService.warn('Uninstallation task is not found in the cache', task.extension.identifier.id); } } @@ -607,7 +617,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return new InstallExtensionInProfileTask(installTask, options.profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource, this.extensionsProfileScannerService); } - private createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions, profile?: URI): IUninstallExtensionTask { + private createUninstallExtensionTask(extension: ILocalExtension, options: ServerUninstallOptions, profile?: URI): IUninstallExtensionTask { return profile && this.userDataProfilesService.defaultProfile.extensionsResource ? new UninstallExtensionFromProfileTask(extension, profile, this.userDataProfilesService, this.extensionsProfileScannerService) : this.createDefaultUninstallExtensionTask(extension, options); } @@ -623,7 +633,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl abstract updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise; protected abstract createDefaultInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: ServerInstallOptions & ServerInstallVSIXOptions): IInstallExtensionTask; - protected abstract createDefaultUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask; + protected abstract createDefaultUninstallExtensionTask(extension: ILocalExtension, options: ServerUninstallOptions): IUninstallExtensionTask; } export function joinErrors(errorOrErrors: (Error | string) | (Array)): Error { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 4e376a1a4c859..3c5ec5205122d 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -361,8 +361,9 @@ export interface UninstallExtensionEvent { } export interface DidUninstallExtensionEvent { - identifier: IExtensionIdentifier; - error?: string; + readonly identifier: IExtensionIdentifier; + readonly version?: string; + readonly error?: string; } export enum ExtensionManagementErrorCode { @@ -403,7 +404,7 @@ export type InstallOptions = { context?: IStringDictionary; }; export type InstallVSIXOptions = Omit & { installOnlyNewlyAddedFromExtensionPack?: boolean }; -export type UninstallOptions = { donotIncludePack?: boolean; donotCheckDependents?: boolean }; +export type UninstallOptions = { readonly donotIncludePack?: boolean; readonly donotCheckDependents?: boolean; readonly versionOnly?: boolean; readonly remove?: boolean }; export interface IExtensionManagementParticipant { postInstall(local: ILocalExtension, source: URI | IGalleryExtension, options: InstallOptions | InstallVSIXOptions, token: CancellationToken): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index 0dfadaa8c4901..8acce96e6e1c2 100644 --- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -10,19 +10,23 @@ import { ResourceMap } from 'vs/base/common/map'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ILocalExtension, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IExtensionIdentifier, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; interface IStoredProfileExtension { - readonly identifier: IExtensionIdentifier; - readonly location: UriComponents; - readonly metadata?: Metadata; + identifier: IExtensionIdentifier; + location: UriComponents; + version: string; + metadata?: Metadata; } export interface IScannedProfileExtension { readonly identifier: IExtensionIdentifier; + readonly version: string; readonly location: URI; readonly metadata?: Metadata; } @@ -39,13 +43,45 @@ export interface IExtensionsProfileScannerService { export class ExtensionsProfileScannerService extends Disposable implements IExtensionsProfileScannerService { readonly _serviceBrand: undefined; + private readonly migratePromise: Promise; private readonly resourcesAccessQueueMap = new ResourceMap>(); constructor( @IFileService private readonly fileService: IFileService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @ILogService private readonly logService: ILogService, ) { super(); + this.migratePromise = this.migrate(); + } + + // TODO: @sandy081 remove it in a month + private async migrate(): Promise { + await Promise.all(this.userDataProfilesService.profiles.map(async e => { + if (!e.extensionsResource) { + return; + } + try { + let needsMigrating: boolean = false; + const storedWebExtensions: IStoredProfileExtension[] = JSON.parse((await this.fileService.readFile(e.extensionsResource)).value.toString()); + for (const e of storedWebExtensions) { + if (!e.location) { + continue; + } + if (!e.version) { + try { + const content = (await this.fileService.readFile(this.uriIdentityService.extUri.joinPath(URI.revive(e.location), 'package.json'))).value.toString(); + e.version = (JSON.parse(content)).version; + needsMigrating = true; + } catch (error) { /* ignore */ } + } + } + if (needsMigrating) { + await this.fileService.writeFile(e.extensionsResource, VSBuffer.fromString(JSON.stringify(storedWebExtensions))); + } + } catch (error) { /* Ignore */ } + })); } scanProfileExtensions(profileLocation: URI): Promise { @@ -56,7 +92,7 @@ export class ExtensionsProfileScannerService extends Disposable implements IExte return this.withProfileExtensions(profileLocation, profileExtensions => { // Remove the existing extension to avoid duplicates profileExtensions = profileExtensions.filter(e => extensions.some(([extension]) => !areSameExtensions(e.identifier, extension.identifier))); - profileExtensions.push(...extensions.map(([extension, metadata]) => ({ identifier: extension.identifier, location: extension.location, metadata }))); + profileExtensions.push(...extensions.map(([extension, metadata]) => ({ identifier: extension.identifier, version: extension.manifest.version, location: extension.location, metadata }))); return profileExtensions; }); } @@ -66,6 +102,7 @@ export class ExtensionsProfileScannerService extends Disposable implements IExte } private async withProfileExtensions(file: URI, updateFn?: (extensions: IScannedProfileExtension[]) => IScannedProfileExtension[]): Promise { + await this.migratePromise; return this.getResourceAccessQueue(file).queue(async () => { let extensions: IScannedProfileExtension[] = []; @@ -74,13 +111,22 @@ export class ExtensionsProfileScannerService extends Disposable implements IExte const content = await this.fileService.readFile(file); const storedWebExtensions: IStoredProfileExtension[] = JSON.parse(content.value.toString()); for (const e of storedWebExtensions) { - if (!e.location || !e.identifier) { - this.logService.info('Ignoring invalid extension while scanning', storedWebExtensions); + if (!e.identifier) { + this.logService.info('Ignoring invalid extension while scanning. Identifier does not exist.', e); + continue; + } + if (!e.location) { + this.logService.info('Ignoring invalid extension while scanning. Location does not exist.', e); + continue; + } + if (!e.version) { + this.logService.info('Ignoring invalid extension while scanning. Version does not exist.', e); continue; } extensions.push({ identifier: e.identifier, location: URI.revive(e.location), + version: e.version, metadata: e.metadata, }); } @@ -96,6 +142,7 @@ export class ExtensionsProfileScannerService extends Disposable implements IExte extensions = updateFn(extensions); const storedProfileExtensions: IStoredProfileExtension[] = extensions.map(e => ({ identifier: e.identifier, + version: e.version, location: e.location.toJSON(), metadata: e.metadata })); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 4c2d6a47d6b02..3eadb31770b30 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -22,10 +22,11 @@ import { extract, ExtractError, IFile, zip } from 'vs/base/node/zip'; import * as nls from 'vs/nls'; import { IDownloadService } from 'vs/platform/download/common/download'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, joinErrors, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, joinErrors } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, - Metadata, ServerInstallOptions, ServerInstallVSIXOptions + IServerExtensionManagementService, + Metadata, ServerInstallOptions, ServerInstallVSIXOptions, ServerUninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; @@ -38,7 +39,7 @@ import { ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensio import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; import { IFileService } from 'vs/platform/files/common/files'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -51,7 +52,14 @@ interface InstallableExtension { metadata?: Metadata; } -export class ExtensionManagementService extends AbstractExtensionManagementService { +export const INativeServerExtensionManagementService = refineServiceDecorator(IServerExtensionManagementService); +export interface INativeServerExtensionManagementService extends IServerExtensionManagementService { + readonly _serviceBrand: undefined; + removeUninstalledExtensions(removeOutdated: boolean): Promise; + getAllUserInstalled(): Promise; +} + +export class ExtensionManagementService extends AbstractExtensionManagementService implements INativeServerExtensionManagementService { private readonly extensionsScanner: ExtensionsScanner; private readonly manifestCache: ExtensionsManifestCache; @@ -117,6 +125,10 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return this.extensionsScanner.scanExtensions(type ?? null, profileLocation); } + getAllUserInstalled(): Promise { + return this.extensionsScanner.scanUserExtensions(false); + } + async install(vsix: URI, options: ServerInstallVSIXOptions = {}): Promise { this.logService.trace('ExtensionManagementService#install', vsix.toString()); @@ -151,8 +163,8 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return local; } - removeDeprecatedExtensions(): Promise { - return this.extensionsScanner.cleanUp(); + removeUninstalledExtensions(removeOutdated: boolean): Promise { + return this.extensionsScanner.cleanUp(removeOutdated); } private async downloadVsix(vsix: URI): Promise { @@ -168,7 +180,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return URI.isUri(extension) ? new InstallVSIXTask(manifest, extension, options, this.galleryService, this.extensionsScanner, this.logService) : new InstallGalleryExtensionTask(manifest, extension, options, this.extensionsDownloader, this.extensionsScanner, this.logService); } - protected createDefaultUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask { + protected createDefaultUninstallExtensionTask(extension: ILocalExtension, options: ServerUninstallOptions): IUninstallExtensionTask { return new UninstallExtensionTask(extension, options, this.extensionsScanner); } @@ -215,9 +227,11 @@ class ExtensionsScanner extends Disposable { this.uninstalledFileLimiter = new Queue(); } - async cleanUp(): Promise { + async cleanUp(removeOutdated: boolean): Promise { await this.removeUninstalledExtensions(); - await this.removeOutdatedExtensions(); + if (removeOutdated) { + await this.removeOutdatedExtensions(); + } } async scanExtensions(type: ExtensionType | null, profileLocation: URI | undefined): Promise { @@ -696,7 +710,7 @@ class UninstallExtensionTask extends AbstractExtensionTask implements IUni constructor( readonly extension: ILocalExtension, - private readonly options: UninstallExtensionTaskOptions, + private readonly options: ServerUninstallOptions, private readonly extensionsScanner: ExtensionsScanner, ) { super(); diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index cdfaa38b84243..54a7232c285b7 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -17,6 +17,7 @@ import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFil import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; +import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; let translations: Translations = Object.create(null); @@ -69,7 +70,9 @@ suite('NativeExtensionsScanerService Test', () => { extensionsPath: userExtensionsLocation.fsPath, }); instantiationService.stub(IProductService, { version: '1.66.0' }); - instantiationService.stub(IExtensionsProfileScannerService, new ExtensionsProfileScannerService(fileService, logService)); + const uriIdentityService = new UriIdentityService(fileService); + const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, logService); + instantiationService.stub(IExtensionsProfileScannerService, new ExtensionsProfileScannerService(fileService, uriIdentityService, userDataProfilesService, logService)); instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, logService)); await fileService.createFolder(systemExtensionsLocation); await fileService.createFolder(userExtensionsLocation); diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index 39d0df3d6ba09..9e9897e8cdc9b 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -81,6 +81,8 @@ if (!isWeb) { }); } +export type DidChangeProfilesEvent = { readonly added: IUserDataProfile[]; readonly removed: IUserDataProfile[]; readonly all: IUserDataProfile[] }; + export const IUserDataProfilesService = createDecorator('IUserDataProfilesService'); export interface IUserDataProfilesService { readonly _serviceBrand: undefined; @@ -88,7 +90,7 @@ export interface IUserDataProfilesService { readonly profilesHome: URI; readonly defaultProfile: IUserDataProfile; - readonly onDidChangeProfiles: Event; + readonly onDidChangeProfiles: Event; readonly profiles: IUserDataProfile[]; newProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags): CustomUserDataProfile; @@ -140,7 +142,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf get defaultProfile(): IUserDataProfile { return this.profiles[0] ?? this._defaultProfile; } get profiles(): IUserDataProfile[] { return []; } - protected readonly _onDidChangeProfiles = this._register(new Emitter()); + protected readonly _onDidChangeProfiles = this._register(new Emitter()); readonly onDidChangeProfiles = this._onDidChangeProfiles.event; constructor( diff --git a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts index 3a7b9503f80aa..089748748149c 100644 --- a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts @@ -85,7 +85,7 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme const storedProfile: StoredUserDataProfile = { name: profile.name, location: profile.location, useDefaultFlags: profile.useDefaultFlags }; const storedProfiles = [...this.getStoredProfiles(), storedProfile]; - this.setStoredProfiles(storedProfiles); + this.setStoredProfiles(storedProfiles, [profile], []); if (workspaceIdentifier) { await this.setProfileForWorkspace(profile, workspaceIdentifier); } @@ -128,7 +128,7 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme await Promises.settled(joiners); this.setStoredWorskpaceInfos(this.getStoredWorskpaceInfos().filter(p => !this.uriIdentityService.extUri.isEqual(p.profile, profile.location))); - this.setStoredProfiles(this.getStoredProfiles().filter(p => !this.uriIdentityService.extUri.isEqual(p.location, profile.location))); + this.setStoredProfiles(this.getStoredProfiles().filter(p => !this.uriIdentityService.extUri.isEqual(p.location, profile.location)), [], [profile]); try { if (this.profiles.length === 2) { @@ -141,10 +141,10 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme } } - private setStoredProfiles(storedProfiles: StoredUserDataProfile[]) { + private setStoredProfiles(storedProfiles: StoredUserDataProfile[], added: IUserDataProfile[], removed: IUserDataProfile[]): void { this.stateMainService.setItem(UserDataProfilesMainService.PROFILES_KEY, storedProfiles); this._profilesObject = undefined; - this._onDidChangeProfiles.fire(this.profiles); + this._onDidChangeProfiles.fire({ added, removed, all: this.profiles }); } private setStoredWorskpaceInfos(storedWorkspaceInfos: StoredWorkspaceInfo[]) { diff --git a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts index 97ae03b8663b1..210bd2288b97e 100644 --- a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts @@ -9,7 +9,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IFileService } from 'vs/platform/files/common/files'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { ILogService } from 'vs/platform/log/common/log'; -import { IUserDataProfile, IUserDataProfilesService, reviveProfile, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { DidChangeProfilesEvent, IUserDataProfile, IUserDataProfilesService, reviveProfile, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; export class UserDataProfilesNativeService extends UserDataProfilesService implements IUserDataProfilesService { @@ -29,9 +29,11 @@ export class UserDataProfilesNativeService extends UserDataProfilesService imple super(environmentService, fileService, logService); this.channel = mainProcessService.getChannel('userDataProfiles'); this._profiles = profiles.map(profile => reviveProfile(profile, this.profilesHome.scheme)); - this._register(this.channel.listen('onDidChangeProfiles')((profiles) => { - this._profiles = profiles.map(profile => reviveProfile(profile, this.profilesHome.scheme)); - this._onDidChangeProfiles.fire(this._profiles); + this._register(this.channel.listen('onDidChangeProfiles')(e => { + const added = e.added.map(profile => reviveProfile(profile, this.profilesHome.scheme)); + const removed = e.removed.map(profile => reviveProfile(profile, this.profilesHome.scheme)); + this._profiles = e.all.map(profile => reviveProfile(profile, this.profilesHome.scheme)); + this._onDidChangeProfiles.fire({ added, removed, all: this.profiles }); })); } diff --git a/src/vs/server/node/remoteExtensionHostAgentCli.ts b/src/vs/server/node/remoteExtensionHostAgentCli.ts index 631a6d18b125b..653748724e072 100644 --- a/src/vs/server/node/remoteExtensionHostAgentCli.ts +++ b/src/vs/server/node/remoteExtensionHostAgentCli.ts @@ -12,9 +12,9 @@ import { IRequestService } from 'vs/platform/request/common/request'; import { RequestService } from 'vs/platform/request/node/requestService'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IExtensionManagementCLIService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; -import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import product from 'vs/platform/product/common/product'; @@ -109,7 +109,7 @@ class CliMain extends Disposable { services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); - services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService)); services.set(ILanguagePackService, new SyncDescriptor(NativeLanguagePackService)); diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index fa2c8004a77a9..797f29fd9e5ae 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -24,10 +24,10 @@ import { IEncryptionMainService } from 'vs/platform/encryption/common/encryption import { EncryptionMainService } from 'vs/platform/encryption/node/encryptionMainService'; import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; -import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; -import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; @@ -165,7 +165,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); - services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); const instantiationService: IInstantiationService = new InstantiationService(services); services.set(ILanguagePackService, instantiationService.createInstance(NativeLanguagePackService)); @@ -188,7 +188,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(ICredentialsMainService, new SyncDescriptor(CredentialsWebMainService)); instantiationService.invokeFunction(accessor => { - const extensionManagementService = accessor.get(IExtensionManagementService); + const extensionManagementService = accessor.get(INativeServerExtensionManagementService); const extensionsScannerService = accessor.get(IExtensionsScannerService); const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, userDataProfilesService, extensionManagementCLIService, logService, extensionHostStatusService, extensionsScannerService); socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel); @@ -213,7 +213,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken socketServer.registerChannel('credentials', credentialsChannel); // clean up deprecated extensions - (extensionManagementService as ExtensionManagementService).removeDeprecatedExtensions(); + extensionManagementService.removeUninstalledExtensions(true); disposables.add(new ErrorTelemetry(accessor.get(ITelemetryService))); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 6fbc52d18ba42..57969a4ba9bcf 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -75,7 +75,8 @@ import { Event } from 'vs/base/common/event'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { UnsupportedExtensionsMigrationContrib } from 'vs/workbench/contrib/extensions/browser/unsupportedExtensionsMigrationContribution'; import { isWeb } from 'vs/base/common/platform'; -import { ExtensionsCleaner } from 'vs/workbench/contrib/extensions/browser/extensionsCleaner'; +import { ExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; +import { IStorageService } from 'vs/platform/storage/common/storage'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -1559,6 +1560,16 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi } +class ExtensionStorageCleaner implements IWorkbenchContribution { + + constructor( + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IStorageService storageService: IStorageService, + ) { + ExtensionStorageService.removeOutdatedExtensionVersions(extensionManagementService, storageService); + } +} + const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Restored); @@ -1571,7 +1582,7 @@ workbenchRegistry.registerWorkbenchContribution(ExtensionEnablementWorkspaceTrus workbenchRegistry.registerWorkbenchContribution(ExtensionsCompletionItemsProvider, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(UnsupportedExtensionsMigrationContrib, LifecyclePhase.Eventually); if (isWeb) { - workbenchRegistry.registerWorkbenchContribution(ExtensionsCleaner, LifecyclePhase.Eventually); + workbenchRegistry.registerWorkbenchContribution(ExtensionStorageCleaner, LifecyclePhase.Eventually); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsCleaner.ts b/src/vs/workbench/contrib/extensions/browser/extensionsCleaner.ts deleted file mode 100644 index 2d9b7872491be..0000000000000 --- a/src/vs/workbench/contrib/extensions/browser/extensionsCleaner.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; - -export class ExtensionsCleaner implements IWorkbenchContribution { - - constructor( - @IExtensionManagementService extensionManagementService: IExtensionManagementService, - @IStorageService storageService: IStorageService, - ) { - ExtensionStorageService.removeOutdatedExtensionVersions(extensionManagementService, storageService); - } -} diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 6c1cba6e975bb..147936d4468f8 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionType, IExtension, IExtensionIdentifier, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; -import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IGalleryMetadata, InstallOperation, IExtensionGalleryService, InstallOptions, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IGalleryMetadata, InstallOperation, IExtensionGalleryService, InstallOptions, Metadata, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IProfileAwareExtensionManagementService, IScannedExtension, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -106,7 +106,7 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe return new InstallExtensionTask(manifest, extension, options, this.webExtensionsScannerService); } - protected createDefaultUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask { + protected createDefaultUninstallExtensionTask(extension: ILocalExtension, options: UninstallOptions): IUninstallExtensionTask { return new UninstallExtensionTask(extension, options, this.webExtensionsScannerService); } @@ -191,7 +191,7 @@ class UninstallExtensionTask extends AbstractExtensionTask implements IUni constructor( readonly extension: ILocalExtension, - options: UninstallExtensionTaskOptions, + options: UninstallOptions, private readonly webExtensionsScannerService: IWebExtensionsScannerService, ) { super(); From 2fdf21622bab057bb9095a3fd62783f636564e01 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sun, 26 Jun 2022 00:19:35 +0200 Subject: [PATCH 2/4] reset when all profiles are removed --- .../sharedProcess/contrib/extensionsCleaner.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts index 3ac907be4588a..1900df95a384b 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts @@ -44,7 +44,7 @@ export class ExtensionsCleaner extends Disposable { class ProfileExtensionsCleaner extends Disposable { private profileExtensionsLocations = new Map; - private readonly initPromise: Promise; + private initPromise: Promise; constructor( @INativeServerExtensionManagementService private readonly extensionManagementService: INativeServerExtensionManagementService, @@ -62,6 +62,7 @@ class ProfileExtensionsCleaner extends Disposable { } private async initialize(): Promise { + this.profileExtensionsLocations.clear(); if (this.userDataProfilesService.profiles.length === 1) { return true; } @@ -80,12 +81,15 @@ class ProfileExtensionsCleaner extends Disposable { } } - private async onDidChangeProfiles({ added, removed }: DidChangeProfilesEvent): Promise { + private async onDidChangeProfiles({ added, removed, all }: DidChangeProfilesEvent): Promise { if (!(await this.initPromise)) { return; } await Promise.all(added.map(profile => profile.extensionsResource ? this.populateExtensionsFromProfile(profile.extensionsResource) : Promise.resolve())); await Promise.all(removed.map(profile => profile.extensionsResource ? this.removeExtensionsFromProfile(profile.extensionsResource) : Promise.resolve())); + if (all.length === 1) { + this.initPromise = this.initialize(); + } } private async onDidInstallExtensions(installedExtensions: readonly ServerInstallExtensionResult[]): Promise { From 36f67de44f9dfc40746b31e5cbc0a27c21e0f39f Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sun, 26 Jun 2022 00:21:39 +0200 Subject: [PATCH 3/4] reset only map --- .../sharedProcess/contrib/extensionsCleaner.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts index 1900df95a384b..ea6ed685b29d0 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts @@ -62,7 +62,6 @@ class ProfileExtensionsCleaner extends Disposable { } private async initialize(): Promise { - this.profileExtensionsLocations.clear(); if (this.userDataProfilesService.profiles.length === 1) { return true; } @@ -88,7 +87,7 @@ class ProfileExtensionsCleaner extends Disposable { await Promise.all(added.map(profile => profile.extensionsResource ? this.populateExtensionsFromProfile(profile.extensionsResource) : Promise.resolve())); await Promise.all(removed.map(profile => profile.extensionsResource ? this.removeExtensionsFromProfile(profile.extensionsResource) : Promise.resolve())); if (all.length === 1) { - this.initPromise = this.initialize(); + this.profileExtensionsLocations.clear(); } } From 59ca0494c8e78c777363bc4135b9d3049172aa3b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sun, 26 Jun 2022 02:05:23 +0200 Subject: [PATCH 4/4] - fix clean up logic - fix installing other versions do not uninstall in profile mode - add more logging --- .../contrib/extensionsCleaner.ts | 90 +++++++++++-------- .../abstractExtensionManagementService.ts | 16 ++-- .../node/extensionManagementService.ts | 4 +- 3 files changed, 65 insertions(+), 45 deletions(-) diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts index ea6ed685b29d0..e266b17342577 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IExtensionGalleryService, IExtensionIdentifier, IGlobalExtensionEnablementService, ServerDidUninstallExtensionEvent, ServerInstallExtensionResult, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { getIdAndVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { migrateUnsupportedExtensions } from 'vs/platform/extensionManagement/common/unsupportedExtensionsMigration'; @@ -44,7 +45,8 @@ export class ExtensionsCleaner extends Disposable { class ProfileExtensionsCleaner extends Disposable { private profileExtensionsLocations = new Map; - private initPromise: Promise; + + private readonly profileModeDisposables = this._register(new MutableDisposable()); constructor( @INativeServerExtensionManagementService private readonly extensionManagementService: INativeServerExtensionManagementService, @@ -54,47 +56,54 @@ class ProfileExtensionsCleaner extends Disposable { @ILogService private readonly logService: ILogService, ) { super(); - - this.initPromise = this.initialize(); - this._register(this.userDataProfilesService.onDidChangeProfiles(e => this.onDidChangeProfiles(e))); - this._register(this.extensionManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e))); - this._register(this.extensionManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e))); + this.onDidChangeProfiles({ added: this.userDataProfilesService.profiles, removed: [], all: this.userDataProfilesService.profiles }); } - private async initialize(): Promise { - if (this.userDataProfilesService.profiles.length === 1) { - return true; + private async onDidChangeProfiles({ added, removed, all }: DidChangeProfilesEvent): Promise { + try { + await Promise.all(removed.map(profile => profile.extensionsResource ? this.removeExtensionsFromProfile(profile.extensionsResource) : Promise.resolve())); + } catch (error) { + this.logService.error(error); } + + if (all.length === 0) { + // Exit profile mode + this.profileModeDisposables.clear(); + // Listen for entering into profile mode + const disposable = this._register(this.userDataProfilesService.onDidChangeProfiles(() => { + disposable.dispose(); + this.onDidChangeProfiles({ added: this.userDataProfilesService.profiles, removed: [], all: this.userDataProfilesService.profiles }); + })); + return; + } + try { - const installed = await this.extensionManagementService.getAllUserInstalled(); - await Promise.all(this.userDataProfilesService.profiles.map(profile => profile.extensionsResource ? this.populateExtensionsFromProfile(profile.extensionsResource) : Promise.resolve())); - const toUninstall = installed.filter(installedExtension => !this.profileExtensionsLocations.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version))); - if (toUninstall.length) { - await Promise.all(toUninstall.map(extension => this.extensionManagementService.uninstall(extension, uninstalOptions))); + if (added.length) { + await Promise.all(added.map(profile => profile.extensionsResource ? this.populateExtensionsFromProfile(profile.extensionsResource) : Promise.resolve())); + // Enter profile mode + if (!this.profileModeDisposables.value) { + this.profileModeDisposables.value = new DisposableStore(); + this.profileModeDisposables.value.add(toDisposable(() => this.profileExtensionsLocations.clear())); + this.profileModeDisposables.value.add(this.userDataProfilesService.onDidChangeProfiles(e => this.onDidChangeProfiles(e))); + this.profileModeDisposables.value.add(this.extensionManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e))); + this.profileModeDisposables.value.add(this.extensionManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e))); + await this.uninstallExtensionsNotInProfiles(); + } } - return true; } catch (error) { - this.logService.error('ExtensionsCleaner: Failed to initialize'); this.logService.error(error); - return false; } } - private async onDidChangeProfiles({ added, removed, all }: DidChangeProfilesEvent): Promise { - if (!(await this.initPromise)) { - return; - } - await Promise.all(added.map(profile => profile.extensionsResource ? this.populateExtensionsFromProfile(profile.extensionsResource) : Promise.resolve())); - await Promise.all(removed.map(profile => profile.extensionsResource ? this.removeExtensionsFromProfile(profile.extensionsResource) : Promise.resolve())); - if (all.length === 1) { - this.profileExtensionsLocations.clear(); + private async uninstallExtensionsNotInProfiles(): Promise { + const installed = await this.extensionManagementService.getAllUserInstalled(); + const toUninstall = installed.filter(installedExtension => !this.profileExtensionsLocations.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version))); + if (toUninstall.length) { + await Promise.all(toUninstall.map(extension => this.extensionManagementService.uninstall(extension, uninstalOptions))); } } private async onDidInstallExtensions(installedExtensions: readonly ServerInstallExtensionResult[]): Promise { - if (!(await this.initPromise)) { - return; - } for (const { local, profileLocation } of installedExtensions) { if (!local || !profileLocation) { continue; @@ -107,9 +116,6 @@ class ProfileExtensionsCleaner extends Disposable { if (!e.profileLocation || !e.version) { return; } - if (!(await this.initPromise)) { - return; - } if (this.removeExtensionWithKey(this.getKey(e.identifier, e.version), e.profileLocation)) { await this.uninstallExtensions([{ identifier: e.identifier, version: e.version }]); } @@ -123,8 +129,16 @@ class ProfileExtensionsCleaner extends Disposable { } private async removeExtensionsFromProfile(removedProfile: URI): Promise { - const profileExtensions = await this.extensionsProfileScannerService.scanProfileExtensions(removedProfile); - const extensionsToRemove = profileExtensions.filter(profileExtension => this.removeExtensionWithKey(this.getKey(profileExtension.identifier, profileExtension.version), removedProfile)); + const extensionsToRemove: { identifier: IExtensionIdentifier; version: string }[] = []; + for (const key of [...this.profileExtensionsLocations.keys()]) { + if (!this.removeExtensionWithKey(key, removedProfile)) { + continue; + } + const extensionToRemove = this.fromKey(key); + if (extensionToRemove) { + extensionsToRemove.push(extensionToRemove); + } + } if (extensionsToRemove.length) { await this.uninstallExtensions(extensionsToRemove); } @@ -149,8 +163,9 @@ class ProfileExtensionsCleaner extends Disposable { } if (!profiles?.length) { this.profileExtensionsLocations.delete(key); + return true; } - return !profiles?.length; + return false; } private async uninstallExtensions(extensionsToRemove: { identifier: IExtensionIdentifier; version: string }[]): Promise { @@ -165,4 +180,9 @@ class ProfileExtensionsCleaner extends Disposable { return `${ExtensionIdentifier.toKey(identifier.id)}@${version}`; } + private fromKey(key: string): { identifier: IExtensionIdentifier; version: string } | undefined { + const [id, version] = getIdAndVersion(key); + return version ? { identifier: { id }, version } : undefined; + } + } diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index c31a9e3c4270a..5e5353eed1fd5 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -436,9 +436,9 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const uninstallExtensionTask = this.createUninstallExtensionTask(extension, uninstallOptions, options.profileLocation); this.uninstallingExtensions.set(getUninstallExtensionTaskKey(uninstallExtensionTask.extension.identifier), uninstallExtensionTask); if (options.profileLocation) { - this.logService.info('Uninstalling extension from the profile:', extension.identifier.id, options.profileLocation.toString()); + this.logService.info('Uninstalling extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, options.profileLocation.toString()); } else { - this.logService.info('Uninstalling extension:', extension.identifier.id); + this.logService.info('Uninstalling extension:', `${extension.identifier.id}@${extension.manifest.version}`); } this._onUninstallExtension.fire({ identifier: extension.identifier, profileLocation: options.profileLocation, applicationScoped: extension.isApplicationScoped }); return uninstallExtensionTask; @@ -447,15 +447,15 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const postUninstallExtension = (extension: ILocalExtension, error?: ExtensionManagementError): void => { if (error) { if (options.profileLocation) { - this.logService.error('Failed to uninstall extension from the profile:', extension.identifier.id, options.profileLocation.toString(), error.message); + this.logService.error('Failed to uninstall extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, options.profileLocation.toString(), error.message); } else { - this.logService.error('Failed to uninstall extension:', extension.identifier.id, error.message); + this.logService.error('Failed to uninstall extension:', `${extension.identifier.id}@${extension.manifest.version}`, error.message); } } else { if (options.profileLocation) { - this.logService.info('Successfully uninstalled extension from the profile', extension.identifier.id, options.profileLocation.toString()); + this.logService.info('Successfully uninstalled extension from the profile', `${extension.identifier.id}@${extension.manifest.version}`, options.profileLocation.toString()); } else { - this.logService.info('Successfully uninstalled extension:', extension.identifier.id); + this.logService.info('Successfully uninstalled extension:', `${extension.identifier.id}@${extension.manifest.version}`); } } reportTelemetry(this.telemetryService, 'extensionGallery:uninstall', { extensionData: getLocalExtensionTelemetryData(extension), error }); @@ -469,7 +469,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl allTasks.push(createUninstallExtensionTask(extension, options)); const installed = await this.getInstalled(ExtensionType.User, options.profileLocation); if (options.donotIncludePack) { - this.logService.info('Uninstalling the extension without including packed extension', extension.identifier.id); + this.logService.info('Uninstalling the extension without including packed extension', `${extension.identifier.id}@${extension.manifest.version}`); } else { const packedExtensions = this.getAllPackExtensionsToUninstall(extension, installed); for (const packedExtension of packedExtensions) { @@ -482,7 +482,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } if (options.donotCheckDependents) { - this.logService.info('Uninstalling the extension without checking dependents', extension.identifier.id); + this.logService.info('Uninstalling the extension without checking dependents', `${extension.identifier.id}@${extension.manifest.version}`); } else { this.checkForDependents(allTasks.map(task => task.extension), installed, extension); } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 3eadb31770b30..56ea181d99eba 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -595,7 +595,7 @@ class InstallGalleryExtensionTask extends InstallExtensionTask { const zipPath = await this.downloadExtension(this.gallery, this._operation); try { const local = await this.installExtension({ zipPath, key: ExtensionKey.create(this.gallery), metadata }, token); - if (existingExtension && (existingExtension.targetPlatform !== local.targetPlatform || semver.neq(existingExtension.manifest.version, local.manifest.version))) { + if (existingExtension && !this.options.profileLocation && (existingExtension.targetPlatform !== local.targetPlatform || semver.neq(existingExtension.manifest.version, local.manifest.version))) { await this.extensionsScanner.setUninstalled(existingExtension); } return { local, metadata }; @@ -664,7 +664,7 @@ class InstallVSIXTask extends InstallExtensionTask { } catch (e) { throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name)); } - } else if (semver.gt(existing.manifest.version, this.manifest.version)) { + } else if (!this.options.profileLocation && semver.gt(existing.manifest.version, this.manifest.version)) { await this.extensionsScanner.setUninstalled(existing); } } else {