diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/extensions.ts b/src/vs/code/electron-browser/sharedProcess/contrib/extensions.ts new file mode 100644 index 0000000000000..2ae46cb355a36 --- /dev/null +++ b/src/vs/code/electron-browser/sharedProcess/contrib/extensions.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { DEFAULT_PROFILE_EXTENSIONS_MIGRATION_KEY, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; +import { migrateUnsupportedExtensions } from 'vs/platform/extensionManagement/common/unsupportedExtensionsMigration'; +import { INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; + +export class ExtensionsContributions extends Disposable { + constructor( + @INativeServerExtensionManagementService extensionManagementService: INativeServerExtensionManagementService, + @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, + @IExtensionStorageService extensionStorageService: IExtensionStorageService, + @IGlobalExtensionEnablementService extensionEnablementService: IGlobalExtensionEnablementService, + @IStorageService storageService: IStorageService, + @ILogService logService: ILogService, + ) { + super(); + + extensionManagementService.migrateDefaultProfileExtensions() + .then(() => storageService.store(DEFAULT_PROFILE_EXTENSIONS_MIGRATION_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE), error => null); + + extensionManagementService.removeUninstalledExtensions(); + migrateUnsupportedExtensions(extensionManagementService, extensionGalleryService, extensionStorageService, extensionEnablementService, logService); + ExtensionStorageService.removeOutdatedExtensionVersions(extensionManagementService, storageService); + } + +} diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts deleted file mode 100644 index 7da094c59650b..0000000000000 --- a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts +++ /dev/null @@ -1,186 +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 { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { IExtensionGalleryService, IExtensionIdentifier, IGlobalExtensionEnablementService, DidUninstallExtensionEvent, InstallExtensionResult, 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'; -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( - @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.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 profileModeDisposables = this._register(new MutableDisposable()); - - 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.onDidChangeProfiles({ added: this.userDataProfilesService.profiles, removed: [], all: this.userDataProfilesService.profiles }); - } - - private async onDidChangeProfiles({ added, removed, all }: Omit): 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 === 1) { - // 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 { - 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(); - } - } - } catch (error) { - this.logService.error(error); - } - } - - private async uninstallExtensionsNotInProfiles(): Promise { - const installed = await this.extensionManagementService.getAllUserInstalled(); - const toUninstall = installed.filter(installedExtension => installedExtension.installedTimestamp /* Installed by VS Code */ && !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 InstallExtensionResult[]): Promise { - for (const { local, profileLocation } of installedExtensions) { - if (!local || !profileLocation) { - continue; - } - this.addExtensionWithKey(this.getKey(local.identifier, local.manifest.version), profileLocation); - } - } - - private async onDidUninstallExtension(e: DidUninstallExtensionEvent): Promise { - if (!e.profileLocation || !e.version) { - 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 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); - } - } - - 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 true; - } - return false; - } - - private async uninstallExtensions(extensionsToRemove: { identifier: IExtensionIdentifier; version: string }[]): Promise { - const installed = await this.extensionManagementService.getAllUserInstalled(); - const toUninstall = installed.filter(installedExtension => installedExtension.installedTimestamp /* Installed by VS Code */ && 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}`; - } - - 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/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index f5bd9adc5a709..8c904e2530808 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -14,7 +14,6 @@ import { URI } from 'vs/base/common/uri'; import { ProxyChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-browser/ipc.mp'; import { CodeCacheCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/codeCacheCleaner'; -import { ExtensionsCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner'; import { LanguagePackCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner'; import { LocalizationsUpdater } from 'vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater'; import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner'; @@ -113,6 +112,7 @@ import { RemoteTunnelService } from 'vs/platform/remoteTunnel/electron-browser/r import { IRemoteTunnelService } from 'vs/platform/remoteTunnel/common/remoteTunnel'; import { ISharedProcessLifecycleService, SharedProcessLifecycleService } from 'vs/platform/lifecycle/electron-browser/sharedProcessLifecycleService'; import { UserDataSyncResourceProviderService } from 'vs/platform/userDataSync/common/userDataSyncResourceProvider'; +import { ExtensionsContributions } from 'vs/code/electron-browser/sharedProcess/contrib/extensions'; class SharedProcessMain extends Disposable { @@ -186,7 +186,7 @@ class SharedProcessMain extends Disposable { instantiationService.createInstance(UnusedWorkspaceStorageDataCleaner), instantiationService.createInstance(LogsDataCleaner), instantiationService.createInstance(LocalizationsUpdater), - instantiationService.createInstance(ExtensionsCleaner), + instantiationService.createInstance(ExtensionsContributions), instantiationService.createInstance(UserDataProfilesCleaner) )); } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index b8e21690c1eb6..a9d491c490808 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -101,7 +101,6 @@ import { CredentialsNativeMainService } from 'vs/platform/credentials/electron-m import { IPolicyService } from 'vs/platform/policy/common/policy'; import { PolicyChannel } from 'vs/platform/policy/common/policyIpc'; import { IUserDataProfilesMainService } from 'vs/platform/userDataProfile/electron-main/userDataProfile'; -import { DefaultExtensionsProfileInitHandler } from 'vs/platform/extensionManagement/electron-main/defaultExtensionsProfileInit'; import { RequestChannel } from 'vs/platform/request/common/requestIpc'; import { IRequestService } from 'vs/platform/request/common/request'; import { ExtensionsProfileScannerService, IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; @@ -558,9 +557,6 @@ export class CodeApplication extends Disposable { // Auth Handler this._register(instantiationService.createInstance(ProxyAuthHandler)); - // Default Extensions Profile Init Handler - this._register(instantiationService.createInstance(DefaultExtensionsProfileInitHandler)); - // Transient profiles handler this._register(instantiationService.createInstance(UserDataTransientProfilesHandler)); } diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 6c65566f766e7..3cf630f878fdf 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -245,7 +245,7 @@ class CliMain extends Disposable { } private async doRun(environmentService: INativeEnvironmentService, fileService: IFileService, userDataProfilesService: IUserDataProfilesService, instantiationService: IInstantiationService): Promise { - const profileLocation = environmentService.args.profile ? userDataProfilesService.profiles.find(p => p.name === environmentService.args.profile)?.extensionsResource : userDataProfilesService.defaultProfile.extensionsResource; + const profileLocation = (environmentService.args.profile ? userDataProfilesService.profiles.find(p => p.name === environmentService.args.profile) ?? userDataProfilesService.defaultProfile : userDataProfilesService.defaultProfile).extensionsResource; // Install Source if (this.argv['install-source']) { diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 52696de6e8176..183e4b527c640 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -24,6 +24,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +export type InstallExtensionTaskOptions = InstallOptions & InstallVSIXOptions & { readonly profileLocation: URI }; export interface IInstallExtensionTask { readonly identifier: IExtensionIdentifier; readonly source: IGalleryExtension | URI; @@ -34,6 +35,7 @@ export interface IInstallExtensionTask { cancel(): void; } +export type UninstallExtensionTaskOptions = UninstallOptions & { readonly profileLocation: URI }; export interface IUninstallExtensionTask { readonly extension: ILocalExtension; run(): Promise; @@ -105,22 +107,6 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return this.uninstallExtension(extension, options); } - async reinstallFromGallery(extension: ILocalExtension): Promise { - this.logService.trace('ExtensionManagementService#reinstallFromGallery', extension.identifier.id); - if (!this.galleryService.isEnabled()) { - throw new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled")); - } - - const targetPlatform = await this.getTargetPlatform(); - const [galleryExtension] = await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: extension.preRelease }], { targetPlatform, compatible: true }, CancellationToken.None); - if (!galleryExtension) { - throw new Error(nls.localize('Not a Marketplace extension', "Only Marketplace Extensions can be reinstalled")); - } - - await this.createUninstallExtensionTask(extension, { remove: true, versionOnly: true }).run(); - await this.installFromGallery(galleryExtension); - } - getExtensionsControlManifest(): Promise { const now = new Date().getTime(); @@ -138,7 +124,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl protected async installExtension(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions & InstallVSIXOptions): Promise { - const getInstallExtensionTaskKey = (extension: IGalleryExtension) => `${ExtensionKey.create(extension).toString()}${options.profileLocation ? `-${options.profileLocation.toString()}` : ''}`; + const installExtensionTaskOptions: InstallExtensionTaskOptions = { + ...options, + installOnlyNewlyAddedFromExtensionPack: URI.isUri(extension) ? options.installOnlyNewlyAddedFromExtensionPack : true, /* always true for gallery extensions */ + profileLocation: !options.profileLocation || isApplicationScopedExtension(manifest) ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation + }; + const getInstallExtensionTaskKey = (extension: IGalleryExtension) => `${ExtensionKey.create(extension).toString()}${installExtensionTaskOptions.profileLocation ? `-${installExtensionTaskOptions.profileLocation.toString()}` : ''}`; // only cache gallery extensions tasks if (!URI.isUri(extension)) { @@ -148,28 +139,27 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const { local } = await installingExtension.task.waitUntilTaskIsFinished(); return local; } - options = { ...options, installOnlyNewlyAddedFromExtensionPack: true /* always true for gallery extensions */ }; } const allInstallExtensionTasks: { task: IInstallExtensionTask; manifest: IExtensionManifest }[] = []; const alreadyRequestedInstallations: Promise[] = []; const installResults: (InstallExtensionResult & { local: ILocalExtension })[] = []; - const installExtensionTask = this.createInstallExtensionTask(manifest, extension, options); + const installExtensionTask = this.createInstallExtensionTask(manifest, extension, installExtensionTaskOptions); if (!URI.isUri(extension)) { this.installingExtensions.set(getInstallExtensionTaskKey(extension), { task: installExtensionTask, waitingTasks: [] }); } - this._onInstallExtension.fire({ identifier: installExtensionTask.identifier, source: extension, profileLocation: options.profileLocation }); + this._onInstallExtension.fire({ identifier: installExtensionTask.identifier, source: extension, profileLocation: installExtensionTaskOptions.profileLocation }); this.logService.info('Installing extension:', installExtensionTask.identifier.id); allInstallExtensionTasks.push({ task: installExtensionTask, manifest }); let installExtensionHasDependents: boolean = false; try { - if (options.donotIncludePackAndDependencies) { + if (installExtensionTaskOptions.donotIncludePackAndDependencies) { this.logService.info('Installing the extension without checking dependencies and pack', installExtensionTask.identifier.id); } else { try { - const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(installExtensionTask.identifier, manifest, !!options.installOnlyNewlyAddedFromExtensionPack, !!options.installPreReleaseVersion, options.profileLocation); - const installed = await this.getInstalled(undefined, options.profileLocation); + const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(installExtensionTask.identifier, manifest, !!installExtensionTaskOptions.installOnlyNewlyAddedFromExtensionPack, !!installExtensionTaskOptions.installPreReleaseVersion, installExtensionTaskOptions.profileLocation); + const installed = await this.getInstalled(undefined, installExtensionTaskOptions.profileLocation); for (const { gallery, manifest } of allDepsAndPackExtensionsToInstall) { installExtensionHasDependents = installExtensionHasDependents || !!manifest.extensionDependencies?.some(id => areSameExtensions({ id }, installExtensionTask.identifier)); const key = getInstallExtensionTaskKey(gallery); @@ -192,9 +182,9 @@ export abstract class AbstractExtensionManagementService extends Disposable impl })); } } else if (!installed.some(({ identifier }) => areSameExtensions(identifier, gallery.identifier))) { - const task = this.createInstallExtensionTask(manifest, gallery, { ...options, donotIncludePackAndDependencies: true }); + const task = this.createInstallExtensionTask(manifest, gallery, { ...installExtensionTaskOptions, donotIncludePackAndDependencies: true }); this.installingExtensions.set(key, { task, waitingTasks: [installExtensionTask] }); - this._onInstallExtension.fire({ identifier: task.identifier, source: gallery, profileLocation: options.profileLocation }); + this._onInstallExtension.fire({ identifier: task.identifier, source: gallery, profileLocation: installExtensionTaskOptions.profileLocation }); this.logService.info('Installing extension:', task.identifier.id, installExtensionTask.identifier.id); allInstallExtensionTasks.push({ task, manifest }); } @@ -238,7 +228,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const startTime = new Date().getTime(); try { const { local } = await task.run(); - await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, options, CancellationToken.None))); + await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, installExtensionTaskOptions, CancellationToken.None))); if (!URI.isUri(task.source)) { const isUpdate = task.operation === InstallOperation.Update; const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; @@ -255,7 +245,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } catch (error) { /* ignore */ } } } - installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source, context: options.context, profileLocation: options.profileLocation, applicationScoped: local.isApplicationScoped }); + installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source, context: installExtensionTaskOptions.context, profileLocation: installExtensionTaskOptions.profileLocation, applicationScoped: local.isApplicationScoped }); } catch (error) { if (!URI.isUri(task.source)) { reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { @@ -287,7 +277,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl // rollback installed extensions if (installResults.length) { try { - const result = await Promise.allSettled(installResults.map(({ local }) => this.createUninstallExtensionTask(local, { versionOnly: true, profileLocation: options.profileLocation }).run())); + const result = await Promise.allSettled(installResults.map(({ local }) => this.createUninstallExtensionTask(local, { versionOnly: true, profileLocation: installExtensionTaskOptions.profileLocation }).run())); for (let index = 0; index < result.length; index++) { const r = result[index]; const { identifier } = installResults[index]; @@ -303,7 +293,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } } - this._onDidInstallExtensions.fire(allInstallExtensionTasks.map(({ task }) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: options.context, profileLocation: options.profileLocation }))); + this._onDidInstallExtensions.fire(allInstallExtensionTasks.map(({ task }) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: installExtensionTaskOptions.context, profileLocation: installExtensionTaskOptions.profileLocation }))); throw error; } finally { for (const [key, { task, waitingTasks }] of this.installingExtensions.entries()) { @@ -475,50 +465,54 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } private async uninstallExtension(extension: ILocalExtension, options: UninstallOptions): Promise { - const getUninstallExtensionTaskKey = (identifier: IExtensionIdentifier) => `${identifier.id.toLowerCase()}${options.versionOnly ? `-${extension.manifest.version}` : ''}${options.profileLocation ? `@${options.profileLocation.toString()}` : ''}`; + const uninstallOptions: UninstallExtensionTaskOptions = { + ...options, + profileLocation: !options.profileLocation || extension.isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation + }; + const getUninstallExtensionTaskKey = (identifier: IExtensionIdentifier) => `${identifier.id.toLowerCase()}${uninstallOptions.versionOnly ? `-${extension.manifest.version}` : ''}${uninstallOptions.profileLocation ? `@${uninstallOptions.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: UninstallOptions): IUninstallExtensionTask => { + const createUninstallExtensionTask = (extension: ILocalExtension): IUninstallExtensionTask => { const uninstallExtensionTask = this.createUninstallExtensionTask(extension, uninstallOptions); this.uninstallingExtensions.set(getUninstallExtensionTaskKey(uninstallExtensionTask.extension.identifier), uninstallExtensionTask); - if (options.profileLocation) { - this.logService.info('Uninstalling extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, options.profileLocation.toString()); + if (uninstallOptions.profileLocation) { + this.logService.info('Uninstalling extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString()); } else { this.logService.info('Uninstalling extension:', `${extension.identifier.id}@${extension.manifest.version}`); } - this._onUninstallExtension.fire({ identifier: extension.identifier, profileLocation: options.profileLocation, applicationScoped: extension.isApplicationScoped }); + this._onUninstallExtension.fire({ identifier: extension.identifier, profileLocation: uninstallOptions.profileLocation, applicationScoped: extension.isApplicationScoped }); return uninstallExtensionTask; }; 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}@${extension.manifest.version}`, options.profileLocation.toString(), error.message); + if (uninstallOptions.profileLocation) { + this.logService.error('Failed to uninstall extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString(), error.message); } else { 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}@${extension.manifest.version}`, options.profileLocation.toString()); + if (uninstallOptions.profileLocation) { + this.logService.info('Successfully uninstalled extension from the profile', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString()); } else { this.logService.info('Successfully uninstalled extension:', `${extension.identifier.id}@${extension.manifest.version}`); } } reportTelemetry(this.telemetryService, 'extensionGallery:uninstall', { extensionData: getLocalExtensionTelemetryData(extension), error }); - this._onDidUninstallExtension.fire({ identifier: extension.identifier, version: extension.manifest.version, error: error?.code, profileLocation: options.profileLocation, applicationScoped: extension.isApplicationScoped }); + this._onDidUninstallExtension.fire({ identifier: extension.identifier, version: extension.manifest.version, error: error?.code, profileLocation: uninstallOptions.profileLocation, applicationScoped: extension.isApplicationScoped }); }; const allTasks: IUninstallExtensionTask[] = []; const processedTasks: IUninstallExtensionTask[] = []; try { - allTasks.push(createUninstallExtensionTask(extension, options)); - const installed = await this.getInstalled(ExtensionType.User, options.profileLocation); - if (options.donotIncludePack) { + allTasks.push(createUninstallExtensionTask(extension)); + const installed = await this.getInstalled(ExtensionType.User, uninstallOptions.profileLocation); + if (uninstallOptions.donotIncludePack) { this.logService.info('Uninstalling the extension without including packed extension', `${extension.identifier.id}@${extension.manifest.version}`); } else { const packedExtensions = this.getAllPackExtensionsToUninstall(extension, installed); @@ -526,12 +520,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl if (this.uninstallingExtensions.has(getUninstallExtensionTaskKey(packedExtension.identifier))) { this.logService.info('Extensions is already requested to uninstall', packedExtension.identifier.id); } else { - allTasks.push(createUninstallExtensionTask(packedExtension, options)); + allTasks.push(createUninstallExtensionTask(packedExtension)); } } } - if (options.donotCheckDependents) { + if (uninstallOptions.donotCheckDependents) { 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); @@ -541,7 +535,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl await this.joinAllSettled(allTasks.map(async task => { try { await task.run(); - await this.joinAllSettled(this.participants.map(participant => participant.postUninstall(task.extension, options, CancellationToken.None))); + await this.joinAllSettled(this.participants.map(participant => participant.postUninstall(task.extension, uninstallOptions, CancellationToken.None))); // only report if extension has a mapped gallery extension. UUID identifies the gallery extension. if (task.extension.identifier.uuid) { try { @@ -652,20 +646,6 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } } - private createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions & InstallVSIXOptions): IInstallExtensionTask { - if (options.profileLocation && isApplicationScopedExtension(manifest)) { - options = { ...options, profileLocation: this.userDataProfilesService.defaultProfile.extensionsResource }; - } - return this.doCreateInstallExtensionTask(manifest, extension, options); - } - - private createUninstallExtensionTask(extension: ILocalExtension, options: UninstallOptions): IUninstallExtensionTask { - if (options.profileLocation && extension.isApplicationScoped) { - options = { ...options, profileLocation: this.userDataProfilesService.defaultProfile.extensionsResource }; - } - return this.doCreateUninstallExtensionTask(extension, options); - } - abstract getTargetPlatform(): Promise; abstract zip(extension: ILocalExtension): Promise; abstract unzip(zipLocation: URI): Promise; @@ -673,13 +653,14 @@ export abstract class AbstractExtensionManagementService extends Disposable impl abstract install(vsix: URI, options?: InstallVSIXOptions): Promise; abstract getInstalled(type?: ExtensionType, profileLocation?: URI): Promise; abstract download(extension: IGalleryExtension, operation: InstallOperation): Promise; + abstract reinstallFromGallery(extension: ILocalExtension): Promise; abstract getMetadata(extension: ILocalExtension): Promise; abstract updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise; abstract updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise; - protected abstract doCreateInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions & InstallVSIXOptions): IInstallExtensionTask; - protected abstract doCreateUninstallExtensionTask(extension: ILocalExtension, options: UninstallOptions): IUninstallExtensionTask; + protected abstract createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask; + protected abstract createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): 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 ba3a2b083ce58..89907b63d3c19 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -16,6 +16,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export const EXTENSION_IDENTIFIER_PATTERN = '^([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$'; export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTERN); export const WEB_EXTENSION_TAG = '__web_extension'; +export const DEFAULT_PROFILE_EXTENSIONS_MIGRATION_KEY = 'DEFAULT_PROFILE_EXTENSIONS_MIGRATION'; export function TargetPlatformToString(targetPlatform: TargetPlatform) { switch (targetPlatform) { @@ -498,13 +499,11 @@ export interface IExtensionTipsService { getAllWorkspacesTips(): Promise; } - export const ExtensionsLabel = localize('extensions', "Extensions"); export const ExtensionsLocalizedLabel = { value: ExtensionsLabel, original: 'Extensions' }; export const PreferencesLabel = localize('preferences', "Preferences"); export const PreferencesLocalizedLabel = { value: PreferencesLabel, original: 'Preferences' }; - export interface CLIOutput { log(s: string): void; error(s: string): void; diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index a182f889b9f51..5bcb3ee7cdcc6 100644 --- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -54,7 +54,7 @@ export class ExtensionsProfileScannerService extends Disposable implements IExte return this.withProfileExtensions(profileLocation); } - addExtensionsToProfile(extensions: [IExtension, Metadata][], profileLocation: URI): Promise { + addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise { return this.withProfileExtensions(profileLocation, profileExtensions => { // Remove the existing extension to avoid duplicates profileExtensions = profileExtensions.filter(e => extensions.some(([extension]) => !areSameExtensions(e.identifier, extension.identifier))); diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 9ecfff2b3219d..3a49730edd0fc 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -190,8 +190,9 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanUserExtensions(scanOptions: ScanOptions): Promise { - this.logService.trace('Started scanning user extensions'); - const extensionsScannerInput = await this.createExtensionScannerInput(scanOptions.profileLocation ?? this.userExtensionsLocation, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language); + const location = scanOptions.profileLocation ?? this.userExtensionsLocation; + this.logService.trace('Started scanning user extensions', location); + const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language); const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode && extensionsScannerInput.excludeObsolete ? this.userExtensionsCachedScanner : this.extensionsScanner; let extensions = await extensionsScanner.scanExtensions(extensionsScannerInput); extensions = await this.applyScanOptions(extensions, ExtensionType.User, scanOptions, true); diff --git a/src/vs/platform/extensionManagement/electron-main/defaultExtensionsProfileInit.ts b/src/vs/platform/extensionManagement/electron-main/defaultExtensionsProfileInit.ts deleted file mode 100644 index 12e6315732003..0000000000000 --- a/src/vs/platform/extensionManagement/electron-main/defaultExtensionsProfileInit.ts +++ /dev/null @@ -1,57 +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 { Disposable } from 'vs/base/common/lifecycle'; -import { joinPath } from 'vs/base/common/resources'; -import { URI } from 'vs/base/common/uri'; -import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; -import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; -import { IFileService } from 'vs/platform/files/common/files'; -import { ILogService } from 'vs/platform/log/common/log'; -import { EXTENSIONS_RESOURCE_NAME } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { IUserDataProfilesMainService } from 'vs/platform/userDataProfile/electron-main/userDataProfile'; - -export class DefaultExtensionsProfileInitHandler extends Disposable { - constructor( - @IUserDataProfilesMainService private readonly userDataProfilesService: IUserDataProfilesMainService, - @IFileService private readonly fileService: IFileService, - @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, - @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, - @ILogService logService: ILogService, - ) { - super(); - if (userDataProfilesService.isEnabled()) { - this._register(userDataProfilesService.onWillCreateProfile(e => { - if (userDataProfilesService.profiles.length === 1) { - e.join(this.initialize()); - } - })); - this._register(userDataProfilesService.onDidChangeProfiles(e => { - if (userDataProfilesService.profiles.length === 1) { - this.uninitialize(); - } - })); - } else { - this.uninitialize().then(null, e => logService.error(e)); - } - } - - private async initialize(): Promise { - /* Create and populate the default extensions profile resource */ - const extensionsProfileResource = this.getDefaultExtensionsProfileResource(); - try { await this.fileService.del(extensionsProfileResource); } catch (error) { /* ignore */ } - const userExtensions = await this.extensionsScannerService.scanUserExtensions({ includeInvalid: true }); - await this.extensionsProfileScannerService.addExtensionsToProfile(userExtensions.map(e => [e, e.metadata]), extensionsProfileResource); - } - - private async uninitialize(): Promise { - /* Remove the default extensions profile resource */ - try { await this.fileService.del(this.getDefaultExtensionsProfileResource()); } catch (error) { /* ignore */ } - } - - private getDefaultExtensionsProfileResource(): URI { - return this.userDataProfilesService.defaultProfile.extensionsResource ?? joinPath(this.userDataProfilesService.defaultProfile.location, EXTENSIONS_RESOURCE_NAME); - } -} diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 520c5667aaef9..683470a4689d2 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Promises, Queue } from 'vs/base/common/async'; +import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStringDictionary } from 'vs/base/common/collections'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -22,10 +23,10 @@ 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 } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, joinErrors, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, - Metadata, InstallOptions, InstallVSIXOptions, UninstallOptions + Metadata, InstallOptions, InstallVSIXOptions } 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'; @@ -54,7 +55,9 @@ interface InstallableExtension { export const INativeServerExtensionManagementService = refineServiceDecorator(IExtensionManagementService); export interface INativeServerExtensionManagementService extends IExtensionManagementService { readonly _serviceBrand: undefined; - removeUninstalledExtensions(removeOutdated: boolean): Promise; + migrateDefaultProfileExtensions(): Promise; + markAsUninstalled(...extensions: ILocalExtension[]): Promise; + removeUninstalledExtensions(): Promise; getAllUserInstalled(): Promise; } @@ -85,13 +88,13 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension))); this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this)); this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader)); - const extensionsWatcher = this._register(new ExtensionsWatcher(this, fileService, environmentService, logService, uriIdentityService)); + const extensionsWatcher = this._register(new ExtensionsWatcher(this, userDataProfilesService, extensionsProfileScannerService, extensionsScannerService, uriIdentityService, fileService, logService)); this._register(extensionsWatcher.onDidChangeExtensionsByAnotherSource(({ added, removed }) => { if (added.length) { - this._onDidInstallExtensions.fire(added.map(local => ({ identifier: local.identifier, operation: InstallOperation.None, local }))); + this._onDidInstallExtensions.fire(added); } - removed.forEach(extension => this._onDidUninstallExtension.fire({ identifier: extension })); + removed.forEach(e => this._onDidUninstallExtension.fire(e)); })); } @@ -131,7 +134,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } getAllUserInstalled(): Promise { - return this.extensionsScanner.scanUserExtensions(false); + return this.extensionsScanner.scanAllUserExtensions(false); } async install(vsix: URI, options: InstallVSIXOptions = {}): Promise { @@ -173,8 +176,37 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return local; } - removeUninstalledExtensions(removeOutdated: boolean): Promise { - return this.extensionsScanner.cleanUp(removeOutdated); + async reinstallFromGallery(extension: ILocalExtension): Promise { + this.logService.trace('ExtensionManagementService#reinstallFromGallery', extension.identifier.id); + if (!this.galleryService.isEnabled()) { + throw new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled")); + } + + const targetPlatform = await this.getTargetPlatform(); + const [galleryExtension] = await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: extension.preRelease }], { targetPlatform, compatible: true }, CancellationToken.None); + if (!galleryExtension) { + throw new Error(nls.localize('Not a Marketplace extension', "Only Marketplace Extensions can be reinstalled")); + } + + await this.extensionsScanner.setUninstalled(extension); + try { + await this.extensionsScanner.removeUninstalledExtension(extension); + } catch (e) { + throw new Error(nls.localize('removeError', "Error while removing the extension: {0}. Please Quit and Start VS Code before trying again.", toErrorMessage(e))); + } + await this.installFromGallery(galleryExtension); + } + + markAsUninstalled(...extensions: ILocalExtension[]): Promise { + return this.extensionsScanner.setUninstalled(...extensions); + } + + removeUninstalledExtensions(): Promise { + return this.extensionsScanner.cleanUp(); + } + + migrateDefaultProfileExtensions(): Promise { + return this.extensionsScanner.migrateDefaultProfileExtensions(); } async download(extension: IGalleryExtension, operation: InstallOperation): Promise { @@ -200,7 +232,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return { location, cleanup }; } - protected doCreateInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions & InstallVSIXOptions): IInstallExtensionTask { + protected createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask { let installExtensionTask: IInstallExtensionTask | undefined; if (URI.isUri(extension)) { installExtensionTask = new InstallVSIXTask(manifest, extension, options, this.galleryService, this.extensionsScanner, this.logService); @@ -212,17 +244,11 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi installExtensionTask.waitUntilTaskIsFinished().then(() => this.installGalleryExtensionsTasks.delete(key)); } } - if (options.profileLocation) { - return new InstallExtensionInProfileTask(installExtensionTask, options.profileLocation, this.extensionsProfileScannerService); - } - return installExtensionTask; + return new InstallExtensionInProfileTask(installExtensionTask, options.profileLocation, this.extensionsProfileScannerService); } - protected doCreateUninstallExtensionTask(extension: ILocalExtension, options: UninstallOptions): IUninstallExtensionTask { - if (options.profileLocation) { - return new UninstallExtensionFromProfileTask(extension, options.profileLocation, this.extensionsProfileScannerService); - } - return new UninstallExtensionTask(extension, options, this.extensionsScanner); + protected createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask { + return new UninstallExtensionFromProfileTask(extension, options.profileLocation, this.extensionsProfileScannerService); } private async collectFiles(extension: ILocalExtension): Promise { @@ -261,6 +287,8 @@ export class ExtensionsScanner extends Disposable { private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise, @IFileService private readonly fileService: IFileService, @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @ILogService private readonly logService: ILogService, ) { super(); @@ -268,14 +296,12 @@ export class ExtensionsScanner extends Disposable { this.uninstalledFileLimiter = new Queue(); } - async cleanUp(removeOutdated: boolean): Promise { + async cleanUp(): Promise { await this.removeUninstalledExtensions(); - if (removeOutdated) { - await this.removeOutdatedExtensions(); - } } async scanExtensions(type: ExtensionType | null, profileLocation: URI | undefined): Promise { + await this.migrateDefaultProfileExtensions(); const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation }; let scannedExtensions: IScannedExtension[] = []; if (type === null || type === ExtensionType.System) { @@ -287,12 +313,13 @@ export class ExtensionsScanner extends Disposable { return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); } - async scanUserExtensions(excludeOutdated: boolean): Promise { + async scanAllUserExtensions(excludeOutdated: boolean): Promise { const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); } async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, metadata: Metadata, token: CancellationToken): Promise { + await this.migrateDefaultProfileExtensions(); const folderName = extensionKey.toString(); const tempPath = path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, `.${generateUuid()}`); const extensionPath = path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, folderName); @@ -335,14 +362,16 @@ export class ExtensionsScanner extends Disposable { async setUninstalled(...extensions: ILocalExtension[]): Promise { const extensionKeys: ExtensionKey[] = extensions.map(e => ExtensionKey.create(e)); - await this.withUninstalledExtensions(uninstalled => { - extensionKeys.forEach(extensionKey => uninstalled[extensionKey.toString()] = true); - }); + await this.withUninstalledExtensions(uninstalled => + extensionKeys.forEach(extensionKey => { + uninstalled[extensionKey.toString()] = true; + this.logService.info('Marked extension as uninstalled', extensionKey.toString()); + })); } async setInstalled(extensionKey: ExtensionKey): Promise { await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); - const userExtensions = await this.scanUserExtensions(true); + const userExtensions = await this.scanAllUserExtensions(true); const localExtension = userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)) || null; if (!localExtension) { return null; @@ -489,35 +518,33 @@ export class ExtensionsScanner extends Disposable { await Promises.settled(toRemove.map(e => this.removeUninstalledExtension(e))); } - private async removeOutdatedExtensions(): Promise { - const systemExtensions = await this.extensionsScannerService.scanSystemExtensions({}); // System extensions - const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions - const toRemove: IScannedExtension[] = []; - - // Outdated extensions - const targetPlatform = await this.extensionsScannerService.getTargetPlatform(); - const byExtension = groupByExtension(extensions, e => e.identifier); - for (const extensions of byExtension) { - if (extensions.length > 1) { - toRemove.push(...extensions.sort((a, b) => { - const vcompare = semver.rcompare(a.manifest.version, b.manifest.version); - if (vcompare !== 0) { - return vcompare; + private _migrateDefaultProfileExtensionsPromise: Promise | undefined = undefined; + migrateDefaultProfileExtensions(): Promise { + if (!this._migrateDefaultProfileExtensionsPromise) { + this._migrateDefaultProfileExtensionsPromise = (async () => { + try { + const migrationMarkerFile = joinPath(this.userDataProfilesService.defaultProfile.location, '.migrated-default-profile'); + if (await this.fileService.exists(migrationMarkerFile)) { + return; } - if (a.targetPlatform === targetPlatform) { - return -1; + if (!(await this.fileService.exists(this.userDataProfilesService.defaultProfile.extensionsResource))) { + this.logService.info('Started migrating default profile extensions from extensions installation folder to extensions manifest.'); + const userExtensions = await this.extensionsScannerService.scanUserExtensions({ includeInvalid: true }); + await this.extensionsProfileScannerService.addExtensionsToProfile(userExtensions.map(e => [e, e.metadata]), this.userDataProfilesService.defaultProfile.extensionsResource); + this.logService.info('Completed migrating default profile extensions from extensions installation folder to extensions manifest.'); } - return 1; - }).slice(1)); - } - if (extensions[0].type === ExtensionType.System) { - const systemExtension = systemExtensions.find(e => areSameExtensions(e.identifier, extensions[0].identifier)); - if (!systemExtension || semver.gte(systemExtension.manifest.version, extensions[0].manifest.version)) { - toRemove.push(extensions[0]); + try { + await this.fileService.createFile(migrationMarkerFile, VSBuffer.fromString('')); + } catch (error) { + this.logService.warn('Failed to create migration marker file for default profile extensions migration.', getErrorMessage(error)); + } + } catch (error) { + this.logService.error(error); + throw error; } - } + })(); } - await Promises.settled(toRemove.map(extension => this.removeExtension(extension, 'outdated'))); + return this._migrateDefaultProfileExtensionsPromise; } private joinErrors(errorOrErrors: (Error | string) | (Array)): Error { @@ -775,46 +802,6 @@ class InstallExtensionInProfileTask implements IInstallExtensionTask { } } -class UninstallExtensionTask extends AbstractExtensionTask implements IUninstallExtensionTask { - - constructor( - readonly extension: ILocalExtension, - private readonly options: UninstallOptions, - private readonly extensionsScanner: ExtensionsScanner, - ) { - super(); - } - - protected async doRun(token: CancellationToken): Promise { - const toUninstall: ILocalExtension[] = []; - const userExtensions = await this.extensionsScanner.scanUserExtensions(false); - if (this.options.versionOnly) { - const extensionKey = ExtensionKey.create(this.extension); - toUninstall.push(...userExtensions.filter(u => extensionKey.equals(ExtensionKey.create(u)))); - } else { - toUninstall.push(...userExtensions.filter(u => areSameExtensions(u.identifier, this.extension.identifier))); - } - - if (!toUninstall.length) { - throw new Error(nls.localize('notInstalled', "Extension '{0}' is not installed.", this.extension.manifest.displayName || this.extension.manifest.name)); - } - await this.extensionsScanner.setUninstalled(...toUninstall); - - if (this.options.remove) { - for (const extension of toUninstall) { - try { - if (!token.isCancellationRequested) { - await this.extensionsScanner.removeUninstalledExtension(extension); - } - } catch (e) { - throw new Error(nls.localize('removeError', "Error while removing the extension: {0}. Please Quit and Start VS Code before trying again.", toErrorMessage(e))); - } - } - } - } - -} - class UninstallExtensionFromProfileTask extends AbstractExtensionTask implements IUninstallExtensionTask { constructor( diff --git a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts index 2693b4e10bda5..e04c1b8b86963 100644 --- a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts +++ b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts @@ -6,42 +6,49 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { DidUninstallExtensionEvent, IExtensionManagementService, ILocalExtension, InstallExtensionEvent, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { DidUninstallExtensionEvent, InstallExtensionResult, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { getIdAndVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; +import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ExtensionIdentifier, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { FileChangeType, IFileChange, IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; export class ExtensionsWatcher extends Disposable { - private readonly _onDidChangeExtensionsByAnotherSource = this._register(new Emitter<{ added: ILocalExtension[]; removed: IExtensionIdentifier[] }>()); + private readonly _onDidChangeExtensionsByAnotherSource = this._register(new Emitter<{ added: InstallExtensionResult[]; removed: DidUninstallExtensionEvent[] }>()); readonly onDidChangeExtensionsByAnotherSource = this._onDidChangeExtensionsByAnotherSource.event; - private startTimestamp = 0; - private installingExtensions: IExtensionIdentifier[] = []; - private installedExtensions: IExtensionIdentifier[] | undefined; + private readonly profileExtensionsLocations = new Map; constructor( - private readonly extensionsManagementService: IExtensionManagementService, - @IFileService fileService: IFileService, - @INativeEnvironmentService environmentService: INativeEnvironmentService, - @ILogService private readonly logService: ILogService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + private readonly extensionManagementService: INativeServerExtensionManagementService, + private readonly userDataProfilesService: IUserDataProfilesService, + private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, + private readonly extensionsScannerService: IExtensionsScannerService, + private readonly uriIdentityService: IUriIdentityService, + private readonly fileService: IFileService, + private readonly logService: ILogService, ) { super(); - this.extensionsManagementService.getInstalled().then(extensions => { - this.installedExtensions = extensions.map(e => e.identifier); - this.startTimestamp = Date.now(); - }); - this._register(extensionsManagementService.onInstallExtension(e => this.onInstallExtension(e))); - this._register(extensionsManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e))); - this._register(extensionsManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e))); + this.initialize().then(() => this.registerListeners(), error => logService.error(error)); + } - const extensionsResource = URI.file(environmentService.extensionsPath); - this._register(fileService.watch(extensionsResource)); - this._register(Event.filter(fileService.onDidFilesChange, e => e.rawChanges.some(change => this.doesChangeAffects(change, extensionsResource)))(() => this.onDidChange())); + private async initialize(): Promise { + await this.extensionManagementService.migrateDefaultProfileExtensions(); + await this.onDidChangeProfiles(this.userDataProfilesService.profiles, []); + await this.uninstallExtensionsNotInProfiles(); + } + + private registerListeners(): void { + this._register(this.userDataProfilesService.onDidChangeProfiles(e => this.onDidChangeProfiles(e.added, e.removed))); + this._register(this.extensionManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e))); + this._register(this.extensionManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e))); + this._register(this.fileService.watch(this.extensionsScannerService.userExtensionsLocation)); + this._register(Event.filter(this.fileService.onDidFilesChange, e => e.rawChanges.some(change => this.doesChangeAffects(change, this.extensionsScannerService.userExtensionsLocation)))(() => this.onDidChange())); } private doesChangeAffects(change: IFileChange, extensionsResource: URI): boolean { @@ -68,78 +75,136 @@ export class ExtensionsWatcher extends Disposable { return true; } - private onInstallExtension(e: InstallExtensionEvent): void { - this.addInstallingExtension(e.identifier); + private async onDidChange(): Promise { + const installed = await this.extensionManagementService.getAllUserInstalled(); + const added = installed.filter(e => { + if (e.installedTimestamp !== undefined) { + return false; + } + if (this.profileExtensionsLocations.has(this.getKey(e.identifier, e.manifest.version))) { + return false; + } + this.logService.info('Detected extension installed from another source', e.identifier.id); + return true; + }); + if (added.length) { + await this.extensionsProfileScannerService.addExtensionsToProfile(added.map(e => [e, undefined]), this.userDataProfilesService.defaultProfile.extensionsResource); + this._onDidChangeExtensionsByAnotherSource.fire({ + added: added.map(local => ({ + identifier: local.identifier, + operation: InstallOperation.None, + profileLocation: this.userDataProfilesService.defaultProfile.extensionsResource, + local + })), + removed: [] + }); + } } - private onDidInstallExtensions(results: readonly InstallExtensionResult[]): void { - for (const e of results) { - this.removeInstallingExtension(e.identifier); - if (e.local) { - this.addInstalledExtension(e.identifier); + private async onDidChangeProfiles(added: readonly IUserDataProfile[], removed: readonly IUserDataProfile[]): Promise { + try { + await Promise.all(removed.map(profile => this.removeExtensionsFromProfile(profile.extensionsResource))); + } catch (error) { + this.logService.error(error); + } + + try { + if (added.length) { + await Promise.all(added.map(profile => this.populateExtensionsFromProfile(profile.extensionsResource))); } + } catch (error) { + this.logService.error(error); } } - private onDidUninstallExtension(e: DidUninstallExtensionEvent): void { - if (!e.error) { - this.removeInstalledExtension(e.identifier); + 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 this.extensionManagementService.markAsUninstalled(...toUninstall); + } + } + + private async onDidInstallExtensions(installedExtensions: readonly InstallExtensionResult[]): Promise { + for (const { local, profileLocation } of installedExtensions) { + if (!local || !profileLocation) { + continue; + } + this.addExtensionWithKey(this.getKey(local.identifier, local.manifest.version), profileLocation); } } - private addInstallingExtension(extension: IExtensionIdentifier) { - this.removeInstallingExtension(extension); - this.installingExtensions.push(extension); + private async onDidUninstallExtension(e: DidUninstallExtensionEvent): Promise { + if (!e.profileLocation || !e.version) { + return; + } + if (this.removeExtensionWithKey(this.getKey(e.identifier, e.version), e.profileLocation)) { + await this.uninstallExtensions([{ identifier: e.identifier, version: e.version }]); + } } - private removeInstallingExtension(identifier: IExtensionIdentifier) { - this.installingExtensions = this.installingExtensions.filter(e => !areSameExtensions(e, identifier)); + 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 addInstalledExtension(extension: IExtensionIdentifier): void { - if (this.installedExtensions) { - this.removeInstalledExtension(extension); - this.installedExtensions.push(extension); + private async removeExtensionsFromProfile(removedProfile: URI): Promise { + 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); } } - private removeInstalledExtension(identifier: IExtensionIdentifier): void { - if (this.installedExtensions) { - this.installedExtensions = this.installedExtensions.filter(e => !areSameExtensions(e, identifier)); + 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 async onDidChange(): Promise { - if (this.installedExtensions) { - const extensions = await this.extensionsManagementService.getInstalled(); - const added = extensions.filter(e => { - if ([...this.installingExtensions, ...this.installedExtensions!].some(identifier => areSameExtensions(identifier, e.identifier))) { - return false; - } - if (e.installedTimestamp && e.installedTimestamp > this.startTimestamp) { - this.logService.info('Detected extension installed from another source', e.identifier.id); - return true; - } else { - this.logService.info('Ignored extension installed by another source because of invalid timestamp', e.identifier.id); - return false; - } - }); - const removed = this.installedExtensions.filter(identifier => { - // Extension being installed - if (this.installingExtensions.some(installingExtension => areSameExtensions(installingExtension, identifier))) { - return false; - } - if (extensions.every(e => !areSameExtensions(e.identifier, identifier))) { - this.logService.info('Detected extension removed from another source', identifier.id); - return true; - } - return false; - }); - this.installedExtensions = extensions.map(e => e.identifier); - if (added.length || removed.length) { - this._onDidChangeExtensionsByAnotherSource.fire({ added, removed }); + 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 true; + } + return false; + } + + 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 this.extensionManagementService.markAsUninstalled(...toUninstall); + } + } + + private getKey(identifier: IExtensionIdentifier, version: string): string { + 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/userDataProfile/browser/userDataProfile.ts b/src/vs/platform/userDataProfile/browser/userDataProfile.ts index 8d4253f2c8899..a3144510cf629 100644 --- a/src/vs/platform/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/browser/userDataProfile.ts @@ -16,7 +16,6 @@ type BroadcastedProfileChanges = UriDto>; export class BrowserUserDataProfilesService extends UserDataProfilesService implements IUserDataProfilesService { - protected override readonly defaultProfileShouldIncludeExtensionsResourceAlways: boolean = true; private readonly changesBroadcastChannel: BroadcastDataChannel; constructor( diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index 2207f1a08e4bb..b74d2b1cfde71 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -7,7 +7,6 @@ import { hash } from 'vs/base/common/hash'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { basename, joinPath } from 'vs/base/common/resources'; -import { isUndefined } from 'vs/base/common/types'; import { URI, UriDto } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -45,7 +44,7 @@ export interface IUserDataProfile { readonly keybindingsResource: URI; readonly tasksResource: URI; readonly snippetsHome: URI; - readonly extensionsResource: URI | undefined; + readonly extensionsResource: URI; readonly useDefaultFlags?: UseDefaultProfileFlags; readonly isTransient?: boolean; } @@ -63,7 +62,7 @@ export function isUserDataProfile(thing: unknown): thing is IUserDataProfile { && URI.isUri(candidate.keybindingsResource) && URI.isUri(candidate.tasksResource) && URI.isUri(candidate.snippetsHome) - && (isUndefined(candidate.extensionsResource) || URI.isUri(candidate.extensionsResource)) + && URI.isUri(candidate.extensionsResource) ); } @@ -138,8 +137,6 @@ export function reviveProfile(profile: UriDto, scheme: string) }; } -export const EXTENSIONS_RESOURCE_NAME = 'extensions.json'; - export function toUserDataProfile(id: string, name: string, location: URI, options?: IUserDataProfileOptions): IUserDataProfile { return { id, @@ -152,7 +149,7 @@ export function toUserDataProfile(id: string, name: string, location: URI, optio keybindingsResource: joinPath(location, 'keybindings.json'), tasksResource: joinPath(location, 'tasks.json'), snippetsHome: joinPath(location, 'snippets'), - extensionsResource: joinPath(location, EXTENSIONS_RESOURCE_NAME), + extensionsResource: joinPath(location, 'extensions.json'), useDefaultFlags: options?.useDefaultFlags, isTransient: options?.transient }; @@ -184,7 +181,6 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf readonly _serviceBrand: undefined; protected enabled: boolean = false; - protected readonly defaultProfileShouldIncludeExtensionsResourceAlways: boolean = false; readonly profilesHome: URI; get defaultProfile(): IUserDataProfile { return this.profiles[0]; } @@ -237,7 +233,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf let emptyWindow: IUserDataProfile | undefined; const workspaces = new ResourceMap(); const defaultProfile = toUserDataProfile(hash(this.environmentService.userRoamingDataHome.path).toString(16), localize('defaultProfile', "Default"), this.environmentService.userRoamingDataHome); - profiles.unshift({ ...defaultProfile, isDefault: true, extensionsResource: this.defaultProfileShouldIncludeExtensionsResourceAlways || profiles.length > 0 || this.transientProfilesObject.profiles.length > 0 ? defaultProfile.extensionsResource : undefined }); + profiles.unshift({ ...defaultProfile, isDefault: true }); if (profiles.length) { const profileAssicaitions = this.getStoredProfileAssociations(); if (profileAssicaitions.workspaces) { diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts index 80475cfdc1074..e146018fb7c4a 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts @@ -54,13 +54,12 @@ suite('UserDataProfileService (Common)', () => { assert.strictEqual(testObject.defaultProfile.settingsResource.toString(), joinPath(environmentService.userRoamingDataHome, 'settings.json').toString()); assert.strictEqual(testObject.defaultProfile.snippetsHome.toString(), joinPath(environmentService.userRoamingDataHome, 'snippets').toString()); assert.strictEqual(testObject.defaultProfile.tasksResource.toString(), joinPath(environmentService.userRoamingDataHome, 'tasks.json').toString()); - assert.strictEqual(testObject.defaultProfile.extensionsResource, undefined); + assert.strictEqual(testObject.defaultProfile.extensionsResource.toString(), joinPath(environmentService.userRoamingDataHome, 'extensions.json').toString()); }); test('profiles always include default profile', () => { assert.deepStrictEqual(testObject.profiles.length, 1); assert.deepStrictEqual(testObject.profiles[0].isDefault, true); - assert.deepStrictEqual(testObject.profiles[0].extensionsResource, undefined); }); test('create profile with id', async () => { @@ -116,7 +115,6 @@ suite('UserDataProfileService (Common)', () => { assert.deepStrictEqual(testObject.profiles.length, 2); assert.deepStrictEqual(testObject.profiles[0].isDefault, true); - assert.deepStrictEqual(testObject.profiles[0].extensionsResource?.toString(), joinPath(environmentService.userRoamingDataHome, 'extensions.json').toString()); }); test('profiles include default profile with extension resource undefined when transiet prrofile is removed', async () => { @@ -125,7 +123,6 @@ suite('UserDataProfileService (Common)', () => { assert.deepStrictEqual(testObject.profiles.length, 1); assert.deepStrictEqual(testObject.profiles[0].isDefault, true); - assert.deepStrictEqual(testObject.profiles[0].extensionsResource, undefined); }); test('update named profile', async () => { diff --git a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts index 7078f0757a4a2..ddca68846011b 100644 --- a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts +++ b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts @@ -51,26 +51,22 @@ suite('UserDataProfileMainService', () => { test('default profile', () => { assert.strictEqual(testObject.defaultProfile.isDefault, true); - assert.strictEqual(testObject.defaultProfile.extensionsResource, undefined); }); test('profiles always include default profile', () => { assert.deepStrictEqual(testObject.profiles.length, 1); assert.deepStrictEqual(testObject.profiles[0].isDefault, true); - assert.deepStrictEqual(testObject.profiles[0].extensionsResource, undefined); }); test('default profile when there are profiles', async () => { await testObject.createNamedProfile('test'); assert.strictEqual(testObject.defaultProfile.isDefault, true); - assert.strictEqual(testObject.defaultProfile.extensionsResource?.toString(), joinPath(environmentService.userRoamingDataHome, 'extensions.json').toString()); }); test('default profile when profiles are removed', async () => { const profile = await testObject.createNamedProfile('test'); await testObject.removeProfile(profile); assert.strictEqual(testObject.defaultProfile.isDefault, true); - assert.strictEqual(testObject.defaultProfile.extensionsResource, undefined); }); }); diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 87098ee8e7135..4e61627c14dce 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -113,7 +113,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse constructor( // profileLocation changes for default profile - public profile: IUserDataProfile, + profile: IUserDataProfile, collection: string | undefined, @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService, @@ -147,7 +147,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const builtinExtensions: IExtensionIdentifier[] = lastSyncUserData?.builtinExtensions || []; const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData?.syncData ? await parseAndMigrateExtensions(lastSyncUserData.syncData, this.extensionManagementService) : null; - const { localExtensions, ignoredExtensions } = await this.localExtensionsProvider.getLocalExtensions(this.profile); + const { localExtensions, ignoredExtensions } = await this.localExtensionsProvider.getLocalExtensions(this.syncResource.profile); if (remoteExtensions) { this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`); @@ -185,7 +185,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse protected async hasRemoteChanged(lastSyncUserData: ILastSyncUserData): Promise { const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData.syncData ? await parseAndMigrateExtensions(lastSyncUserData.syncData, this.extensionManagementService) : null; - const { localExtensions, ignoredExtensions } = await this.localExtensionsProvider.getLocalExtensions(this.profile); + const { localExtensions, ignoredExtensions } = await this.localExtensionsProvider.getLocalExtensions(this.syncResource.profile); const { remote } = merge(localExtensions, lastSyncExtensions, lastSyncExtensions, lastSyncUserData.skippedExtensions || [], ignoredExtensions, lastSyncUserData.builtinExtensions || []); return remote !== null; } @@ -239,7 +239,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } private async acceptLocal(resourcePreview: IExtensionResourcePreview): Promise { - const installedExtensions = await this.extensionManagementService.getInstalled(undefined, this.profile.location); + const installedExtensions = await this.extensionManagementService.getInstalled(undefined, this.syncResource.profile.extensionsResource); const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); const mergeResult = merge(resourcePreview.localExtensions, null, null, resourcePreview.skippedExtensions, ignoredExtensions, resourcePreview.builtinExtensions); const { local, remote } = mergeResult; @@ -253,7 +253,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } private async acceptRemote(resourcePreview: IExtensionResourcePreview): Promise { - const installedExtensions = await this.extensionManagementService.getInstalled(undefined, this.profile.location); + const installedExtensions = await this.extensionManagementService.getInstalled(undefined, this.syncResource.profile.extensionsResource); const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null; if (remoteExtensions !== null) { @@ -287,7 +287,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse if (localChange !== Change.None) { await this.backupLocal(JSON.stringify(localExtensions)); - skippedExtensions = await this.localExtensionsProvider.updateLocalExtensions(local.added, local.removed, local.updated, skippedExtensions, this.profile); + skippedExtensions = await this.localExtensionsProvider.updateLocalExtensions(local.added, local.removed, local.updated, skippedExtensions, this.syncResource.profile); } if (remote) { @@ -325,7 +325,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse async hasLocalData(): Promise { try { - const { localExtensions } = await this.localExtensionsProvider.getLocalExtensions(this.profile); + const { localExtensions } = await this.localExtensionsProvider.getLocalExtensions(this.syncResource.profile); if (localExtensions.some(e => e.installed || e.disabled)) { return true; } diff --git a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts index 4d3767d988d43..8be70621bfdc7 100644 --- a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts +++ b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts @@ -180,15 +180,27 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i await this.backupLocal(stringifyLocalProfiles(this.getLocalUserDataProfiles(), false)); const promises: Promise[] = []; for (const profile of local.added) { - promises.push(this.userDataProfilesService.createProfile(profile.id, profile.name, { shortName: profile.shortName })); + promises.push((async () => { + this.logService.trace(`${this.syncResourceLogLabel}: Creating '${profile.name}' profile...`); + await this.userDataProfilesService.createProfile(profile.id, profile.name, { shortName: profile.shortName }); + this.logService.info(`${this.syncResourceLogLabel}: Created profile '${profile.name}'.`); + })()); } for (const profile of local.removed) { - promises.push(this.userDataProfilesService.removeProfile(profile)); + promises.push((async () => { + this.logService.trace(`${this.syncResourceLogLabel}: Removing '${profile.name}' profile...`); + await this.userDataProfilesService.removeProfile(profile); + this.logService.info(`${this.syncResourceLogLabel}: Removed profile '${profile.name}'.`); + })()); } for (const profile of local.updated) { const localProfile = this.userDataProfilesService.profiles.find(p => p.id === profile.id); if (localProfile) { - promises.push(this.userDataProfilesService.updateProfile(localProfile, { name: profile.name, shortName: profile.shortName })); + promises.push((async () => { + this.logService.trace(`${this.syncResourceLogLabel}: Updating '${profile.name}' profile...`); + await this.userDataProfilesService.updateProfile(localProfile, { name: profile.name, shortName: profile.shortName }); + this.logService.info(`${this.syncResourceLogLabel}: Updated profile '${profile.name}'.`); + })()); } else { this.logService.info(`${this.syncResourceLogLabel}: Could not find profile with id '${profile.id}' to update.`); } diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index ab5261c5cc38e..2741e974dd259 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -542,19 +542,6 @@ class ProfileSynchronizer extends Disposable { @IEnvironmentService private readonly environmentService: IEnvironmentService, ) { super(); - if (this._profile.isDefault) { - this._register(userDataProfilesService.onDidChangeProfiles(() => { - if ((userDataProfilesService.defaultProfile.extensionsResource && !this._profile.extensionsResource) || - (!userDataProfilesService.defaultProfile.extensionsResource && this._profile.extensionsResource)) { - this._profile = userDataProfilesService.defaultProfile; - for (const [synchronizer] of this._enabled) { - if (synchronizer instanceof ExtensionsSynchroniser) { - synchronizer.profile = this._profile; - } - } - } - })); - } this._register(userDataSyncEnablementService.onDidChangeResourceEnablement(([syncResource, enablement]) => this.onDidChangeResourceEnablement(syncResource, enablement))); this._register(toDisposable(() => this._enabled.splice(0, this._enabled.length).forEach(([, , disposable]) => disposable.dispose()))); for (const syncResource of ALL_SYNC_RESOURCES) { diff --git a/src/vs/server/node/remoteAgentEnvironmentImpl.ts b/src/vs/server/node/remoteAgentEnvironmentImpl.ts index 7f581f36ea58d..f16217b0125bc 100644 --- a/src/vs/server/node/remoteAgentEnvironmentImpl.ts +++ b/src/vs/server/node/remoteAgentEnvironmentImpl.ts @@ -29,6 +29,7 @@ import { IExtensionsScannerService, toExtensionDescription } from 'vs/platform/e import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { ExtensionManagementCLI } from 'vs/platform/extensionManagement/common/extensionManagementCLI'; +import { INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; export class RemoteAgentEnvironmentChannel implements IServerChannel { @@ -44,6 +45,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { private readonly _logService: ILogService, private readonly _extensionHostStatusService: IExtensionHostStatusService, private readonly _extensionsScannerService: IExtensionsScannerService, + private readonly _extensionManagementService: INativeServerExtensionManagementService, ) { if (_environmentService.args['install-builtin-extension']) { const installOptions: InstallOptions = { isMachineScoped: !!_environmentService.args['do-not-sync'], installPreReleaseVersion: !!_environmentService.args['pre-release'] }; @@ -334,6 +336,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { } private async _scanInstalledExtensions(language: string): Promise { + await this._extensionManagementService.migrateDefaultProfileExtensions(); const scannedExtensions = await this._extensionsScannerService.scanUserExtensions({ language, useCache: true }); return scannedExtensions.map(e => toExtensionDescription(e, false)); } diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 6978f8f6e4f47..9d94002e098de 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -196,7 +196,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken instantiationService.invokeFunction(accessor => { const extensionManagementService = accessor.get(INativeServerExtensionManagementService); const extensionsScannerService = accessor.get(IExtensionsScannerService); - const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, userDataProfilesService, instantiationService.createInstance(ExtensionManagementCLI), logService, extensionHostStatusService, extensionsScannerService); + const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, userDataProfilesService, instantiationService.createInstance(ExtensionManagementCLI), logService, extensionHostStatusService, extensionsScannerService, extensionManagementService); socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel); const telemetryChannel = new ServerTelemetryChannel(accessor.get(IServerTelemetryService), oneDsAppender); @@ -219,7 +219,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken socketServer.registerChannel('credentials', credentialsChannel); // clean up deprecated extensions - extensionManagementService.removeUninstalledExtensions(true); + extensionManagementService.removeUninstalledExtensions(); disposables.add(new ErrorTelemetry(accessor.get(ITelemetryService))); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 3569cdb16573a..7c0e6f44ab830 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -2501,6 +2501,7 @@ export class ReinstallAction extends Action { constructor( id: string = ReinstallAction.ID, label: string = ReinstallAction.LABEL, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IQuickInputService private readonly quickInputService: IQuickInputService, @INotificationService private readonly notificationService: INotificationService, @IHostService private readonly hostService: IHostService, @@ -2523,7 +2524,7 @@ export class ReinstallAction extends Action { return this.extensionsWorkbenchService.queryLocal() .then(local => { const entries = local - .filter(extension => !extension.isBuiltin) + .filter(extension => !extension.isBuiltin && extension.server !== this.extensionManagementServerService.webExtensionManagementServer) .map(extension => { return { id: extension.identifier.id, diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index 905f49db4de6d..abf7fbbcd96be 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -371,7 +371,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return this.readSystemExtensions(); } - async scanUserExtensions(profileLocation?: URI, scanOptions?: ScanOptions): Promise { + async scanUserExtensions(profileLocation: URI, scanOptions?: ScanOptions): Promise { const extensions = new Map(); // Custom builtin extensions defined through `additionalBuiltinExtensions` API @@ -410,7 +410,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return result; } - async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, profileLocation?: URI): Promise { + async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, profileLocation: URI): Promise { if (extensionType === ExtensionType.System) { const systemExtensions = await this.scanSystemExtensions(); return systemExtensions.find(e => e.location.toString() === extensionLocation.toString()) || null; @@ -419,7 +419,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return userExtensions.find(e => e.location.toString() === extensionLocation.toString()) || null; } - async scanMetadata(extensionLocation: URI, profileLocation?: URI): Promise { + async scanMetadata(extensionLocation: URI, profileLocation: URI): Promise { const extension = await this.scanExistingExtension(extensionLocation, ExtensionType.User, profileLocation); return extension?.metadata; } @@ -437,19 +437,19 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return null; } - async addExtensionFromGallery(galleryExtension: IGalleryExtension, metadata: Metadata, profileLocation?: URI): Promise { + async addExtensionFromGallery(galleryExtension: IGalleryExtension, metadata: Metadata, profileLocation: URI): Promise { const webExtension = await this.toWebExtensionFromGallery(galleryExtension, metadata); return this.addWebExtension(webExtension, profileLocation); } - async addExtension(location: URI, metadata: Metadata, profileLocation?: URI): Promise { + async addExtension(location: URI, metadata: Metadata, profileLocation: URI): Promise { const webExtension = await this.toWebExtension(location, undefined, undefined, undefined, undefined, undefined, undefined, metadata); const extension = await this.toScannedExtension(webExtension, false); await this.addToInstalledExtensions([webExtension], profileLocation); return extension; } - async removeExtension(extension: IScannedExtension, profileLocation?: URI): Promise { + async removeExtension(extension: IScannedExtension, profileLocation: URI): Promise { await this.writeInstalledExtensions(profileLocation, installedExtensions => installedExtensions.filter(installedExtension => !areSameExtensions(installedExtension.identifier, extension.identifier))); } @@ -467,7 +467,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } } - private async addWebExtension(webExtension: IWebExtension, profileLocation?: URI): Promise { + private async addWebExtension(webExtension: IWebExtension, profileLocation: URI): Promise { const isSystem = !!(await this.scanSystemExtensions()).find(e => areSameExtensions(e.identifier, webExtension.identifier)); const isBuiltin = !!webExtension.metadata?.isBuiltin; const extension = await this.toScannedExtension(webExtension, isBuiltin); @@ -504,7 +504,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return extension; } - private async addToInstalledExtensions(webExtensions: IWebExtension[], profileLocation?: URI): Promise { + private async addToInstalledExtensions(webExtensions: IWebExtension[], profileLocation: URI): Promise { await this.writeInstalledExtensions(profileLocation, installedExtensions => { // Remove the existing extension to avoid duplicates installedExtensions = installedExtensions.filter(installedExtension => webExtensions.some(extension => !areSameExtensions(installedExtension.identifier, extension.identifier))); @@ -513,11 +513,11 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten }); } - private async scanInstalledExtensions(profileLocation?: URI, scanOptions?: ScanOptions): Promise { + private async scanInstalledExtensions(profileLocation: URI, scanOptions?: ScanOptions): Promise { let installedExtensions = await this.readInstalledExtensions(profileLocation); // If current profile is not a default profile, then add the application extensions to the list - if (this.userDataProfilesService.defaultProfile.extensionsResource && !this.uriIdentityService.extUri.isEqual(profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource)) { + if (!this.uriIdentityService.extUri.isEqual(profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource)) { // Remove application extensions from the non default profile installedExtensions = installedExtensions.filter(i => !i.metadata?.isApplicationScoped); // Add application extensions from the default profile to the list @@ -764,14 +764,14 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return this._migratePackageNLSUrisPromise; } - private async readInstalledExtensions(profileLocation?: URI): Promise { + private async readInstalledExtensions(profileLocation: URI): Promise { if (this.uriIdentityService.extUri.isEqual(profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource)) { await this.migratePackageNLSUris(); } return this.withWebExtensions(profileLocation); } - private writeInstalledExtensions(profileLocation: URI | undefined, updateFn: (extensions: IWebExtension[]) => IWebExtension[]): Promise { + private writeInstalledExtensions(profileLocation: URI, updateFn: (extensions: IWebExtension[]) => IWebExtension[]): Promise { return this.withWebExtensions(profileLocation, updateFn); } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 34d4367b5252c..aecd0f677dbb6 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -167,15 +167,15 @@ export interface IWebExtensionsScannerService { readonly _serviceBrand: undefined; scanSystemExtensions(): Promise; - scanUserExtensions(profileLocation: URI | undefined, options?: ScanOptions): Promise; + scanUserExtensions(profileLocation: URI, options?: ScanOptions): Promise; scanExtensionsUnderDevelopment(): Promise; - scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, profileLocation: URI | undefined): Promise; + scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, profileLocation: URI): Promise; - addExtension(location: URI, metadata: Metadata, profileLocation: URI | undefined): Promise; - addExtensionFromGallery(galleryExtension: IGalleryExtension, metadata: Metadata, profileLocation: URI | undefined): Promise; - removeExtension(extension: IScannedExtension, profileLocation: URI | undefined): Promise; + addExtension(location: URI, metadata: Metadata, profileLocation: URI): Promise; + addExtensionFromGallery(galleryExtension: IGalleryExtension, metadata: Metadata, profileLocation: URI): Promise; + removeExtension(extension: IScannedExtension, profileLocation: URI): Promise; copyExtensions(fromProfileLocation: URI, toProfileLocation: URI, filter: (extension: IScannedExtension) => boolean): Promise; - scanMetadata(extensionLocation: URI, profileLocation: URI | undefined): Promise; + scanMetadata(extensionLocation: URI, profileLocation: URI): Promise; scanExtensionManifest(extensionLocation: URI): Promise; } diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 79333395143a1..0deec1d363161 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionIdentifier, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; -import { ILocalExtension, IGalleryExtension, IGalleryMetadata, InstallOperation, IExtensionGalleryService, Metadata, InstallOptions, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, IGalleryMetadata, InstallOperation, IExtensionGalleryService, Metadata, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { Emitter, 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 } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, UninstallExtensionTaskOptions } 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'; @@ -129,17 +129,11 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe return local; } - protected doCreateInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions): IInstallExtensionTask { - if (!options.profileLocation) { - options = { ...options, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }; - } + protected createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask { return new InstallExtensionTask(manifest, extension, options, this.webExtensionsScannerService); } - protected doCreateUninstallExtensionTask(extension: ILocalExtension, options: UninstallOptions): IUninstallExtensionTask { - if (!options.profileLocation) { - options = { ...options, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }; - } + protected createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask { return new UninstallExtensionTask(extension, options, this.webExtensionsScannerService); } @@ -148,6 +142,7 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe getManifest(vsix: URI): Promise { throw new Error('unsupported'); } updateExtensionScope(): Promise { throw new Error('unsupported'); } download(): Promise { throw new Error('unsupported'); } + reinstallFromGallery(): Promise { throw new Error('unsupported'); } private async whenProfileChanged(e: DidChangeUserDataProfileEvent): Promise { const previousProfileLocation = e.previous.extensionsResource; @@ -202,7 +197,7 @@ class InstallExtensionTask extends AbstractExtensionTask<{ local: ILocalExtensio constructor( manifest: IExtensionManifest, private readonly extension: URI | IGalleryExtension, - private readonly options: InstallOptions, + private readonly options: InstallExtensionTaskOptions, private readonly webExtensionsScannerService: IWebExtensionsScannerService, ) { super(); @@ -243,7 +238,7 @@ class UninstallExtensionTask extends AbstractExtensionTask implements IUni constructor( readonly extension: ILocalExtension, - private readonly options: UninstallOptions, + private readonly options: UninstallExtensionTaskOptions, private readonly webExtensionsScannerService: IWebExtensionsScannerService, ) { super(); diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts index da1f95ec42ecb..6d2bf5f714b7e 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts @@ -15,7 +15,6 @@ import { delta } from 'vs/base/common/arrays'; import { compare } from 'vs/base/common/strings'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { DidChangeUserDataProfileEvent, IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; -import { EXTENSIONS_RESOURCE_NAME } from 'vs/platform/userDataProfile/common/userDataProfile'; import { joinPath } from 'vs/base/common/resources'; import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { Schemas } from 'vs/base/common/network'; @@ -106,8 +105,7 @@ export class NativeExtensionManagementService extends ExtensionManagementChannel } private async whenProfileChanged(e: DidChangeUserDataProfileEvent): Promise { - const previousExtensionsResource = e.previous.extensionsResource ?? joinPath(e.previous.location, EXTENSIONS_RESOURCE_NAME); - const oldExtensions = await super.getInstalled(ExtensionType.User, previousExtensionsResource); + const oldExtensions = await super.getInstalled(ExtensionType.User, e.previous.extensionsResource); if (e.preserveData) { const extensions: [ILocalExtension, Metadata | undefined][] = await Promise.all(oldExtensions .filter(e => !e.isApplicationScoped) /* remove application scoped extensions */ diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index dd9be1540682b..b2a8611aec9ed 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -16,6 +16,9 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IHostService } from 'vs/workbench/services/host/browser/host'; import { timeout } from 'vs/base/common/async'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { DEFAULT_PROFILE_EXTENSIONS_MIGRATION_KEY } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; export class CachedExtensionScanner { @@ -28,6 +31,8 @@ export class CachedExtensionScanner { @IHostService private readonly _hostService: IHostService, @IExtensionsScannerService private readonly _extensionsScannerService: IExtensionsScannerService, @IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService, + @IUserDataProfilesService private readonly _userDataProfilesService: IUserDataProfilesService, + @IStorageService private readonly _storageService: IStorageService, @ILogService private readonly _logService: ILogService, ) { this.scannedExtensions = new Promise((resolve, reject) => { @@ -53,9 +58,10 @@ export class CachedExtensionScanner { private async _scanInstalledExtensions(): Promise { try { const language = platform.language; + const profileLocation = this._userDataProfilesService.profiles.length === 1 && this._userDataProfileService.currentProfile.isDefault && !this._storageService.getBoolean(DEFAULT_PROFILE_EXTENSIONS_MIGRATION_KEY, StorageScope.APPLICATION, false) ? undefined : this._userDataProfileService.currentProfile.extensionsResource; const [scannedSystemExtensions, scannedUserExtensions] = await Promise.all([ this._extensionsScannerService.scanSystemExtensions({ language, useCache: true, checkControlFile: true }), - this._extensionsScannerService.scanUserExtensions({ language, profileLocation: this._userDataProfileService.currentProfile.extensionsResource, useCache: true })]); + this._extensionsScannerService.scanUserExtensions({ language, profileLocation, useCache: true })]); const scannedDevelopedExtensions = await this._extensionsScannerService.scanExtensionsUnderDevelopment({ language }, [...scannedSystemExtensions, ...scannedUserExtensions]); const system = scannedSystemExtensions.map(e => toExtensionDescription(e, false)); const user = scannedUserExtensions.map(e => toExtensionDescription(e, false)); diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts index 63b78ba06b763..2f2f2c8a1fc20 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts @@ -34,15 +34,6 @@ export class UserDataProfileService extends Disposable implements IUserDataProfi super(); this._currentProfile = currentProfile; this._register(userDataProfilesService.onDidChangeProfiles(e => { - /** - * If the current profile is default profile, then reset it because, - * In Desktop the extensions resource will be set/unset in the default profile when profiles are changed. - */ - if (this._currentProfile.isDefault) { - this._currentProfile = userDataProfilesService.defaultProfile; - return; - } - const updatedCurrentProfile = e.updated.find(p => this._currentProfile.id === p.id); if (updatedCurrentProfile) { this._currentProfile = updatedCurrentProfile; diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index ebec1acd03c12..4b09620745a50 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -68,7 +68,7 @@ const NULL_PROFILE = { keybindingsResource: joinPath(URI.file(homeDir), 'keybindings.json'), tasksResource: joinPath(URI.file(homeDir), 'tasks.json'), snippetsHome: joinPath(URI.file(homeDir), 'snippets'), - extensionsResource: undefined + extensionsResource: joinPath(URI.file(homeDir), 'extensions.json') }; export const TestNativeWindowConfiguration: INativeWindowConfiguration = {