Skip to content

Commit 2423a7a

Browse files
authored
support updating installed extensions (microsoft#165485)
* support updating installed extensions when installed externally (cli, manual) * 💄 * refactor
1 parent f60ff40 commit 2423a7a

File tree

5 files changed

+277
-142
lines changed

5 files changed

+277
-142
lines changed

src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
503503
}
504504
}
505505
reportTelemetry(this.telemetryService, 'extensionGallery:uninstall', { extensionData: getLocalExtensionTelemetryData(extension), error });
506-
this._onDidUninstallExtension.fire({ identifier: extension.identifier, version: extension.manifest.version, error: error?.code, profileLocation: uninstallOptions.profileLocation, applicationScoped: extension.isApplicationScoped });
506+
this._onDidUninstallExtension.fire({ identifier: extension.identifier, error: error?.code, profileLocation: uninstallOptions.profileLocation, applicationScoped: extension.isApplicationScoped });
507507
};
508508

509509
const allTasks: IUninstallExtensionTask[] = [];

src/vs/platform/extensionManagement/common/extensionManagement.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,6 @@ export interface UninstallExtensionEvent {
372372

373373
export interface DidUninstallExtensionEvent {
374374
readonly identifier: IExtensionIdentifier;
375-
readonly version?: string;
376375
readonly error?: string;
377376
readonly profileLocation?: URI;
378377
readonly applicationScoped?: boolean;

src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { Queue } from 'vs/base/common/async';
77
import { VSBuffer } from 'vs/base/common/buffer';
88
import { Disposable } from 'vs/base/common/lifecycle';
9+
import { Emitter, Event } from 'vs/base/common/event';
910
import { ResourceMap } from 'vs/base/common/map';
1011
import { URI, UriComponents } from 'vs/base/common/uri';
1112
import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement';
@@ -29,18 +30,48 @@ export interface IScannedProfileExtension {
2930
readonly metadata?: Metadata;
3031
}
3132

33+
export interface ProfileExtensionsEvent {
34+
readonly extensions: readonly IExtension[];
35+
readonly profileLocation: URI;
36+
}
37+
38+
export interface DidAddProfileExtensionsEvent extends ProfileExtensionsEvent {
39+
readonly error?: Error;
40+
}
41+
42+
export interface DidRemoveProfileExtensionsEvent extends ProfileExtensionsEvent {
43+
readonly error?: Error;
44+
}
45+
3246
export const IExtensionsProfileScannerService = createDecorator<IExtensionsProfileScannerService>('IExtensionsProfileScannerService');
3347
export interface IExtensionsProfileScannerService {
3448
readonly _serviceBrand: undefined;
3549

50+
readonly onAddExtensions: Event<ProfileExtensionsEvent>;
51+
readonly onDidAddExtensions: Event<DidAddProfileExtensionsEvent>;
52+
readonly onRemoveExtensions: Event<ProfileExtensionsEvent>;
53+
readonly onDidRemoveExtensions: Event<DidRemoveProfileExtensionsEvent>;
54+
3655
scanProfileExtensions(profileLocation: URI): Promise<IScannedProfileExtension[]>;
3756
addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise<IScannedProfileExtension[]>;
38-
removeExtensionFromProfile(identifier: IExtensionIdentifier, profileLocation: URI): Promise<IScannedProfileExtension[]>;
57+
removeExtensionFromProfile(extension: IExtension, profileLocation: URI): Promise<void>;
3958
}
4059

4160
export class ExtensionsProfileScannerService extends Disposable implements IExtensionsProfileScannerService {
4261
readonly _serviceBrand: undefined;
4362

63+
private readonly _onAddExtensions = this._register(new Emitter<ProfileExtensionsEvent>());
64+
readonly onAddExtensions = this._onAddExtensions.event;
65+
66+
private readonly _onDidAddExtensions = this._register(new Emitter<DidAddProfileExtensionsEvent>());
67+
readonly onDidAddExtensions = this._onDidAddExtensions.event;
68+
69+
private readonly _onRemoveExtensions = this._register(new Emitter<ProfileExtensionsEvent>());
70+
readonly onRemoveExtensions = this._onRemoveExtensions.event;
71+
72+
private readonly _onDidRemoveExtensions = this._register(new Emitter<DidRemoveProfileExtensionsEvent>());
73+
readonly onDidRemoveExtensions = this._onDidRemoveExtensions.event;
74+
4475
private readonly resourcesAccessQueueMap = new ResourceMap<Queue<IScannedProfileExtension[]>>();
4576

4677
constructor(
@@ -54,17 +85,33 @@ export class ExtensionsProfileScannerService extends Disposable implements IExte
5485
return this.withProfileExtensions(profileLocation);
5586
}
5687

57-
addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise<IScannedProfileExtension[]> {
58-
return this.withProfileExtensions(profileLocation, profileExtensions => {
59-
// Remove the existing extension to avoid duplicates
60-
profileExtensions = profileExtensions.filter(e => extensions.some(([extension]) => !areSameExtensions(e.identifier, extension.identifier)));
61-
profileExtensions.push(...extensions.map(([extension, metadata]) => ({ identifier: extension.identifier, version: extension.manifest.version, location: extension.location, metadata })));
62-
return profileExtensions;
63-
});
88+
async addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise<IScannedProfileExtension[]> {
89+
this._onAddExtensions.fire({ extensions: extensions.map(e => e[0]), profileLocation });
90+
try {
91+
const allExtensions = await this.withProfileExtensions(profileLocation, profileExtensions => {
92+
// Remove the existing extension to avoid duplicates
93+
profileExtensions = profileExtensions.filter(e => extensions.some(([extension]) => !areSameExtensions(e.identifier, extension.identifier)));
94+
profileExtensions.push(...extensions.map(([extension, metadata]) => ({ identifier: extension.identifier, version: extension.manifest.version, location: extension.location, metadata })));
95+
return profileExtensions;
96+
});
97+
const addedExtensions = allExtensions.filter(e => extensions.some(([extension]) => areSameExtensions(e.identifier, extension.identifier)));
98+
this._onDidAddExtensions.fire({ extensions: extensions.map(e => e[0]), profileLocation });
99+
return addedExtensions;
100+
} catch (error) {
101+
this._onDidAddExtensions.fire({ extensions: extensions.map(e => e[0]), error, profileLocation });
102+
throw error;
103+
}
64104
}
65105

66-
removeExtensionFromProfile(identifier: IExtensionIdentifier, profileLocation: URI): Promise<IScannedProfileExtension[]> {
67-
return this.withProfileExtensions(profileLocation, profileExtensions => profileExtensions.filter(extension => !(areSameExtensions(extension.identifier, identifier))));
106+
async removeExtensionFromProfile(extension: IExtension, profileLocation: URI): Promise<void> {
107+
this._onRemoveExtensions.fire({ extensions: [extension], profileLocation });
108+
try {
109+
await this.withProfileExtensions(profileLocation, profileExtensions => profileExtensions.filter(e => !(areSameExtensions(e.identifier, extension.identifier))));
110+
this._onDidRemoveExtensions.fire({ extensions: [extension], profileLocation });
111+
} catch (error) {
112+
this._onDidRemoveExtensions.fire({ extensions: [extension], error, profileLocation });
113+
throw error;
114+
}
68115
}
69116

70117
private async withProfileExtensions(file: URI, updateFn?: (extensions: IScannedProfileExtension[]) => IScannedProfileExtension[]): Promise<IScannedProfileExtension[]> {

src/vs/platform/extensionManagement/node/extensionManagementService.ts

Lines changed: 98 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import { CancellationToken } from 'vs/base/common/cancellation';
99
import { IStringDictionary } from 'vs/base/common/collections';
1010
import { toErrorMessage } from 'vs/base/common/errorMessage';
1111
import { getErrorMessage } from 'vs/base/common/errors';
12+
import { Emitter } from 'vs/base/common/event';
1213
import { Disposable } from 'vs/base/common/lifecycle';
14+
import { ResourceSet } from 'vs/base/common/map';
1315
import { Schemas } from 'vs/base/common/network';
1416
import * as path from 'vs/base/common/path';
1517
import { isMacintosh, isWindows } from 'vs/base/common/platform';
@@ -35,10 +37,10 @@ import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/exten
3537
import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle';
3638
import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
3739
import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache';
38-
import { ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher';
39-
import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions';
40+
import { DidChangeProfileExtensionsEvent, ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher';
41+
import { ExtensionType, IExtension, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions';
4042
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
41-
import { IFileService } from 'vs/platform/files/common/files';
43+
import { FileChangesEvent, IFileService } from 'vs/platform/files/common/files';
4244
import { IInstantiationService, refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation';
4345
import { ILogService } from 'vs/platform/log/common/log';
4446
import { IProductService } from 'vs/platform/product/common/productService';
@@ -56,7 +58,7 @@ export const INativeServerExtensionManagementService = refineServiceDecorator<IE
5658
export interface INativeServerExtensionManagementService extends IExtensionManagementService {
5759
readonly _serviceBrand: undefined;
5860
migrateDefaultProfileExtensions(): Promise<void>;
59-
markAsUninstalled(...extensions: ILocalExtension[]): Promise<void>;
61+
markAsUninstalled(...extensions: IExtension[]): Promise<void>;
6062
removeUninstalledExtensions(): Promise<void>;
6163
getAllUserInstalled(): Promise<ILocalExtension[]>;
6264
}
@@ -80,22 +82,18 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
8082
@IInstantiationService instantiationService: IInstantiationService,
8183
@IFileService private readonly fileService: IFileService,
8284
@IProductService productService: IProductService,
83-
@IUriIdentityService uriIdentityService: IUriIdentityService,
85+
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
8486
@IUserDataProfilesService userDataProfilesService: IUserDataProfilesService
8587
) {
8688
super(galleryService, telemetryService, logService, productService, userDataProfilesService);
8789
const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle));
8890
this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension)));
8991
this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this));
9092
this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader));
91-
const extensionsWatcher = this._register(new ExtensionsWatcher(this, userDataProfilesService, extensionsProfileScannerService, extensionsScannerService, uriIdentityService, fileService, logService));
9293

93-
this._register(extensionsWatcher.onDidChangeExtensionsByAnotherSource(({ added, removed }) => {
94-
if (added.length) {
95-
this._onDidInstallExtensions.fire(added);
96-
}
97-
removed.forEach(e => this._onDidUninstallExtension.fire(e));
98-
}));
94+
const extensionsWatcher = this._register(new ExtensionsWatcher(this, userDataProfilesService, extensionsProfileScannerService, uriIdentityService, fileService, logService));
95+
this._register(extensionsWatcher.onDidChangeExtensionsByAnotherSource(e => this.onDidChangeExtensionsFromAnotherSource(e)));
96+
this.watchForExtensionsNotInstalledBySystem();
9997
}
10098

