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