Path: blob/main/src/vs/platform/extensionManagement/node/extensionsWatcher.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { getErrorMessage } from '../../../base/common/errors.js';6import { Emitter } from '../../../base/common/event.js';7import { combinedDisposable, Disposable, DisposableMap } from '../../../base/common/lifecycle.js';8import { ResourceSet } from '../../../base/common/map.js';9import { URI } from '../../../base/common/uri.js';10import { getIdAndVersion } from '../common/extensionManagementUtil.js';11import { DidAddProfileExtensionsEvent, DidRemoveProfileExtensionsEvent, IExtensionsProfileScannerService, ProfileExtensionsEvent } from '../common/extensionsProfileScannerService.js';12import { IExtensionsScannerService } from '../common/extensionsScannerService.js';13import { INativeServerExtensionManagementService } from './extensionManagementService.js';14import { ExtensionIdentifier, IExtension, IExtensionIdentifier } from '../../extensions/common/extensions.js';15import { FileChangesEvent, FileChangeType, IFileService } from '../../files/common/files.js';16import { ILogService } from '../../log/common/log.js';17import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';18import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';1920export interface DidChangeProfileExtensionsEvent {21readonly added?: { readonly extensions: readonly IExtensionIdentifier[]; readonly profileLocation: URI };22readonly removed?: { readonly extensions: readonly IExtensionIdentifier[]; readonly profileLocation: URI };23}2425export class ExtensionsWatcher extends Disposable {2627private readonly _onDidChangeExtensionsByAnotherSource = this._register(new Emitter<DidChangeProfileExtensionsEvent>());28readonly onDidChangeExtensionsByAnotherSource = this._onDidChangeExtensionsByAnotherSource.event;2930private readonly allExtensions = new Map<string, ResourceSet>;31private readonly extensionsProfileWatchDisposables = this._register(new DisposableMap<string>());3233constructor(34private readonly extensionManagementService: INativeServerExtensionManagementService,35private readonly extensionsScannerService: IExtensionsScannerService,36private readonly userDataProfilesService: IUserDataProfilesService,37private readonly extensionsProfileScannerService: IExtensionsProfileScannerService,38private readonly uriIdentityService: IUriIdentityService,39private readonly fileService: IFileService,40private readonly logService: ILogService,41) {42super();43this.initialize().then(null, error => logService.error('Error while initializing Extensions Watcher', getErrorMessage(error)));44}4546private async initialize(): Promise<void> {47await this.extensionsScannerService.initializeDefaultProfileExtensions();48await this.onDidChangeProfiles(this.userDataProfilesService.profiles);49this.registerListeners();50await this.deleteExtensionsNotInProfiles();51}5253private registerListeners(): void {54this._register(this.userDataProfilesService.onDidChangeProfiles(e => this.onDidChangeProfiles(e.added)));55this._register(this.extensionsProfileScannerService.onAddExtensions(e => this.onAddExtensions(e)));56this._register(this.extensionsProfileScannerService.onDidAddExtensions(e => this.onDidAddExtensions(e)));57this._register(this.extensionsProfileScannerService.onRemoveExtensions(e => this.onRemoveExtensions(e)));58this._register(this.extensionsProfileScannerService.onDidRemoveExtensions(e => this.onDidRemoveExtensions(e)));59this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e)));60}6162private async onDidChangeProfiles(added: readonly IUserDataProfile[]): Promise<void> {63try {64if (added.length) {65await Promise.all(added.map(profile => {66this.extensionsProfileWatchDisposables.set(profile.id, combinedDisposable(67this.fileService.watch(this.uriIdentityService.extUri.dirname(profile.extensionsResource)),68// Also listen to the resource incase the resource is a symlink - https://github.com/microsoft/vscode/issues/11813469this.fileService.watch(profile.extensionsResource)70));71return this.populateExtensionsFromProfile(profile.extensionsResource);72}));73}74} catch (error) {75this.logService.error(error);76throw error;77}78}7980private async onAddExtensions(e: ProfileExtensionsEvent): Promise<void> {81for (const extension of e.extensions) {82this.addExtensionWithKey(this.getKey(extension.identifier, extension.version), e.profileLocation);83}84}8586private async onDidAddExtensions(e: DidAddProfileExtensionsEvent): Promise<void> {87for (const extension of e.extensions) {88const key = this.getKey(extension.identifier, extension.version);89if (e.error) {90this.removeExtensionWithKey(key, e.profileLocation);91} else {92this.addExtensionWithKey(key, e.profileLocation);93}94}95}9697private async onRemoveExtensions(e: ProfileExtensionsEvent): Promise<void> {98for (const extension of e.extensions) {99this.removeExtensionWithKey(this.getKey(extension.identifier, extension.version), e.profileLocation);100}101}102103private async onDidRemoveExtensions(e: DidRemoveProfileExtensionsEvent): Promise<void> {104const extensionsToDelete: IExtension[] = [];105const promises: Promise<void>[] = [];106for (const extension of e.extensions) {107const key = this.getKey(extension.identifier, extension.version);108if (e.error) {109this.addExtensionWithKey(key, e.profileLocation);110} else {111this.removeExtensionWithKey(key, e.profileLocation);112if (!this.allExtensions.has(key)) {113this.logService.debug('Extension is removed from all profiles', extension.identifier.id, extension.version);114promises.push(this.extensionManagementService.scanInstalledExtensionAtLocation(extension.location)115.then(result => {116if (result) {117extensionsToDelete.push(result);118} else {119this.logService.info('Extension not found at the location', extension.location.toString());120}121}, error => this.logService.error(error)));122}123}124}125try {126await Promise.all(promises);127if (extensionsToDelete.length) {128await this.deleteExtensionsNotInProfiles(extensionsToDelete);129}130} catch (error) {131this.logService.error(error);132}133}134135private onDidFilesChange(e: FileChangesEvent): void {136for (const profile of this.userDataProfilesService.profiles) {137if (e.contains(profile.extensionsResource, FileChangeType.UPDATED, FileChangeType.ADDED)) {138this.onDidExtensionsProfileChange(profile.extensionsResource);139}140}141}142143private async onDidExtensionsProfileChange(profileLocation: URI): Promise<void> {144const added: IExtensionIdentifier[] = [], removed: IExtensionIdentifier[] = [];145const extensions = await this.extensionsProfileScannerService.scanProfileExtensions(profileLocation);146const extensionKeys = new Set<string>();147const cached = new Set<string>();148for (const [key, profiles] of this.allExtensions) {149if (profiles.has(profileLocation)) {150cached.add(key);151}152}153for (const extension of extensions) {154const key = this.getKey(extension.identifier, extension.version);155extensionKeys.add(key);156if (!cached.has(key)) {157added.push(extension.identifier);158this.addExtensionWithKey(key, profileLocation);159}160}161for (const key of cached) {162if (!extensionKeys.has(key)) {163const extension = this.fromKey(key);164if (extension) {165removed.push(extension.identifier);166this.removeExtensionWithKey(key, profileLocation);167}168}169}170if (added.length || removed.length) {171this._onDidChangeExtensionsByAnotherSource.fire({ added: added.length ? { extensions: added, profileLocation } : undefined, removed: removed.length ? { extensions: removed, profileLocation } : undefined });172}173}174175private async populateExtensionsFromProfile(extensionsProfileLocation: URI): Promise<void> {176const extensions = await this.extensionsProfileScannerService.scanProfileExtensions(extensionsProfileLocation);177for (const extension of extensions) {178this.addExtensionWithKey(this.getKey(extension.identifier, extension.version), extensionsProfileLocation);179}180}181182private async deleteExtensionsNotInProfiles(toDelete?: IExtension[]): Promise<void> {183if (!toDelete) {184const installed = await this.extensionManagementService.scanAllUserInstalledExtensions();185toDelete = installed.filter(installedExtension => !this.allExtensions.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version)));186}187if (toDelete.length) {188await this.extensionManagementService.deleteExtensions(...toDelete);189}190}191192private addExtensionWithKey(key: string, extensionsProfileLocation: URI): void {193let profiles = this.allExtensions.get(key);194if (!profiles) {195this.allExtensions.set(key, profiles = new ResourceSet((uri) => this.uriIdentityService.extUri.getComparisonKey(uri)));196}197profiles.add(extensionsProfileLocation);198}199200private removeExtensionWithKey(key: string, profileLocation: URI): void {201const profiles = this.allExtensions.get(key);202if (profiles) {203profiles.delete(profileLocation);204}205if (!profiles?.size) {206this.allExtensions.delete(key);207}208}209210private getKey(identifier: IExtensionIdentifier, version: string): string {211return `${ExtensionIdentifier.toKey(identifier.id)}@${version}`;212}213214private fromKey(key: string): { identifier: IExtensionIdentifier; version: string } | undefined {215const [id, version] = getIdAndVersion(key);216return version ? { identifier: { id }, version } : undefined;217}218219}220221222