Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.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 { ExtensionIdentifier, ExtensionType, IExtension, IExtensionIdentifier, IExtensionManifest, TargetPlatform } from '../../../../platform/extensions/common/extensions.js';
7
import { ILocalExtension, IGalleryExtension, InstallOperation, IExtensionGalleryService, Metadata, InstallOptions, IProductVersion, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js';
8
import { URI } from '../../../../base/common/uri.js';
9
import { Emitter, Event } from '../../../../base/common/event.js';
10
import { areSameExtensions, getGalleryExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
11
import { IProfileAwareExtensionManagementService, IScannedExtension, IWebExtensionsScannerService } from './extensionManagement.js';
12
import { ILogService } from '../../../../platform/log/common/log.js';
13
import { CancellationToken } from '../../../../base/common/cancellation.js';
14
import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, toExtensionManagementError, UninstallExtensionTaskOptions } from '../../../../platform/extensionManagement/common/abstractExtensionManagementService.js';
15
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
16
import { IExtensionManifestPropertiesService } from '../../extensions/common/extensionManifestPropertiesService.js';
17
import { IProductService } from '../../../../platform/product/common/productService.js';
18
import { isBoolean, isUndefined } from '../../../../base/common/types.js';
19
import { DidChangeUserDataProfileEvent, IUserDataProfileService } from '../../userDataProfile/common/userDataProfile.js';
20
import { delta } from '../../../../base/common/arrays.js';
21
import { compare } from '../../../../base/common/strings.js';
22
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
23
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
24
import { DisposableStore } from '../../../../base/common/lifecycle.js';
25
26
export class WebExtensionManagementService extends AbstractExtensionManagementService implements IProfileAwareExtensionManagementService {
27
28
declare readonly _serviceBrand: undefined;
29
30
private readonly disposables = this._register(new DisposableStore());
31
32
get onProfileAwareInstallExtension() { return super.onInstallExtension; }
33
override get onInstallExtension() { return Event.filter(this.onProfileAwareInstallExtension, e => this.filterEvent(e), this.disposables); }
34
35
get onProfileAwareDidInstallExtensions() { return super.onDidInstallExtensions; }
36
override get onDidInstallExtensions() {
37
return Event.filter(
38
Event.map(this.onProfileAwareDidInstallExtensions, results => results.filter(e => this.filterEvent(e)), this.disposables),
39
results => results.length > 0, this.disposables);
40
}
41
42
get onProfileAwareUninstallExtension() { return super.onUninstallExtension; }
43
override get onUninstallExtension() { return Event.filter(this.onProfileAwareUninstallExtension, e => this.filterEvent(e), this.disposables); }
44
45
get onProfileAwareDidUninstallExtension() { return super.onDidUninstallExtension; }
46
override get onDidUninstallExtension() { return Event.filter(this.onProfileAwareDidUninstallExtension, e => this.filterEvent(e), this.disposables); }
47
48
private readonly _onDidChangeProfile = this._register(new Emitter<{ readonly added: ILocalExtension[]; readonly removed: ILocalExtension[] }>());
49
readonly onDidChangeProfile = this._onDidChangeProfile.event;
50
51
get onProfileAwareDidUpdateExtensionMetadata() { return super.onDidUpdateExtensionMetadata; }
52
53
constructor(
54
@IExtensionGalleryService extensionGalleryService: IExtensionGalleryService,
55
@ITelemetryService telemetryService: ITelemetryService,
56
@ILogService logService: ILogService,
57
@IWebExtensionsScannerService private readonly webExtensionsScannerService: IWebExtensionsScannerService,
58
@IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService,
59
@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,
60
@IProductService productService: IProductService,
61
@IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService,
62
@IUserDataProfilesService userDataProfilesService: IUserDataProfilesService,
63
@IUriIdentityService uriIdentityService: IUriIdentityService,
64
) {
65
super(extensionGalleryService, telemetryService, uriIdentityService, logService, productService, allowedExtensionsService, userDataProfilesService);
66
this._register(userDataProfileService.onDidChangeCurrentProfile(e => {
67
if (!this.uriIdentityService.extUri.isEqual(e.previous.extensionsResource, e.profile.extensionsResource)) {
68
e.join(this.whenProfileChanged(e));
69
}
70
}));
71
}
72
73
private filterEvent({ profileLocation, applicationScoped }: { profileLocation?: URI; applicationScoped?: boolean }): boolean {
74
profileLocation = profileLocation ?? this.userDataProfileService.currentProfile.extensionsResource;
75
return applicationScoped || this.uriIdentityService.extUri.isEqual(this.userDataProfileService.currentProfile.extensionsResource, profileLocation);
76
}
77
78
async getTargetPlatform(): Promise<TargetPlatform> {
79
return TargetPlatform.WEB;
80
}
81
82
protected override async isExtensionPlatformCompatible(extension: IGalleryExtension): Promise<boolean> {
83
if (this.isConfiguredToExecuteOnWeb(extension)) {
84
return true;
85
}
86
return super.isExtensionPlatformCompatible(extension);
87
}
88
89
async getInstalled(type?: ExtensionType, profileLocation?: URI): Promise<ILocalExtension[]> {
90
const extensions = [];
91
if (type === undefined || type === ExtensionType.System) {
92
const systemExtensions = await this.webExtensionsScannerService.scanSystemExtensions();
93
extensions.push(...systemExtensions);
94
}
95
if (type === undefined || type === ExtensionType.User) {
96
const userExtensions = await this.webExtensionsScannerService.scanUserExtensions(profileLocation ?? this.userDataProfileService.currentProfile.extensionsResource);
97
extensions.push(...userExtensions);
98
}
99
return extensions.map(e => toLocalExtension(e));
100
}
101
102
async install(location: URI, options: InstallOptions = {}): Promise<ILocalExtension> {
103
this.logService.trace('ExtensionManagementService#install', location.toString());
104
const manifest = await this.webExtensionsScannerService.scanExtensionManifest(location);
105
if (!manifest || !manifest.name || !manifest.version) {
106
throw new Error(`Cannot find a valid extension from the location ${location.toString()}`);
107
}
108
const result = await this.installExtensions([{ manifest, extension: location, options }]);
109
if (result[0]?.local) {
110
return result[0]?.local;
111
}
112
if (result[0]?.error) {
113
throw result[0].error;
114
}
115
throw toExtensionManagementError(new Error(`Unknown error while installing extension ${getGalleryExtensionId(manifest.publisher, manifest.name)}`));
116
}
117
118
installFromLocation(location: URI, profileLocation: URI): Promise<ILocalExtension> {
119
return this.install(location, { profileLocation });
120
}
121
122
protected async deleteExtension(extension: ILocalExtension): Promise<void> {
123
// do nothing
124
}
125
126
protected async copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial<Metadata>): Promise<ILocalExtension> {
127
const target = await this.webExtensionsScannerService.scanExistingExtension(extension.location, extension.type, toProfileLocation);
128
const source = await this.webExtensionsScannerService.scanExistingExtension(extension.location, extension.type, fromProfileLocation);
129
metadata = { ...source?.metadata, ...metadata };
130
131
let scanned;
132
if (target) {
133
scanned = await this.webExtensionsScannerService.updateMetadata(extension, { ...target.metadata, ...metadata }, toProfileLocation);
134
} else {
135
scanned = await this.webExtensionsScannerService.addExtension(extension.location, metadata, toProfileLocation);
136
}
137
return toLocalExtension(scanned);
138
}
139
140
protected async moveExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial<Metadata>): Promise<ILocalExtension> {
141
const target = await this.webExtensionsScannerService.scanExistingExtension(extension.location, extension.type, toProfileLocation);
142
const source = await this.webExtensionsScannerService.scanExistingExtension(extension.location, extension.type, fromProfileLocation);
143
metadata = { ...source?.metadata, ...metadata };
144
145
let scanned;
146
if (target) {
147
scanned = await this.webExtensionsScannerService.updateMetadata(extension, { ...target.metadata, ...metadata }, toProfileLocation);
148
} else {
149
scanned = await this.webExtensionsScannerService.addExtension(extension.location, metadata, toProfileLocation);
150
if (source) {
151
await this.webExtensionsScannerService.removeExtension(source, fromProfileLocation);
152
}
153
}
154
return toLocalExtension(scanned);
155
}
156
157
protected async removeExtension(extension: ILocalExtension, fromProfileLocation: URI): Promise<void> {
158
const source = await this.webExtensionsScannerService.scanExistingExtension(extension.location, extension.type, fromProfileLocation);
159
if (source) {
160
await this.webExtensionsScannerService.removeExtension(source, fromProfileLocation);
161
}
162
}
163
164
async installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise<ILocalExtension[]> {
165
const result: ILocalExtension[] = [];
166
const extensionsToInstall = (await this.webExtensionsScannerService.scanUserExtensions(fromProfileLocation))
167
.filter(e => extensions.some(id => areSameExtensions(id, e.identifier)));
168
if (extensionsToInstall.length) {
169
await Promise.allSettled(extensionsToInstall.map(async e => {
170
let local = await this.installFromLocation(e.location, toProfileLocation);
171
if (e.metadata) {
172
local = await this.updateMetadata(local, e.metadata, fromProfileLocation);
173
}
174
result.push(local);
175
}));
176
}
177
return result;
178
}
179
180
async updateMetadata(local: ILocalExtension, metadata: Partial<Metadata>, profileLocation: URI): Promise<ILocalExtension> {
181
// unset if false
182
if (metadata.isMachineScoped === false) {
183
metadata.isMachineScoped = undefined;
184
}
185
if (metadata.isBuiltin === false) {
186
metadata.isBuiltin = undefined;
187
}
188
if (metadata.pinned === false) {
189
metadata.pinned = undefined;
190
}
191
const updatedExtension = await this.webExtensionsScannerService.updateMetadata(local, metadata, profileLocation);
192
const updatedLocalExtension = toLocalExtension(updatedExtension);
193
this._onDidUpdateExtensionMetadata.fire({ local: updatedLocalExtension, profileLocation });
194
return updatedLocalExtension;
195
}
196
197
override async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise<void> {
198
await this.webExtensionsScannerService.copyExtensions(fromProfileLocation, toProfileLocation, e => !e.metadata?.isApplicationScoped);
199
}
200
201
protected override async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean, productVersion: IProductVersion): Promise<IGalleryExtension | null> {
202
const compatibleExtension = await super.getCompatibleVersion(extension, sameVersion, includePreRelease, productVersion);
203
if (compatibleExtension) {
204
return compatibleExtension;
205
}
206
if (this.isConfiguredToExecuteOnWeb(extension)) {
207
return extension;
208
}
209
return null;
210
}
211
212
private isConfiguredToExecuteOnWeb(gallery: IGalleryExtension): boolean {
213
const configuredExtensionKind = this.extensionManifestPropertiesService.getUserConfiguredExtensionKind(gallery.identifier);
214
return !!configuredExtensionKind && configuredExtensionKind.includes('web');
215
}
216
217
protected getCurrentExtensionsManifestLocation(): URI {
218
return this.userDataProfileService.currentProfile.extensionsResource;
219
}
220
221
protected createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask {
222
return new InstallExtensionTask(manifest, extension, options, this.webExtensionsScannerService, this.userDataProfilesService);
223
}
224
225
protected createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask {
226
return new UninstallExtensionTask(extension, options, this.webExtensionsScannerService);
227
}
228
229
zip(extension: ILocalExtension): Promise<URI> { throw new Error('unsupported'); }
230
getManifest(vsix: URI): Promise<IExtensionManifest> { throw new Error('unsupported'); }
231
download(): Promise<URI> { throw new Error('unsupported'); }
232
233
async cleanUp(): Promise<void> { }
234
235
private async whenProfileChanged(e: DidChangeUserDataProfileEvent): Promise<void> {
236
const previousProfileLocation = e.previous.extensionsResource;
237
const currentProfileLocation = e.profile.extensionsResource;
238
if (!previousProfileLocation || !currentProfileLocation) {
239
throw new Error('This should not happen');
240
}
241
const oldExtensions = await this.webExtensionsScannerService.scanUserExtensions(previousProfileLocation);
242
const newExtensions = await this.webExtensionsScannerService.scanUserExtensions(currentProfileLocation);
243
const { added, removed } = delta(oldExtensions, newExtensions, (a, b) => compare(`${ExtensionIdentifier.toKey(a.identifier.id)}@${a.manifest.version}`, `${ExtensionIdentifier.toKey(b.identifier.id)}@${b.manifest.version}`));
244
this._onDidChangeProfile.fire({ added: added.map(e => toLocalExtension(e)), removed: removed.map(e => toLocalExtension(e)) });
245
}
246
}
247
248
function toLocalExtension(extension: IExtension): ILocalExtension {
249
const metadata = getMetadata(undefined, extension);
250
return {
251
...extension,
252
identifier: { id: extension.identifier.id, uuid: metadata.id ?? extension.identifier.uuid },
253
isMachineScoped: !!metadata.isMachineScoped,
254
isApplicationScoped: !!metadata.isApplicationScoped,
255
publisherId: metadata.publisherId || null,
256
publisherDisplayName: metadata.publisherDisplayName,
257
installedTimestamp: metadata.installedTimestamp,
258
isPreReleaseVersion: !!metadata.isPreReleaseVersion,
259
hasPreReleaseVersion: !!metadata.hasPreReleaseVersion,
260
preRelease: extension.preRelease,
261
targetPlatform: TargetPlatform.WEB,
262
updated: !!metadata.updated,
263
pinned: !!metadata?.pinned,
264
private: !!metadata.private,
265
isWorkspaceScoped: false,
266
source: metadata?.source ?? (extension.identifier.uuid ? 'gallery' : 'resource'),
267
size: metadata.size ?? 0,
268
};
269
}
270
271
function getMetadata(options?: InstallOptions, existingExtension?: IExtension): Metadata {
272
const metadata: Metadata = { ...((<IScannedExtension>existingExtension)?.metadata || {}) };
273
metadata.isMachineScoped = options?.isMachineScoped || metadata.isMachineScoped;
274
return metadata;
275
}
276
277
class InstallExtensionTask extends AbstractExtensionTask<ILocalExtension> implements IInstallExtensionTask {
278
279
readonly identifier: IExtensionIdentifier;
280
readonly source: URI | IGalleryExtension;
281
282
private _profileLocation: URI;
283
get profileLocation() { return this._profileLocation; }
284
285
private _operation = InstallOperation.Install;
286
get operation() { return isUndefined(this.options.operation) ? this._operation : this.options.operation; }
287
288
constructor(
289
readonly manifest: IExtensionManifest,
290
private readonly extension: URI | IGalleryExtension,
291
readonly options: InstallExtensionTaskOptions,
292
private readonly webExtensionsScannerService: IWebExtensionsScannerService,
293
private readonly userDataProfilesService: IUserDataProfilesService,
294
) {
295
super();
296
this._profileLocation = options.profileLocation;
297
this.identifier = URI.isUri(extension) ? { id: getGalleryExtensionId(manifest.publisher, manifest.name) } : extension.identifier;
298
this.source = extension;
299
}
300
301
protected async doRun(token: CancellationToken): Promise<ILocalExtension> {
302
const userExtensions = await this.webExtensionsScannerService.scanUserExtensions(this.options.profileLocation);
303
const existingExtension = userExtensions.find(e => areSameExtensions(e.identifier, this.identifier));
304
if (existingExtension) {
305
this._operation = InstallOperation.Update;
306
}
307
308
const metadata = getMetadata(this.options, existingExtension);
309
if (!URI.isUri(this.extension)) {
310
metadata.id = this.extension.identifier.uuid;
311
metadata.publisherDisplayName = this.extension.publisherDisplayName;
312
metadata.publisherId = this.extension.publisherId;
313
metadata.installedTimestamp = Date.now();
314
metadata.isPreReleaseVersion = this.extension.properties.isPreReleaseVersion;
315
metadata.hasPreReleaseVersion = metadata.hasPreReleaseVersion || this.extension.properties.isPreReleaseVersion;
316
metadata.isBuiltin = this.options.isBuiltin || existingExtension?.isBuiltin;
317
metadata.isSystem = existingExtension?.type === ExtensionType.System ? true : undefined;
318
metadata.updated = !!existingExtension;
319
metadata.isApplicationScoped = this.options.isApplicationScoped || metadata.isApplicationScoped;
320
metadata.private = this.extension.private;
321
metadata.preRelease = isBoolean(this.options.preRelease)
322
? this.options.preRelease
323
: this.options.installPreReleaseVersion || this.extension.properties.isPreReleaseVersion || metadata.preRelease;
324
metadata.source = URI.isUri(this.extension) ? 'resource' : 'gallery';
325
}
326
metadata.pinned = this.options.installGivenVersion ? true : (this.options.pinned ?? metadata.pinned);
327
328
this._profileLocation = metadata.isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : this.options.profileLocation;
329
const scannedExtension = URI.isUri(this.extension) ? await this.webExtensionsScannerService.addExtension(this.extension, metadata, this.profileLocation)
330
: await this.webExtensionsScannerService.addExtensionFromGallery(this.extension, metadata, this.profileLocation);
331
return toLocalExtension(scannedExtension);
332
}
333
}
334
335
class UninstallExtensionTask extends AbstractExtensionTask<void> implements IUninstallExtensionTask {
336
337
constructor(
338
readonly extension: ILocalExtension,
339
readonly options: UninstallExtensionTaskOptions,
340
private readonly webExtensionsScannerService: IWebExtensionsScannerService,
341
) {
342
super();
343
}
344
345
protected doRun(token: CancellationToken): Promise<void> {
346
return this.webExtensionsScannerService.removeExtension(this.extension, this.options.profileLocation);
347
}
348
}
349
350