10199
private _targetPlatformPromise: Promise<TargetPlatform> | undefined;
@@ -197,7 +195,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
197195
await this.installFromGallery(galleryExtension);
198196
}
199197

200-
markAsUninstalled(...extensions: ILocalExtension[]): Promise<void> {
198+
markAsUninstalled(...extensions: IExtension[]): Promise<void> {
201199
return this.extensionsScanner.setUninstalled(...extensions);
202200
}
203201

@@ -276,13 +274,85 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
276274
return files.map(f => (<IFile>{ path: `extension/${path.relative(extension.location.fsPath, f)}`, localPath: f }));
277275
}
278276

277+
private async onDidChangeExtensionsFromAnotherSource({ added, removed }: DidChangeProfileExtensionsEvent): Promise<void> {
278+
if (removed) {
279+
for (const identifier of removed.extensions) {
280+
this._onDidUninstallExtension.fire({ identifier, profileLocation: removed.profileLocation });
281+
}
282+
}
283+
if (added) {
284+
const extensions = await this.extensionsScanner.scanExtensions(ExtensionType.User, added.profileLocation);
285+
const addedExtensions = extensions.filter(e => added.extensions.some(identifier => areSameExtensions(identifier, e.identifier)));
286+
this._onDidInstallExtensions.fire(addedExtensions.map(local => ({ identifier: local.identifier, local, profileLocation: added.profileLocation, operation: InstallOperation.None })));
287+
}
288+
}
289+
290+
private readonly knownDirectories = new ResourceSet();
291+
private async watchForExtensionsNotInstalledBySystem(): Promise<void> {
292+
this._register(this.extensionsScanner.onExtract(resource => this.knownDirectories.add(resource)));
293+
const stat = await this.fileService.resolve(this.extensionsScannerService.userExtensionsLocation);
294+
for (const childStat of stat.children ?? []) {
295+
if (childStat.isDirectory) {
296+
this.knownDirectories.add(childStat.resource);
297+
}
298+
}
299+
this._register(this.fileService.watch(this.extensionsScannerService.userExtensionsLocation));
300+
this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e)));
301+
}
302+
303+
private async onDidFilesChange(e: FileChangesEvent): Promise<void> {
304+
const added: ILocalExtension[] = [];
305+
for (const resource of e.rawAdded) {
306+
// Check if this is a known directory
307+
if (this.knownDirectories.has(resource)) {
308+
continue;
309+
}
310+
311+
// Is not immediate child of extensions resource
312+
if (!this.uriIdentityService.extUri.isEqual(this.uriIdentityService.extUri.dirname(resource), this.extensionsScannerService.userExtensionsLocation)) {
313+
continue;
314+
}
315+
316+
// .obsolete file changed
317+
if (this.uriIdentityService.extUri.isEqual(resource, this.uriIdentityService.extUri.joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete'))) {
318+
continue;
319+
}
320+
321+
// Ignore changes to files starting with `.`
322+
if (this.uriIdentityService.extUri.basename(resource).startsWith('.')) {
323+
continue;
324+
}
325+
326+
// Check if this is a directory
327+
if (!(await this.fileService.stat(resource)).isDirectory) {
328+
continue;
329+
}
330+
331+
// Check if this is an extension added by another source
332+
// Extension added by another source will not have installed timestamp
333+
const extension = await this.extensionsScanner.scanUserExtensionAtLocation(resource);
334+
if (extension && extension.installedTimestamp === undefined) {
335+
this.knownDirectories.add(resource);
336+
added.push(extension);
337+
}
338+
}
339+
340+
if (added.length) {
341+
await this.extensionsProfileScannerService.addExtensionsToProfile(added.map(local => ([local, undefined])), this.userDataProfilesService.defaultProfile.extensionsResource);
342+
this._onDidInstallExtensions.fire(added.map(local => ({ local, version: local.manifest.version, identifier: local.identifier, operation: InstallOperation.None, profileLocation: this.userDataProfilesService.defaultProfile.extensionsResource })));
343+
}
344+
}
345+
279346
}
280347

