Skip to content

Commit 1e5e1a0

Browse files
authored
support application scoped extensions (#152975)
* support application scoped extensions * fix tests
1 parent 619ab6d commit 1e5e1a0

File tree

13 files changed

+160
-45
lines changed

13 files changed

+160
-45
lines changed

src/vs/code/electron-main/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,7 @@ export class CodeApplication extends Disposable {
710710
mainProcessElectronServer.registerChannel(LOCAL_FILE_SYSTEM_CHANNEL_NAME, fileSystemProviderChannel);
711711
sharedProcessClient.then(client => client.registerChannel(LOCAL_FILE_SYSTEM_CHANNEL_NAME, fileSystemProviderChannel));
712712

713-
// Profiles
713+
// User Data Profiles
714714
const userDataProfilesService = ProxyChannel.fromService(accessor.get(IUserDataProfilesMainService));
715715
mainProcessElectronServer.registerChannel('userDataProfiles', userDataProfilesService);
716716
sharedProcessClient.then(client => client.registerChannel('userDataProfiles', userDataProfilesService));

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

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ import {
1818
} from 'vs/platform/extensionManagement/common/extensionManagement';
1919
import { areSameExtensions, ExtensionKey, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
2020
import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
21-
import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
21+
import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions';
2222
import { ILogService } from 'vs/platform/log/common/log';
2323
import { IProductService } from 'vs/platform/product/common/productService';
2424
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
25+
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
26+
import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
2527

2628
export interface IInstallExtensionTask {
2729
readonly identifier: IExtensionIdentifier;
@@ -65,6 +67,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
6567
private readonly participants: IExtensionManagementParticipant[] = [];
6668

6769
constructor(
70+
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
71+
@IUriIdentityService private readonly uriIdenityService: IUriIdentityService,
6872
@IExtensionGalleryService protected readonly galleryService: IExtensionGalleryService,
6973
@IExtensionsProfileScannerService protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService,
7074
@ITelemetryService protected readonly telemetryService: ITelemetryService,
@@ -142,18 +146,16 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
142146
const installExtensionTask = this.installingExtensions.get(ExtensionKey.create(extension).toString());
143147
if (installExtensionTask) {
144148
this.logService.info('Extensions is already requested to install', extension.identifier.id);
145-
const { local, metadata } = await installExtensionTask.waitUntilTaskIsFinished();
146-
if (options.profileLocation) {
147-
await this.extensionsProfileScannerService.addExtensionsToProfile([[local, metadata]], options.profileLocation);
148-
}
149+
const waitUntilTaskIsFinishedTask = this.createWaitUntilInstallExtensionTaskIsFinishedTask(installExtensionTask, options);
150+
const { local } = await waitUntilTaskIsFinishedTask.waitUntilTaskIsFinished();
149151
return local;
150152
}
151153
options = { ...options, installOnlyNewlyAddedFromExtensionPack: true /* always true for gallery extensions */ };
152154
}
153155

154156
const allInstallExtensionTasks: { task: IInstallExtensionTask; manifest: IExtensionManifest }[] = [];
155157
const installResults: (InstallExtensionResult & { local: ILocalExtension })[] = [];
156-
const installExtensionTask = this.createDefaultInstallExtensionTask(manifest, extension, options);
158+
const installExtensionTask = this.createInstallExtensionTask(manifest, extension, options);
157159
if (!URI.isUri(extension)) {
158160
this.installingExtensions.set(ExtensionKey.create(extension).toString(), installExtensionTask);
159161
}
@@ -174,7 +176,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
174176
if (this.installingExtensions.has(key)) {
175177
this.logService.info('Extension is already requested to install', gallery.identifier.id);
176178
} else {
177-
const task = this.createDefaultInstallExtensionTask(manifest, gallery, { ...options, donotIncludePackAndDependencies: true });
179+
const task = this.createInstallExtensionTask(manifest, gallery, { ...options, donotIncludePackAndDependencies: true });
178180
this.installingExtensions.set(key, task);
179181
this._onInstallExtension.fire({ identifier: task.identifier, source: gallery });
180182
this.logService.info('Installing extension:', task.identifier.id);
@@ -219,10 +221,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
219221
await this.joinAllSettled(extensionsToInstall.map(async ({ task }) => {
220222
const startTime = new Date().getTime();
221223
try {
222-
const { local, metadata } = await task.run();
223-
if (options.profileLocation) {
224-
await this.extensionsProfileScannerService.addExtensionsToProfile([[local, metadata]], options.profileLocation);
225-
}
224+
const { local } = await task.run();
226225
await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, options, CancellationToken.None)));
227226
if (!URI.isUri(task.source)) {
228227
const isUpdate = task.operation === InstallOperation.Update;
@@ -593,8 +592,23 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
593592
}
594593
}
595594

595+
private createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: ServerInstallOptions & ServerInstallVSIXOptions): IInstallExtensionTask {
596+
const installTask = this.createDefaultInstallExtensionTask(manifest, extension, options);
597+
return options.profileLocation && this.userDataProfilesService.defaultProfile.extensionsResource ? new InstallExtensionInProfileTask(installTask, options.profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource, this.extensionsProfileScannerService) : installTask;
598+
}
599+
600+
private createWaitUntilInstallExtensionTaskIsFinishedTask(installTask: IInstallExtensionTask, options: ServerInstallOptions & ServerInstallVSIXOptions): IInstallExtensionTask {
601+
if (!options.profileLocation || !this.userDataProfilesService.defaultProfile.extensionsResource) {
602+
return installTask;
603+
}
604+
if (installTask instanceof InstallExtensionInProfileTask && this.uriIdenityService.extUri.isEqual(installTask.profileLocation, options.profileLocation)) {
605+
return installTask;
606+
}
607+
return new InstallExtensionInProfileTask(installTask, options.profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource, this.extensionsProfileScannerService);
608+
}
609+
596610
private createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions, profile?: URI): IUninstallExtensionTask {
597-
return profile ? new UninstallExtensionFromProfileTask(extension, profile, this.extensionsProfileScannerService) : this.createDefaultUninstallExtensionTask(extension, options);
611+
return profile && this.userDataProfilesService.defaultProfile.extensionsResource ? new UninstallExtensionFromProfileTask(extension, profile, this.userDataProfilesService, this.extensionsProfileScannerService) : this.createDefaultUninstallExtensionTask(extension, options);
598612
}
599613

600614
abstract getTargetPlatform(): Promise<TargetPlatform>;
@@ -707,18 +721,66 @@ export abstract class AbstractExtensionTask<T> {
707721
protected abstract doRun(token: CancellationToken): Promise<T>;
708722
}
709723

710-
export class UninstallExtensionFromProfileTask extends AbstractExtensionTask<void> implements IUninstallExtensionTask {
724+
class InstallExtensionInProfileTask implements IInstallExtensionTask {
725+
726+
readonly identifier = this.task.identifier;
727+
readonly source = this.task.source;
728+
readonly operation = this.task.operation;
729+
730+
private readonly promise: Promise<{ local: ILocalExtension; metadata: Metadata }>;
731+
732+
constructor(
733+
private readonly task: IInstallExtensionTask,
734+
readonly profileLocation: URI,
735+
private readonly defaultProfileLocation: URI,
736+
private readonly extensionsProfileScannerService: IExtensionsProfileScannerService,
737+
) {
738+
this.promise = this.waitAndAddExtensionToProfile();
739+
}
740+
741+
private async waitAndAddExtensionToProfile(): Promise<{ local: ILocalExtension; metadata: Metadata }> {
742+
const result = await this.task.waitUntilTaskIsFinished();
743+
let profileLocation = this.profileLocation;
744+
if (isApplicationScopedExtension(result.local.manifest)) {
745+
profileLocation = this.defaultProfileLocation;
746+
result.metadata = { ...result.metadata, isApplicationScoped: true };
747+
}
748+
await this.extensionsProfileScannerService.addExtensionsToProfile([[result.local, result.metadata]], profileLocation);
749+
return result;
750+
}
751+
752+
async run(): Promise<{ local: ILocalExtension; metadata: Metadata }> {
753+
await this.task.run();
754+
return this.promise;
755+
}
756+
757+
waitUntilTaskIsFinished(): Promise<{ local: ILocalExtension; metadata: Metadata }> {
758+
return this.promise;
759+
}
760+
761+
cancel(): void {
762+
return this.task.cancel();
763+
}
764+
}
765+
766+
class UninstallExtensionFromProfileTask extends AbstractExtensionTask<void> implements IUninstallExtensionTask {
711767

712768
constructor(
713769
readonly extension: ILocalExtension,
714770
private readonly profileLocation: URI,
771+
private readonly userDataProfilesService: IUserDataProfilesService,
715772
private readonly extensionsProfileScannerService: IExtensionsProfileScannerService,
716773
) {
717774
super();
718775
}
719776

720777
protected async doRun(token: CancellationToken): Promise<void> {
721-
await this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension.identifier, this.profileLocation);
778+
const promises: Promise<any>[] = [];
779+
promises.push(this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension.identifier, this.profileLocation));
780+
if (isApplicationScopedExtension(this.extension.manifest) && this.userDataProfilesService.defaultProfile.extensionsResource) {
781+
promises.push(this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension.identifier, this.userDataProfilesService.defaultProfile.extensionsResource));
782+
}
783+
await Promise.all(promises);
722784
}
723785

724786
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export interface IGalleryMetadata {
238238
targetPlatform?: TargetPlatform;
239239
}
240240

241-
export type Metadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean; isSystem: boolean; updated: boolean; preRelease: boolean; installedTimestamp: number }>;
241+
export type Metadata = Partial<IGalleryMetadata & { isApplicationScoped: boolean; isMachineScoped: boolean; isBuiltin: boolean; isSystem: boolean; updated: boolean; preRelease: boolean; installedTimestamp: number }>;
242242

243243
export interface ILocalExtension extends IExtension {
244244
isMachineScoped: boolean;

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ import { ILogService } from 'vs/platform/log/common/log';
1818
interface IStoredProfileExtension {
1919
readonly identifier: IExtensionIdentifier;
2020
readonly location: UriComponents;
21-
readonly metadata: Metadata;
21+
readonly metadata?: Metadata;
2222
}
2323

2424
export interface IScannedProfileExtension {
2525
readonly identifier: IExtensionIdentifier;
2626
readonly location: URI;
27-
readonly metadata: Metadata;
27+
readonly metadata?: Metadata;
2828
}
2929

3030
export const IExtensionsProfileScannerService = createDecorator<IExtensionsProfileScannerService>('IExtensionsProfileScannerService');
@@ -94,12 +94,12 @@ export class ExtensionsProfileScannerService extends Disposable implements IExte
9494
// Update
9595
if (updateFn) {
9696
extensions = updateFn(extensions);
97-
const storedWebExtensions: IStoredProfileExtension[] = extensions.map(e => ({
97+
const storedProfileExtensions: IStoredProfileExtension[] = extensions.map(e => ({
9898
identifier: e.identifier,
9999
location: e.location.toJSON(),
100100
metadata: e.metadata
101101
}));
102-
await this.fileService.writeFile(file, VSBuffer.fromString(JSON.stringify(storedWebExtensions)));
102+
await this.fileService.writeFile(file, VSBuffer.fromString(JSON.stringify(storedProfileExtensions)));
103103
}
104104

105105
return extensions;

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

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import { ILogService } from 'vs/platform/log/common/log';
3232
import { IProductService } from 'vs/platform/product/common/productService';
3333
import { Emitter, Event } from 'vs/base/common/event';
3434
import { revive } from 'vs/base/common/marshalling';
35-
import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
35+
import { IExtensionsProfileScannerService, IScannedProfileExtension } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
36+
import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
3637

3738
export type IScannedExtensionManifest = IRelaxedExtensionManifest & { __metadata?: Metadata };
3839

@@ -147,6 +148,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem
147148
readonly userExtensionsLocation: URI,
148149
private readonly extensionsControlLocation: URI,
149150
private readonly cacheLocation: URI,
151+
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
150152
@IExtensionsProfileScannerService protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService,
151153
@IFileService protected readonly fileService: IFileService,
152154
@ILogService protected readonly logService: ILogService,
@@ -378,18 +380,14 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem
378380

379381
private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean = true): Promise<ExtensionScannerInput> {
380382
const translations = await this.getTranslations(language ?? platform.language);
381-
let mtime: number | undefined;
382-
try {
383-
const folderStat = await this.fileService.stat(location);
384-
if (typeof folderStat.mtime === 'number') {
385-
mtime = folderStat.mtime;
386-
}
387-
} catch (err) {
388-
// That's ok...
389-
}
383+
const mtime = await this.getMtime(location);
384+
const applicationExtensionsLocation = this.userDataProfilesService.defaultProfile.extensionsResource;
385+
const applicationExtensionsLocationMtime = applicationExtensionsLocation ? await this.getMtime(applicationExtensionsLocation) : undefined;
390386
return new ExtensionScannerInput(
391387
location,
392388
mtime,
389+
applicationExtensionsLocation,
390+
applicationExtensionsLocationMtime,
393391
profile,
394392
type,
395393
excludeObsolete,
@@ -403,13 +401,27 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem
403401
);
404402
}
405403

404+
private async getMtime(location: URI): Promise<number | undefined> {
405+
try {
406+
const stat = await this.fileService.stat(location);
407+
if (typeof stat.mtime === 'number') {
408+
return stat.mtime;
409+
}
410+
} catch (err) {
411+
// That's ok...
412+
}
413+
return undefined;
414+
}
415+
406416
}
407417

408418
class ExtensionScannerInput {
409419

410420
constructor(
411421
public readonly location: URI,
412422
public readonly mtime: number | undefined,
423+
public readonly applicationExtensionslocation: URI | undefined,
424+
public readonly applicationExtensionslocationMtime: number | undefined,
413425
public readonly profile: boolean,
414426
public readonly type: ExtensionType,
415427
public readonly excludeObsolete: boolean,
@@ -437,6 +449,8 @@ class ExtensionScannerInput {
437449
return (
438450
isEqual(a.location, b.location)
439451
&& a.mtime === b.mtime
452+
&& isEqual(a.applicationExtensionslocation, b.applicationExtensionslocation)
453+
&& a.applicationExtensionslocationMtime === b.applicationExtensionslocationMtime
440454
&& a.profile === b.profile
441455
&& a.type === b.type
442456
&& a.excludeObsolete === b.excludeObsolete
@@ -495,21 +509,36 @@ class ExtensionsScanner extends Disposable {
495509
if (input.type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) {
496510
return null;
497511
}
498-
const extensionScannerInput = new ExtensionScannerInput(c.resource, input.mtime, input.profile, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations);
512+
const extensionScannerInput = new ExtensionScannerInput(c.resource, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations);
499513
return this.scanExtension(extensionScannerInput);
500514
}));
501515
return coalesce(extensions);
502516
}
503517

504518
private async scanExtensionsFromProfile(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {
505-
const scannedProfileExtensions = await this.extensionsProfileScannerService.scanProfileExtensions(input.location);
519+
const profileExtensions = await this.scanExtensionsFromProfileResource(input.location, () => true, input);
520+
const applicationExtensions = await this.scanApplicationExtensions(input);
521+
return [...profileExtensions, ...applicationExtensions];
522+
}
523+
524+
private async scanApplicationExtensions(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {
525+
return input.applicationExtensionslocation
526+
? this.scanExtensionsFromProfileResource(input.applicationExtensionslocation, (e) => !!e.metadata?.isApplicationScoped, input)
527+
: [];
528+
}
529+
530+
private async scanExtensionsFromProfileResource(profileResource: URI, filter: (extensionInfo: IScannedProfileExtension) => boolean, input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {
531+
const scannedProfileExtensions = await this.extensionsProfileScannerService.scanProfileExtensions(profileResource);
506532
if (!scannedProfileExtensions.length) {
507533
return [];
508534
}
509535
const extensions = await Promise.all<IRelaxedScannedExtension | null>(
510536
scannedProfileExtensions.map(async extensionInfo => {
511-
const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.profile, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations);
512-
return this.scanExtension(extensionScannerInput);
537+
if (filter(extensionInfo)) {
538+
const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations);
539+
return this.scanExtension(extensionScannerInput, extensionInfo.metadata);
540+
}
541+
return null;
513542
}));
514543
return coalesce(extensions);
515544
}
@@ -910,6 +939,7 @@ export class NativeExtensionsScannerService extends AbstractExtensionsScannerSer
910939
userExtensionsLocation: URI,
911940
userHome: URI,
912941
userDataPath: URI,
942+
userDataProfilesService: IUserDataProfilesService,
913943
extensionsProfileScannerService: IExtensionsProfileScannerService,
914944
fileService: IFileService,
915945
logService: ILogService,
@@ -921,7 +951,7 @@ export class NativeExtensionsScannerService extends AbstractExtensionsScannerSer
921951
userExtensionsLocation,
922952
joinPath(userHome, '.vscode-oss-dev', 'extensions', 'control.json'),
923953
joinPath(userDataPath, MANIFEST_CACHE_FOLDER),
924-
extensionsProfileScannerService, fileService, logService, environmentService, productService);
954+
userDataProfilesService, extensionsProfileScannerService, fileService, logService, environmentService, productService);
925955
this.translationsPromise = (async () => {
926956
if (platform.translationsConfigFile) {
927957
try {

0 commit comments

Comments
 (0)