From 4c4a7389857cf2804d464652b3db082999074ea4 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 2 Nov 2022 17:33:14 -0700 Subject: [PATCH] Revert "Fix #164690 #164730 (#165279)" This reverts commit 9c0c68c8cfd9341b8e345e409c5b434e5b80e14e. --- .../sharedProcess/contrib/extensions.ts | 33 --- .../contrib/extensionsCleaner.ts | 186 +++++++++++++++ .../sharedProcess/sharedProcessMain.ts | 4 +- src/vs/code/electron-main/app.ts | 4 + src/vs/code/node/cliProcessMain.ts | 2 +- .../abstractExtensionManagementService.ts | 103 +++++---- .../common/extensionManagement.ts | 3 +- .../common/extensionsProfileScannerService.ts | 2 +- .../common/extensionsScannerService.ts | 5 +- .../defaultExtensionsProfileInit.ts | 57 +++++ .../node/extensionManagementService.ts | 173 +++++++------- .../node/extensionsWatcher.ts | 213 ++++++------------ .../browser/userDataProfile.ts | 1 + .../userDataProfile/common/userDataProfile.ts | 12 +- .../common/userDataProfileService.test.ts | 5 +- .../userDataProfileMainService.test.ts | 4 + .../userDataSync/common/extensionsSync.ts | 14 +- .../common/userDataProfilesManifestSync.ts | 18 +- .../common/userDataSyncService.ts | 13 ++ .../server/node/remoteAgentEnvironmentImpl.ts | 3 - src/vs/server/node/serverServices.ts | 4 +- .../extensions/browser/extensionsActions.ts | 3 +- .../browser/webExtensionsScannerService.ts | 24 +- .../common/extensionManagement.ts | 12 +- .../common/webExtensionManagementService.ts | 19 +- .../nativeExtensionManagementService.ts | 4 +- .../cachedExtensionScanner.ts | 8 +- .../common/userDataProfileService.ts | 9 + .../electron-browser/workbenchTestServices.ts | 2 +- 29 files changed, 570 insertions(+), 370 deletions(-) delete mode 100644 src/vs/code/electron-browser/sharedProcess/contrib/extensions.ts create mode 100644 src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts create mode 100644 src/vs/platform/extensionManagement/electron-main/defaultExtensionsProfileInit.ts diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/extensions.ts b/src/vs/code/electron-browser/sharedProcess/contrib/extensions.ts deleted file mode 100644 index 2ae46cb355a36..0000000000000 --- a/src/vs/code/electron-browser/sharedProcess/contrib/extensions.ts +++ /dev/null @@ -1,33 +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 { 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 new file mode 100644 index 0000000000000..7da094c59650b --- /dev/null +++ b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts @@ -0,0 +1,186 @@ +/*--------------------------------------------------------------------------------------------- + * 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 8c904e2530808..f5bd9adc5a709 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -14,6 +14,7 @@ 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'; @@ -112,7 +113,6 @@ 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(ExtensionsContributions), + instantiationService.createInstance(ExtensionsCleaner), instantiationService.createInstance(UserDataProfilesCleaner) )); } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index a9d491c490808..b8e21690c1eb6 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -101,6 +101,7 @@ 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'; @@ -557,6 +558,9 @@ 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 3cf630f878fdf..6c65566f766e7 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) ?? userDataProfilesService.defaultProfile : userDataProfilesService.defaultProfile).extensionsResource; + const profileLocation = environmentService.args.profile ? userDataProfilesService.profiles.find(p => p.name === environmentService.args.profile)?.extensionsResource : 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 183e4b527c640..52696de6e8176 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -24,7 +24,6 @@ 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; @@ -35,7 +34,6 @@ export interface IInstallExtensionTask { cancel(): void; } -export type UninstallExtensionTaskOptions = UninstallOptions & { readonly profileLocation: URI }; export interface IUninstallExtensionTask { readonly extension: ILocalExtension; run(): Promise; @@ -107,6 +105,22 @@ 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(); @@ -124,12 +138,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl protected async installExtension(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions & InstallVSIXOptions): Promise { - 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()}` : ''}`; + const getInstallExtensionTaskKey = (extension: IGalleryExtension) => `${ExtensionKey.create(extension).toString()}${options.profileLocation ? `-${options.profileLocation.toString()}` : ''}`; // only cache gallery extensions tasks if (!URI.isUri(extension)) { @@ -139,27 +148,28 @@ 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, installExtensionTaskOptions); + const installExtensionTask = this.createInstallExtensionTask(manifest, extension, options); if (!URI.isUri(extension)) { this.installingExtensions.set(getInstallExtensionTaskKey(extension), { task: installExtensionTask, waitingTasks: [] }); } - this._onInstallExtension.fire({ identifier: installExtensionTask.identifier, source: extension, profileLocation: installExtensionTaskOptions.profileLocation }); + this._onInstallExtension.fire({ identifier: installExtensionTask.identifier, source: extension, profileLocation: options.profileLocation }); this.logService.info('Installing extension:', installExtensionTask.identifier.id); allInstallExtensionTasks.push({ task: installExtensionTask, manifest }); let installExtensionHasDependents: boolean = false; try { - if (installExtensionTaskOptions.donotIncludePackAndDependencies) { + if (options.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, !!installExtensionTaskOptions.installOnlyNewlyAddedFromExtensionPack, !!installExtensionTaskOptions.installPreReleaseVersion, installExtensionTaskOptions.profileLocation); - const installed = await this.getInstalled(undefined, installExtensionTaskOptions.profileLocation); + const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(installExtensionTask.identifier, manifest, !!options.installOnlyNewlyAddedFromExtensionPack, !!options.installPreReleaseVersion, options.profileLocation); + const installed = await this.getInstalled(undefined, options.profileLocation); for (const { gallery, manifest } of allDepsAndPackExtensionsToInstall) { installExtensionHasDependents = installExtensionHasDependents || !!manifest.extensionDependencies?.some(id => areSameExtensions({ id }, installExtensionTask.identifier)); const key = getInstallExtensionTaskKey(gallery); @@ -182,9 +192,9 @@ export abstract class AbstractExtensionManagementService extends Disposable impl })); } } else if (!installed.some(({ identifier }) => areSameExtensions(identifier, gallery.identifier))) { - const task = this.createInstallExtensionTask(manifest, gallery, { ...installExtensionTaskOptions, donotIncludePackAndDependencies: true }); + const task = this.createInstallExtensionTask(manifest, gallery, { ...options, donotIncludePackAndDependencies: true }); this.installingExtensions.set(key, { task, waitingTasks: [installExtensionTask] }); - this._onInstallExtension.fire({ identifier: task.identifier, source: gallery, profileLocation: installExtensionTaskOptions.profileLocation }); + this._onInstallExtension.fire({ identifier: task.identifier, source: gallery, profileLocation: options.profileLocation }); this.logService.info('Installing extension:', task.identifier.id, installExtensionTask.identifier.id); allInstallExtensionTasks.push({ task, manifest }); } @@ -228,7 +238,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, installExtensionTaskOptions, CancellationToken.None))); + await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, options, CancellationToken.None))); if (!URI.isUri(task.source)) { const isUpdate = task.operation === InstallOperation.Update; const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; @@ -245,7 +255,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } catch (error) { /* ignore */ } } } - installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source, context: installExtensionTaskOptions.context, profileLocation: installExtensionTaskOptions.profileLocation, applicationScoped: local.isApplicationScoped }); + installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source, context: options.context, profileLocation: options.profileLocation, applicationScoped: local.isApplicationScoped }); } catch (error) { if (!URI.isUri(task.source)) { reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { @@ -277,7 +287,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: installExtensionTaskOptions.profileLocation }).run())); + const result = await Promise.allSettled(installResults.map(({ local }) => this.createUninstallExtensionTask(local, { versionOnly: true, profileLocation: options.profileLocation }).run())); for (let index = 0; index < result.length; index++) { const r = result[index]; const { identifier } = installResults[index]; @@ -293,7 +303,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } } - this._onDidInstallExtensions.fire(allInstallExtensionTasks.map(({ task }) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: installExtensionTaskOptions.context, profileLocation: installExtensionTaskOptions.profileLocation }))); + this._onDidInstallExtensions.fire(allInstallExtensionTasks.map(({ task }) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: options.context, profileLocation: options.profileLocation }))); throw error; } finally { for (const [key, { task, waitingTasks }] of this.installingExtensions.entries()) { @@ -465,54 +475,50 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } private async uninstallExtension(extension: ILocalExtension, options: UninstallOptions): Promise { - 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 getUninstallExtensionTaskKey = (identifier: IExtensionIdentifier) => `${identifier.id.toLowerCase()}${options.versionOnly ? `-${extension.manifest.version}` : ''}${options.profileLocation ? `@${options.profileLocation.toString()}` : ''}`; const uninstallExtensionTask = this.uninstallingExtensions.get(getUninstallExtensionTaskKey(extension.identifier)); if (uninstallExtensionTask) { this.logService.info('Extensions is already requested to uninstall', extension.identifier.id); return uninstallExtensionTask.waitUntilTaskIsFinished(); } - const createUninstallExtensionTask = (extension: ILocalExtension): IUninstallExtensionTask => { + const createUninstallExtensionTask = (extension: ILocalExtension, uninstallOptions: UninstallOptions): IUninstallExtensionTask => { const uninstallExtensionTask = this.createUninstallExtensionTask(extension, uninstallOptions); this.uninstallingExtensions.set(getUninstallExtensionTaskKey(uninstallExtensionTask.extension.identifier), uninstallExtensionTask); - if (uninstallOptions.profileLocation) { - this.logService.info('Uninstalling extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString()); + if (options.profileLocation) { + this.logService.info('Uninstalling extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, options.profileLocation.toString()); } else { this.logService.info('Uninstalling extension:', `${extension.identifier.id}@${extension.manifest.version}`); } - this._onUninstallExtension.fire({ identifier: extension.identifier, profileLocation: uninstallOptions.profileLocation, applicationScoped: extension.isApplicationScoped }); + this._onUninstallExtension.fire({ identifier: extension.identifier, profileLocation: options.profileLocation, applicationScoped: extension.isApplicationScoped }); return uninstallExtensionTask; }; const postUninstallExtension = (extension: ILocalExtension, error?: ExtensionManagementError): void => { if (error) { - if (uninstallOptions.profileLocation) { - this.logService.error('Failed to uninstall extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString(), error.message); + if (options.profileLocation) { + this.logService.error('Failed to uninstall extension from the profile:', `${extension.identifier.id}@${extension.manifest.version}`, options.profileLocation.toString(), error.message); } else { this.logService.error('Failed to uninstall extension:', `${extension.identifier.id}@${extension.manifest.version}`, error.message); } } else { - if (uninstallOptions.profileLocation) { - this.logService.info('Successfully uninstalled extension from the profile', `${extension.identifier.id}@${extension.manifest.version}`, uninstallOptions.profileLocation.toString()); + if (options.profileLocation) { + this.logService.info('Successfully uninstalled extension from the profile', `${extension.identifier.id}@${extension.manifest.version}`, options.profileLocation.toString()); } else { this.logService.info('Successfully uninstalled extension:', `${extension.identifier.id}@${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: uninstallOptions.profileLocation, applicationScoped: extension.isApplicationScoped }); + this._onDidUninstallExtension.fire({ identifier: extension.identifier, version: extension.manifest.version, error: error?.code, profileLocation: options.profileLocation, applicationScoped: extension.isApplicationScoped }); }; const allTasks: IUninstallExtensionTask[] = []; const processedTasks: IUninstallExtensionTask[] = []; try { - allTasks.push(createUninstallExtensionTask(extension)); - const installed = await this.getInstalled(ExtensionType.User, uninstallOptions.profileLocation); - if (uninstallOptions.donotIncludePack) { + allTasks.push(createUninstallExtensionTask(extension, options)); + const installed = await this.getInstalled(ExtensionType.User, options.profileLocation); + if (options.donotIncludePack) { this.logService.info('Uninstalling the extension without including packed extension', `${extension.identifier.id}@${extension.manifest.version}`); } else { const packedExtensions = this.getAllPackExtensionsToUninstall(extension, installed); @@ -520,12 +526,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)); + allTasks.push(createUninstallExtensionTask(packedExtension, options)); } } } - if (uninstallOptions.donotCheckDependents) { + if (options.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); @@ -535,7 +541,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, uninstallOptions, CancellationToken.None))); + await this.joinAllSettled(this.participants.map(participant => participant.postUninstall(task.extension, options, CancellationToken.None))); // only report if extension has a mapped gallery extension. UUID identifies the gallery extension. if (task.extension.identifier.uuid) { try { @@ -646,6 +652,20 @@ 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; @@ -653,14 +673,13 @@ 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 createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask; - protected abstract createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask; + protected abstract doCreateInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions & InstallVSIXOptions): IInstallExtensionTask; + protected abstract doCreateUninstallExtensionTask(extension: ILocalExtension, options: UninstallOptions): 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 89907b63d3c19..ba3a2b083ce58 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -16,7 +16,6 @@ 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) { @@ -499,11 +498,13 @@ 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 5bcb3ee7cdcc6..a182f889b9f51 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 | undefined][], profileLocation: URI): Promise { + addExtensionsToProfile(extensions: [IExtension, Metadata][], 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 3a49730edd0fc..9ecfff2b3219d 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -190,9 +190,8 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanUserExtensions(scanOptions: ScanOptions): Promise { - 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); + 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 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 new file mode 100644 index 0000000000000..12e6315732003 --- /dev/null +++ b/src/vs/platform/extensionManagement/electron-main/defaultExtensionsProfileInit.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * 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 34dc946046bf2..520c5667aaef9 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ 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'; @@ -23,10 +22,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, InstallExtensionTaskOptions, IUninstallExtensionTask, joinErrors, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, joinErrors } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, - Metadata, InstallOptions, InstallVSIXOptions + Metadata, InstallOptions, InstallVSIXOptions, UninstallOptions } 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'; @@ -55,9 +54,7 @@ interface InstallableExtension { export const INativeServerExtensionManagementService = refineServiceDecorator(IExtensionManagementService); export interface INativeServerExtensionManagementService extends IExtensionManagementService { readonly _serviceBrand: undefined; - migrateDefaultProfileExtensions(): Promise; - markAsUninstalled(...extensions: ILocalExtension[]): Promise; - removeUninstalledExtensions(): Promise; + removeUninstalledExtensions(removeOutdated: boolean): Promise; getAllUserInstalled(): Promise; } @@ -88,13 +85,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, userDataProfilesService, extensionsProfileScannerService, extensionsScannerService, uriIdentityService, fileService, logService)); + const extensionsWatcher = this._register(new ExtensionsWatcher(this, fileService, environmentService, logService, uriIdentityService)); this._register(extensionsWatcher.onDidChangeExtensionsByAnotherSource(({ added, removed }) => { if (added.length) { - this._onDidInstallExtensions.fire(added); + this._onDidInstallExtensions.fire(added.map(local => ({ identifier: local.identifier, operation: InstallOperation.None, local }))); } - removed.forEach(e => this._onDidUninstallExtension.fire(e)); + removed.forEach(extension => this._onDidUninstallExtension.fire({ identifier: extension })); })); } @@ -134,7 +131,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } getAllUserInstalled(): Promise { - return this.extensionsScanner.scanAllUserExtensions(false); + return this.extensionsScanner.scanUserExtensions(false); } async install(vsix: URI, options: InstallVSIXOptions = {}): Promise { @@ -176,37 +173,8 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return local; } - 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(); + removeUninstalledExtensions(removeOutdated: boolean): Promise { + return this.extensionsScanner.cleanUp(removeOutdated); } async download(extension: IGalleryExtension, operation: InstallOperation): Promise { @@ -232,7 +200,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return { location, cleanup }; } - protected createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask { + protected doCreateInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions & InstallVSIXOptions): IInstallExtensionTask { let installExtensionTask: IInstallExtensionTask | undefined; if (URI.isUri(extension)) { installExtensionTask = new InstallVSIXTask(manifest, extension, options, this.galleryService, this.extensionsScanner, this.logService); @@ -244,11 +212,17 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi installExtensionTask.waitUntilTaskIsFinished().then(() => this.installGalleryExtensionsTasks.delete(key)); } } - return new InstallExtensionInProfileTask(installExtensionTask, options.profileLocation, this.extensionsProfileScannerService); + if (options.profileLocation) { + return new InstallExtensionInProfileTask(installExtensionTask, options.profileLocation, this.extensionsProfileScannerService); + } + return installExtensionTask; } - protected createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask { - return new UninstallExtensionFromProfileTask(extension, 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); } private async collectFiles(extension: ILocalExtension): Promise { @@ -287,8 +261,6 @@ 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(); @@ -296,12 +268,14 @@ export class ExtensionsScanner extends Disposable { this.uninstalledFileLimiter = new Queue(); } - async cleanUp(): Promise { + async cleanUp(removeOutdated: boolean): 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) { @@ -313,13 +287,12 @@ export class ExtensionsScanner extends Disposable { return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); } - async scanAllUserExtensions(excludeOutdated: boolean): Promise { + async scanUserExtensions(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); @@ -362,16 +335,14 @@ 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; - this.logService.info('Marked extension as uninstalled', extensionKey.toString()); - })); + await this.withUninstalledExtensions(uninstalled => { + extensionKeys.forEach(extensionKey => uninstalled[extensionKey.toString()] = true); + }); } async setInstalled(extensionKey: ExtensionKey): Promise { await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); - const userExtensions = await this.scanAllUserExtensions(true); + const userExtensions = await this.scanUserExtensions(true); const localExtension = userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)) || null; if (!localExtension) { return null; @@ -518,33 +489,35 @@ export class ExtensionsScanner extends Disposable { await Promises.settled(toRemove.map(e => this.removeUninstalledExtension(e))); } - private _migrateDefaultProfileExtensionsPromise: Promise | undefined = undefined; - migrateDefaultProfileExtensions(): Promise { - if (!this._migrateDefaultProfileExtensionsPromise) { - this._migrateDefaultProfileExtensionsPromise = (async () => { - try { - const migrationMarkerFile = joinPath(this.extensionsScannerService.userExtensionsLocation, '.migrated-default-profile'); - if (await this.fileService.exists(migrationMarkerFile)) { - return; - } - 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.'); + 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; } - 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)); + if (a.targetPlatform === targetPlatform) { + return -1; } - } catch (error) { - this.logService.error(error); - throw error; + 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]); } - })(); + } } - return this._migrateDefaultProfileExtensionsPromise; + await Promises.settled(toRemove.map(extension => this.removeExtension(extension, 'outdated'))); } private joinErrors(errorOrErrors: (Error | string) | (Array)): Error { @@ -802,6 +775,46 @@ 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 e04c1b8b86963..2693b4e10bda5 100644 --- a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts +++ b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts @@ -6,49 +6,42 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -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 { 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 { 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: InstallExtensionResult[]; removed: DidUninstallExtensionEvent[] }>()); + private readonly _onDidChangeExtensionsByAnotherSource = this._register(new Emitter<{ added: ILocalExtension[]; removed: IExtensionIdentifier[] }>()); readonly onDidChangeExtensionsByAnotherSource = this._onDidChangeExtensionsByAnotherSource.event; - private readonly profileExtensionsLocations = new Map; + private startTimestamp = 0; + private installingExtensions: IExtensionIdentifier[] = []; + private installedExtensions: IExtensionIdentifier[] | undefined; constructor( - 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, + private readonly extensionsManagementService: IExtensionManagementService, + @IFileService fileService: IFileService, + @INativeEnvironmentService environmentService: INativeEnvironmentService, + @ILogService private readonly logService: ILogService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, ) { super(); - this.initialize().then(() => this.registerListeners(), error => logService.error(error)); - } - - private async initialize(): Promise { - await this.extensionManagementService.migrateDefaultProfileExtensions(); - await this.onDidChangeProfiles(this.userDataProfilesService.profiles, []); - await this.uninstallExtensionsNotInProfiles(); - } + 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))); - 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())); + 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 doesChangeAffects(change: IFileChange, extensionsResource: URI): boolean { @@ -75,136 +68,78 @@ export class ExtensionsWatcher extends Disposable { return true; } - 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 onInstallExtension(e: InstallExtensionEvent): void { + this.addInstallingExtension(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))); + private onDidInstallExtensions(results: readonly InstallExtensionResult[]): void { + for (const e of results) { + this.removeInstallingExtension(e.identifier); + if (e.local) { + this.addInstalledExtension(e.identifier); } - } catch (error) { - this.logService.error(error); } } - 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 onDidUninstallExtension(e: DidUninstallExtensionEvent): void { + if (!e.error) { + this.removeInstalledExtension(e.identifier); } } - 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 addInstallingExtension(extension: IExtensionIdentifier) { + this.removeInstallingExtension(extension); + this.installingExtensions.push(extension); } - 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 removeInstallingExtension(identifier: IExtensionIdentifier) { + this.installingExtensions = this.installingExtensions.filter(e => !areSameExtensions(e, identifier)); } - 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 addInstalledExtension(extension: IExtensionIdentifier): void { + if (this.installedExtensions) { + this.removeInstalledExtension(extension); + this.installedExtensions.push(extension); } } - private addExtensionWithKey(key: string, extensionsProfileLocation: URI): void { - let locations = this.profileExtensionsLocations.get(key); - if (!locations) { - locations = []; - this.profileExtensionsLocations.set(key, locations); + private removeInstalledExtension(identifier: IExtensionIdentifier): void { + if (this.installedExtensions) { + this.installedExtensions = this.installedExtensions.filter(e => !areSameExtensions(e, identifier)); } - 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); + 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 }); } } - 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 a3144510cf629..8d4253f2c8899 100644 --- a/src/vs/platform/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/browser/userDataProfile.ts @@ -16,6 +16,7 @@ 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 b74d2b1cfde71..2207f1a08e4bb 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -7,6 +7,7 @@ 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'; @@ -44,7 +45,7 @@ export interface IUserDataProfile { readonly keybindingsResource: URI; readonly tasksResource: URI; readonly snippetsHome: URI; - readonly extensionsResource: URI; + readonly extensionsResource: URI | undefined; readonly useDefaultFlags?: UseDefaultProfileFlags; readonly isTransient?: boolean; } @@ -62,7 +63,7 @@ export function isUserDataProfile(thing: unknown): thing is IUserDataProfile { && URI.isUri(candidate.keybindingsResource) && URI.isUri(candidate.tasksResource) && URI.isUri(candidate.snippetsHome) - && URI.isUri(candidate.extensionsResource) + && (isUndefined(candidate.extensionsResource) || URI.isUri(candidate.extensionsResource)) ); } @@ -137,6 +138,8 @@ 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, @@ -149,7 +152,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.json'), + extensionsResource: joinPath(location, EXTENSIONS_RESOURCE_NAME), useDefaultFlags: options?.useDefaultFlags, isTransient: options?.transient }; @@ -181,6 +184,7 @@ 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]; } @@ -233,7 +237,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 }); + profiles.unshift({ ...defaultProfile, isDefault: true, extensionsResource: this.defaultProfileShouldIncludeExtensionsResourceAlways || profiles.length > 0 || this.transientProfilesObject.profiles.length > 0 ? defaultProfile.extensionsResource : undefined }); 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 e146018fb7c4a..80475cfdc1074 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts @@ -54,12 +54,13 @@ 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.toString(), joinPath(environmentService.userRoamingDataHome, 'extensions.json').toString()); + 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('create profile with id', async () => { @@ -115,6 +116,7 @@ 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 () => { @@ -123,6 +125,7 @@ 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 ddca68846011b..7078f0757a4a2 100644 --- a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts +++ b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts @@ -51,22 +51,26 @@ 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 4e61627c14dce..87098ee8e7135 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 - profile: IUserDataProfile, + public 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.syncResource.profile); + const { localExtensions, ignoredExtensions } = await this.localExtensionsProvider.getLocalExtensions(this.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.syncResource.profile); + const { localExtensions, ignoredExtensions } = await this.localExtensionsProvider.getLocalExtensions(this.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.syncResource.profile.extensionsResource); + const installedExtensions = await this.extensionManagementService.getInstalled(undefined, this.profile.location); 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.syncResource.profile.extensionsResource); + const installedExtensions = await this.extensionManagementService.getInstalled(undefined, this.profile.location); 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.syncResource.profile); + skippedExtensions = await this.localExtensionsProvider.updateLocalExtensions(local.added, local.removed, local.updated, skippedExtensions, this.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.syncResource.profile); + const { localExtensions } = await this.localExtensionsProvider.getLocalExtensions(this.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 8be70621bfdc7..4d3767d988d43 100644 --- a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts +++ b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts @@ -180,27 +180,15 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i await this.backupLocal(stringifyLocalProfiles(this.getLocalUserDataProfiles(), false)); const promises: Promise[] = []; for (const profile of local.added) { - 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}'.`); - })()); + promises.push(this.userDataProfilesService.createProfile(profile.id, profile.name, { shortName: profile.shortName })); } for (const profile of local.removed) { - 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}'.`); - })()); + promises.push(this.userDataProfilesService.removeProfile(profile)); } for (const profile of local.updated) { const localProfile = this.userDataProfilesService.profiles.find(p => p.id === profile.id); if (localProfile) { - 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}'.`); - })()); + promises.push(this.userDataProfilesService.updateProfile(localProfile, { name: profile.name, shortName: profile.shortName })); } 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 2741e974dd259..ab5261c5cc38e 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -542,6 +542,19 @@ 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 f16217b0125bc..7f581f36ea58d 100644 --- a/src/vs/server/node/remoteAgentEnvironmentImpl.ts +++ b/src/vs/server/node/remoteAgentEnvironmentImpl.ts @@ -29,7 +29,6 @@ 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 { @@ -45,7 +44,6 @@ 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'] }; @@ -336,7 +334,6 @@ 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 9d94002e098de..6978f8f6e4f47 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, extensionManagementService); + const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, userDataProfilesService, instantiationService.createInstance(ExtensionManagementCLI), logService, extensionHostStatusService, extensionsScannerService); 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(); + extensionManagementService.removeUninstalledExtensions(true); 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 7c0e6f44ab830..3569cdb16573a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -2501,7 +2501,6 @@ 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, @@ -2524,7 +2523,7 @@ export class ReinstallAction extends Action { return this.extensionsWorkbenchService.queryLocal() .then(local => { const entries = local - .filter(extension => !extension.isBuiltin && extension.server !== this.extensionManagementServerService.webExtensionManagementServer) + .filter(extension => !extension.isBuiltin) .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 abf7fbbcd96be..905f49db4de6d 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.uriIdentityService.extUri.isEqual(profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource)) { + if (this.userDataProfilesService.defaultProfile.extensionsResource && !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, updateFn: (extensions: IWebExtension[]) => IWebExtension[]): Promise { + private writeInstalledExtensions(profileLocation: URI | undefined, 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 aecd0f677dbb6..34d4367b5252c 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, options?: ScanOptions): Promise; + scanUserExtensions(profileLocation: URI | undefined, options?: ScanOptions): Promise; scanExtensionsUnderDevelopment(): Promise; - scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, profileLocation: URI): Promise; + scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, 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; + 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; copyExtensions(fromProfileLocation: URI, toProfileLocation: URI, filter: (extension: IScannedExtension) => boolean): Promise; - scanMetadata(extensionLocation: URI, profileLocation: URI): Promise; + scanMetadata(extensionLocation: URI, profileLocation: URI | undefined): 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 0deec1d363161..79333395143a1 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 } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, IGalleryMetadata, InstallOperation, IExtensionGalleryService, Metadata, InstallOptions, UninstallOptions } 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, InstallExtensionTaskOptions, IUninstallExtensionTask, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -129,11 +129,17 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe return local; } - protected createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask { + protected doCreateInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions): IInstallExtensionTask { + if (!options.profileLocation) { + options = { ...options, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }; + } return new InstallExtensionTask(manifest, extension, options, this.webExtensionsScannerService); } - protected createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask { + protected doCreateUninstallExtensionTask(extension: ILocalExtension, options: UninstallOptions): IUninstallExtensionTask { + if (!options.profileLocation) { + options = { ...options, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }; + } return new UninstallExtensionTask(extension, options, this.webExtensionsScannerService); } @@ -142,7 +148,6 @@ 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; @@ -197,7 +202,7 @@ class InstallExtensionTask extends AbstractExtensionTask<{ local: ILocalExtensio constructor( manifest: IExtensionManifest, private readonly extension: URI | IGalleryExtension, - private readonly options: InstallExtensionTaskOptions, + private readonly options: InstallOptions, private readonly webExtensionsScannerService: IWebExtensionsScannerService, ) { super(); @@ -238,7 +243,7 @@ class UninstallExtensionTask extends AbstractExtensionTask implements IUni constructor( readonly extension: ILocalExtension, - private readonly options: UninstallExtensionTaskOptions, + private readonly options: UninstallOptions, 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 6d2bf5f714b7e..da1f95ec42ecb 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts @@ -15,6 +15,7 @@ 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'; @@ -105,7 +106,8 @@ export class NativeExtensionManagementService extends ExtensionManagementChannel } private async whenProfileChanged(e: DidChangeUserDataProfileEvent): Promise { - const oldExtensions = await super.getInstalled(ExtensionType.User, e.previous.extensionsResource); + const previousExtensionsResource = e.previous.extensionsResource ?? joinPath(e.previous.location, EXTENSIONS_RESOURCE_NAME); + const oldExtensions = await super.getInstalled(ExtensionType.User, previousExtensionsResource); 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 b2a8611aec9ed..dd9be1540682b 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -16,9 +16,6 @@ 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 { @@ -31,8 +28,6 @@ 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) => { @@ -58,10 +53,9 @@ 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, useCache: true })]); + this._extensionsScannerService.scanUserExtensions({ language, profileLocation: this._userDataProfileService.currentProfile.extensionsResource, 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 2f2f2c8a1fc20..63b78ba06b763 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts @@ -34,6 +34,15 @@ 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 4b09620745a50..ebec1acd03c12 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: joinPath(URI.file(homeDir), 'extensions.json') + extensionsResource: undefined }; export const TestNativeWindowConfiguration: INativeWindowConfiguration = {