@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
212 lines (211 loc) • 8.11 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter } from '@sussudio/base/common/event.mjs';
import { combinedDisposable, Disposable, DisposableMap } from '@sussudio/base/common/lifecycle.mjs';
import { ResourceSet } from '@sussudio/base/common/map.mjs';
import { getIdAndVersion } from '../common/extensionManagementUtil.mjs';
import { ExtensionIdentifier } from '../../extensions/common/extensions.mjs';
export class ExtensionsWatcher extends Disposable {
extensionManagementService;
extensionsScannerService;
userDataProfilesService;
extensionsProfileScannerService;
uriIdentityService;
fileService;
logService;
_onDidChangeExtensionsByAnotherSource = this._register(new Emitter());
onDidChangeExtensionsByAnotherSource = this._onDidChangeExtensionsByAnotherSource.event;
allExtensions = new Map();
extensionsProfileWatchDisposables = this._register(new DisposableMap());
constructor(
extensionManagementService,
extensionsScannerService,
userDataProfilesService,
extensionsProfileScannerService,
uriIdentityService,
fileService,
logService,
) {
super();
this.extensionManagementService = extensionManagementService;
this.extensionsScannerService = extensionsScannerService;
this.userDataProfilesService = userDataProfilesService;
this.extensionsProfileScannerService = extensionsProfileScannerService;
this.uriIdentityService = uriIdentityService;
this.fileService = fileService;
this.logService = logService;
this.initialize().then(null, (error) => logService.error(error));
}
async initialize() {
await this.extensionsScannerService.initializeDefaultProfileExtensions();
await this.onDidChangeProfiles(this.userDataProfilesService.profiles, []);
this.registerListeners();
await this.uninstallExtensionsNotInProfiles();
}
registerListeners() {
this._register(
this.userDataProfilesService.onDidChangeProfiles((e) => this.onDidChangeProfiles(e.added, e.removed)),
);
this._register(this.extensionsProfileScannerService.onAddExtensions((e) => this.onAddExtensions(e)));
this._register(this.extensionsProfileScannerService.onDidAddExtensions((e) => this.onDidAddExtensions(e)));
this._register(this.extensionsProfileScannerService.onRemoveExtensions((e) => this.onRemoveExtensions(e)));
this._register(this.extensionsProfileScannerService.onDidRemoveExtensions((e) => this.onDidRemoveExtensions(e)));
this._register(this.fileService.onDidFilesChange((e) => this.onDidFilesChange(e)));
}
async onDidChangeProfiles(added, removed) {
try {
await Promise.all(
removed.map((profile) => {
this.extensionsProfileWatchDisposables.deleteAndDispose(profile.id);
return this.removeExtensionsFromProfile(profile.extensionsResource);
}),
);
if (added.length) {
await Promise.all(
added.map((profile) => {
this.extensionsProfileWatchDisposables.set(
profile.id,
combinedDisposable(
this.fileService.watch(this.uriIdentityService.extUri.dirname(profile.extensionsResource)),
// Also listen to the resource incase the resource is a symlink - https://github.com/microsoft/vscode/issues/118134
this.fileService.watch(profile.extensionsResource),
),
);
return this.populateExtensionsFromProfile(profile.extensionsResource);
}),
);
}
} catch (error) {
this.logService.error(error);
throw error;
}
}
async onAddExtensions(e) {
for (const extension of e.extensions) {
this.addExtensionWithKey(this.getKey(extension.identifier, extension.version), e.profileLocation);
}
}
async onDidAddExtensions(e) {
for (const extension of e.extensions) {
const key = this.getKey(extension.identifier, extension.version);
if (e.error) {
this.removeExtensionWithKey(key, e.profileLocation);
} else {
this.addExtensionWithKey(key, e.profileLocation);
}
}
}
async onRemoveExtensions(e) {
for (const extension of e.extensions) {
this.removeExtensionWithKey(this.getKey(extension.identifier, extension.version), e.profileLocation);
}
}
async onDidRemoveExtensions(e) {
let hasToUninstallExtensions = false;
for (const extension of e.extensions) {
const key = this.getKey(extension.identifier, extension.version);
if (e.error) {
this.addExtensionWithKey(key, e.profileLocation);
} else {
this.removeExtensionWithKey(key, e.profileLocation);
hasToUninstallExtensions = hasToUninstallExtensions || !this.allExtensions.has(key);
}
}
if (hasToUninstallExtensions) {
await this.uninstallExtensionsNotInProfiles();
}
}
onDidFilesChange(e) {
for (const profile of this.userDataProfilesService.profiles) {
if (e.contains(profile.extensionsResource, 0 /* FileChangeType.UPDATED */, 1 /* FileChangeType.ADDED */)) {
this.onDidExtensionsProfileChange(profile.extensionsResource);
}
}
}
async onDidExtensionsProfileChange(profileLocation) {
const added = [],
removed = [];
const extensions = await this.extensionsProfileScannerService.scanProfileExtensions(profileLocation);
const extensionKeys = new Set();
const cached = new Set();
for (const [key, profiles] of this.allExtensions) {
if (profiles.has(profileLocation)) {
cached.add(key);
}
}
for (const extension of extensions) {
const key = this.getKey(extension.identifier, extension.version);
extensionKeys.add(key);
if (!cached.has(key)) {
added.push(extension.identifier);
this.addExtensionWithKey(key, profileLocation);
}
}
for (const key of cached) {
if (!extensionKeys.has(key)) {
const extension = this.fromKey(key);
if (extension) {
removed.push(extension.identifier);
this.removeExtensionWithKey(key, profileLocation);
}
}
}
if (added.length || removed.length) {
this._onDidChangeExtensionsByAnotherSource.fire({
added: added.length ? { extensions: added, profileLocation } : undefined,
removed: removed.length ? { extensions: removed, profileLocation } : undefined,
});
}
}
async populateExtensionsFromProfile(extensionsProfileLocation) {
const extensions = await this.extensionsProfileScannerService.scanProfileExtensions(extensionsProfileLocation);
for (const extension of extensions) {
this.addExtensionWithKey(this.getKey(extension.identifier, extension.version), extensionsProfileLocation);
}
}
async removeExtensionsFromProfile(removedProfile) {
for (const key of [...this.allExtensions.keys()]) {
this.removeExtensionWithKey(key, removedProfile);
}
await this.uninstallExtensionsNotInProfiles();
}
async uninstallExtensionsNotInProfiles() {
const installed = await this.extensionManagementService.getAllUserInstalled();
const toUninstall = installed.filter(
(installedExtension) =>
installedExtension.installedTimestamp /* Installed by VS Code */ &&
!this.allExtensions.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version)),
);
if (toUninstall.length) {
await this.extensionManagementService.markAsUninstalled(...toUninstall);
}
}
addExtensionWithKey(key, extensionsProfileLocation) {
let profiles = this.allExtensions.get(key);
if (!profiles) {
this.allExtensions.set(
key,
(profiles = new ResourceSet((uri) => this.uriIdentityService.extUri.getComparisonKey(uri))),
);
}
profiles.add(extensionsProfileLocation);
}
removeExtensionWithKey(key, profileLocation) {
const profiles = this.allExtensions.get(key);
if (profiles) {
profiles.delete(profileLocation);
}
if (!profiles?.size) {
this.allExtensions.delete(key);
}
}
getKey(identifier, version) {
return `${ExtensionIdentifier.toKey(identifier.id)}@${version}`;
}
fromKey(key) {
const [id, version] = getIdAndVersion(key);
return version ? { identifier: { id }, version } : undefined;
}
}