281348
export class ExtensionsScanner extends Disposable {
282349

283350
private readonly uninstalledPath: string;
284351
private readonly uninstalledFileLimiter: Queue<any>;
285352

353+
private readonly _onExtract = this._register(new Emitter<URI>());
354+
readonly onExtract = this._onExtract.event;
355+
286356
constructor(
287357
private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise<void>,
288358
@IFileService private readonly fileService: IFileService,
@@ -318,6 +388,18 @@ export class ExtensionsScanner extends Disposable {
318388
return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension)));
319389
}
320390

391+
async scanUserExtensionAtLocation(location: URI): Promise<ILocalExtension | null> {
392+
try {
393+
const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, ExtensionType.User, { includeInvalid: true });
394+
if (scannedExtension) {
395+
return await this.toLocalExtension(scannedExtension);
396+
}
397+
} catch (error) {
398+
this.logService.error(error);
399+
}
400+
return null;
401+
}
402+
321403
async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, metadata: Metadata, token: CancellationToken): Promise<ILocalExtension> {
322404
await this.migrateDefaultProfileExtensions();
323405
const folderName = extensionKey.toString();
@@ -334,6 +416,7 @@ export class ExtensionsScanner extends Disposable {
334416
await this.extensionsScannerService.updateMetadata(URI.file(tempPath), metadata);
335417

336418
try {
419+
this._onExtract.fire(URI.file(extensionPath));
337420
await this.rename(extensionKey, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
338421
this.logService.info('Renamed to', extensionPath);
339422
} catch (error) {
@@ -360,7 +443,7 @@ export class ExtensionsScanner extends Disposable {
360443
return this.withUninstalledExtensions();
361444
}
362445

363-
async setUninstalled(...extensions: ILocalExtension[]): Promise<void> {
446+
async setUninstalled(...extensions: IExtension[]): Promise<void> {
364447
const extensionKeys: ExtensionKey[] = extensions.map(e => ExtensionKey.create(e));
365448
await this.withUninstalledExtensions(uninstalled =>
366449
extensionKeys.forEach(extensionKey => {
@@ -813,7 +896,7 @@ class UninstallExtensionFromProfileTask extends AbstractExtensionTask<void> impl
813896
}
814897

815898
protected async doRun(token: CancellationToken): Promise<void> {
816-
await this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension.identifier, this.profileLocation);
899+
await this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.profileLocation);
817900
}
818901

819902
}

0 commit comments

Comments
 (0)