Merge vscode 1.67 (#20883)

* Fix initial build breaks from 1.67 merge (#2514)

* Update yarn lock files

* Update build scripts

* Fix tsconfig

* Build breaks

* WIP

* Update yarn lock files

* Misc breaks

* Updates to package.json

* Breaks

* Update yarn

* Fix breaks

* Breaks

* Build breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Missing file

* Breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Fix several runtime breaks (#2515)

* Missing files

* Runtime breaks

* Fix proxy ordering issue

* Remove commented code

* Fix breaks with opening query editor

* Fix post merge break

* Updates related to setup build and other breaks (#2516)

* Fix bundle build issues

* Update distro

* Fix distro merge and update build JS files

* Disable pipeline steps

* Remove stats call

* Update license name

* Make new RPM dependencies a warning

* Fix extension manager version checks

* Update JS file

* Fix a few runtime breaks

* Fixes

* Fix runtime issues

* Fix build breaks

* Update notebook tests (part 1)

* Fix broken tests

* Linting errors

* Fix hygiene

* Disable lint rules

* Bump distro

* Turn off smoke tests

* Disable integration tests

* Remove failing "activate" test

* Remove failed test assertion

* Disable other broken test

* Disable query history tests

* Disable extension unit tests

* Disable failing tasks
This commit is contained in:
Karl Burtram
2022-10-19 19:13:18 -07:00
committed by GitHub
parent 33c6daaea1
commit 8a3d08f0de
3738 changed files with 192313 additions and 107208 deletions

View File

@@ -6,7 +6,7 @@
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { Barrier, CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { canceled, getErrorMessage } from 'vs/base/common/errors';
import { CancellationError, getErrorMessage } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { isWeb } from 'vs/base/common/platform';
@@ -14,10 +14,10 @@ import { URI } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import {
DidUninstallExtensionEvent, ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOperation, InstallOptions,
InstallVSIXOptions, IReportedExtension, StatisticType, UninstallOptions, TargetPlatform, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode
InstallVSIXOptions, IExtensionsControlManifest, StatisticType, UninstallOptions, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { areSameExtensions, ExtensionKey, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@@ -44,7 +44,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
declare readonly _serviceBrand: undefined;
private reportedExtensions: Promise<IReportedExtension[]> | undefined;
private extensionsControlManifest: Promise<IExtensionsControlManifest> | undefined;
private lastReportTimestamp = 0;
private readonly installingExtensions = new Map<string, IInstallExtensionTask>();
private readonly uninstallingExtensions = new Map<string, IUninstallExtensionTask>();
@@ -84,44 +84,18 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
}
async installFromGallery(extension: IGalleryExtension, options: InstallOptions = {}): Promise<ILocalExtension> {
if (!this.galleryService.isEnabled()) {
throw new ExtensionManagementError(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"), ExtensionManagementErrorCode.Internal);
}
if (!await this.canInstall(extension)) {
const targetPlatform = await this.getTargetPlatform();
const error = new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.Incompatible);
this.logService.error(`Cannot install extension.`, extension.identifier.id, error.message);
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
throw error;
}
try {
extension = await this.checkAndGetCompatibleVersion(extension, !options.installGivenVersion);
if (!this.galleryService.isEnabled()) {
throw new ExtensionManagementError(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"), ExtensionManagementErrorCode.Internal);
}
const compatible = await this.checkAndGetCompatibleVersion(extension, !options.installGivenVersion, !!options.installPreReleaseVersion);
return await this.installExtension(compatible.manifest, compatible.extension, options);
} catch (error) {
this.logService.error(getErrorMessage(error));
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
throw error;
reportTelemetry(this.telemetryService, 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(extension), error });
this.logService.error(`Failed to install extension.`, extension.identifier.id);
this.logService.error(error);
throw toExtensionManagementError(error);
}
const manifest = await this.galleryService.getManifest(extension, CancellationToken.None);
if (manifest === null) {
const error = new ExtensionManagementError(`Missing manifest for extension ${extension.identifier.id}`, ExtensionManagementErrorCode.Invalid);
this.logService.error(`Failed to install extension:`, extension.identifier.id, error.message);
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
throw error;
}
/* {{SQL CARBON EDIT}} Remove this check as we don't want to enforce the manifest versions matching since those are often coming directly from the main branch
if (manifest.version !== extension.version) {
const error = new ExtensionManagementError(`Cannot install '${extension.identifier.id}' extension because of version mismatch in Marketplace`, ExtensionManagementErrorCode.Invalid);
this.logService.error(error.message);
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
throw error;
}
*/
return this.installExtension(manifest, extension, options);
}
async uninstall(extension: ILocalExtension, options: UninstallOptions = {}): Promise<void> {
@@ -135,7 +109,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
throw new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"));
}
const galleryExtension = await this.findGalleryExtension(extension);
const targetPlatform = await this.getTargetPlatform();
const [galleryExtension] = await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: extension.preRelease }], { targetPlatform, compatible: true }, CancellationToken.None);
if (!galleryExtension) {
throw new Error(nls.localize('Not a Marketplace extension', "Only Marketplace Extensions can be reinstalled"));
}
@@ -144,15 +119,15 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
await this.installFromGallery(galleryExtension);
}
getExtensionsReport(): Promise<IReportedExtension[]> {
getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
const now = new Date().getTime();
if (!this.reportedExtensions || now - this.lastReportTimestamp > 1000 * 60 * 5) { // 5 minute cache freshness
this.reportedExtensions = this.updateReportCache();
if (!this.extensionsControlManifest || now - this.lastReportTimestamp > 1000 * 60 * 5) { // 5 minute cache freshness
this.extensionsControlManifest = this.updateControlCache();
this.lastReportTimestamp = now;
}
return this.reportedExtensions;
return this.extensionsControlManifest;
}
registerParticipant(participant: IExtensionManagementParticipant): void {
@@ -162,7 +137,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
protected async installExtension(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions & InstallVSIXOptions): Promise<ILocalExtension> {
// only cache gallery extensions tasks
if (!URI.isUri(extension)) {
let installExtensionTask = this.installingExtensions.get(new ExtensionIdentifierWithVersion(extension.identifier, extension.version).key());
let installExtensionTask = this.installingExtensions.get(ExtensionKey.create(extension).toString());
if (installExtensionTask) {
this.logService.info('Extensions is already requested to install', extension.identifier.id);
return installExtensionTask.waitUntilTaskIsFinished();
@@ -170,11 +145,11 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
options = { ...options, installOnlyNewlyAddedFromExtensionPack: true /* always true for gallery extensions */ };
}
const allInstallExtensionTasks: { task: IInstallExtensionTask, manifest: IExtensionManifest }[] = [];
const allInstallExtensionTasks: { task: IInstallExtensionTask; manifest: IExtensionManifest }[] = [];
const installResults: (InstallExtensionResult & { local: ILocalExtension })[] = [];
const installExtensionTask = this.createInstallExtensionTask(manifest, extension, options);
if (!URI.isUri(extension)) {
this.installingExtensions.set(new ExtensionIdentifierWithVersion(installExtensionTask.identifier, manifest.version).key(), installExtensionTask);
this.installingExtensions.set(ExtensionKey.create(extension).toString(), installExtensionTask);
}
this._onInstallExtension.fire({ identifier: installExtensionTask.identifier, source: extension });
this.logService.info('Installing extension:', installExtensionTask.identifier.id);
@@ -186,14 +161,15 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
this.logService.info('Installing the extension without checking dependencies and pack', installExtensionTask.identifier.id);
} else {
try {
const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensionsToInstall(installExtensionTask.identifier, manifest, !!options.installOnlyNewlyAddedFromExtensionPack);
const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensionsToInstall(installExtensionTask.identifier, manifest, !!options.installOnlyNewlyAddedFromExtensionPack, !!options.installPreReleaseVersion);
for (const { gallery, manifest } of allDepsAndPackExtensionsToInstall) {
installExtensionHasDependents = installExtensionHasDependents || !!manifest.extensionDependencies?.some(id => areSameExtensions({ id }, installExtensionTask.identifier));
if (this.installingExtensions.has(new ExtensionIdentifierWithVersion(gallery.identifier, gallery.version).key())) {
const key = ExtensionKey.create(gallery).toString();
if (this.installingExtensions.has(key)) {
this.logService.info('Extension is already requested to install', gallery.identifier.id);
} else {
const task = this.createInstallExtensionTask(manifest, gallery, { ...options, donotIncludePackAndDependencies: true });
this.installingExtensions.set(new ExtensionIdentifierWithVersion(task.identifier, manifest.version).key(), task);
this.installingExtensions.set(key, task);
this._onInstallExtension.fire({ identifier: task.identifier, source: gallery });
this.logService.info('Installing extension:', task.identifier.id);
allInstallExtensionTasks.push({ task, manifest });
@@ -211,7 +187,6 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
}
} else {
this.logService.error('Error while preparing to install dependencies and extension packs of the extension:', installExtensionTask.identifier.id);
this.logService.error(error);
throw error;
}
}
@@ -220,7 +195,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
const extensionsToInstallMap = allInstallExtensionTasks.reduce((result, { task, manifest }) => {
result.set(task.identifier.id.toLowerCase(), { task, manifest });
return result;
}, new Map<string, { task: IInstallExtensionTask, manifest: IExtensionManifest }>());
}, new Map<string, { task: IInstallExtensionTask; manifest: IExtensionManifest }>());
while (extensionsToInstallMap.size) {
let extensionsToInstall;
@@ -241,9 +216,14 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
const local = await task.run();
await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, options, CancellationToken.None)));
if (!URI.isUri(task.source)) {
reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', getGalleryExtensionTelemetryData(task.source), new Date().getTime() - startTime, undefined);
const isUpdate = task.operation === InstallOperation.Update;
reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', {
extensionData: getGalleryExtensionTelemetryData(task.source),
duration: new Date().getTime() - startTime,
durationSinceUpdate: isUpdate ? undefined : new Date().getTime() - task.source.lastUpdated
});
// In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX.
if (isWeb && task.operation === InstallOperation.Install) {
if (isWeb && task.operation !== InstallOperation.Update) {
try {
await this.galleryService.reportStatistic(local.manifest.publisher, local.manifest.name, local.manifest.version, StatisticType.Install);
} catch (error) { /* ignore */ }
@@ -252,10 +232,9 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source });
} catch (error) {
if (!URI.isUri(task.source)) {
reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', getGalleryExtensionTelemetryData(task.source), new Date().getTime() - startTime, error);
reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), duration: new Date().getTime() - startTime, error });
}
this.logService.error('Error while installing the extension:', task.identifier.id);
this.logService.error(error);
throw error;
} finally { extensionsToInstallMap.delete(task.identifier.id.toLowerCase()); }
}));
@@ -289,18 +268,13 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
}
}
this.logService.error(`Failed to install extension:`, installExtensionTask.identifier.id, getErrorMessage(error));
this._onDidInstallExtensions.fire(allInstallExtensionTasks.map(({ task }) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source })));
if (error instanceof Error) {
error.name = error && (<ExtensionManagementError>error).code ? (<ExtensionManagementError>error).code : ExtensionManagementErrorCode.Internal;
}
throw error;
} finally {
/* Remove the gallery tasks from the cache */
for (const { task, manifest } of allInstallExtensionTasks) {
for (const { task } of allInstallExtensionTasks) {
if (!URI.isUri(task.source)) {
const key = new ExtensionIdentifierWithVersion(task.identifier, manifest.version).key();
const key = ExtensionKey.create(task.source).toString();
if (!this.installingExtensions.delete(key)) {
this.logService.warn('Installation task is not found in the cache', key);
}
@@ -325,7 +299,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
return results;
}
private async getAllDepsAndPackExtensionsToInstall(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean): Promise<{ gallery: IGalleryExtension, manifest: IExtensionManifest }[]> {
private async getAllDepsAndPackExtensionsToInstall(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> {
if (!this.galleryService.isEnabled()) {
return [];
}
@@ -333,7 +307,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
let installed = await this.getInstalled();
const knownIdentifiers = [extensionIdentifier, ...(installed).map(i => i.identifier)];
const allDependenciesAndPacks: { gallery: IGalleryExtension, manifest: IExtensionManifest }[] = [];
const allDependenciesAndPacks: { gallery: IGalleryExtension; manifest: IExtensionManifest }[] = [];
const collectDependenciesAndPackExtensionsToInstall = async (extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest): Promise<void> => {
const dependecies: string[] = manifest.extensionDependencies || [];
const dependenciesAndPackExtensions = [...dependecies];
@@ -352,25 +326,27 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
if (dependenciesAndPackExtensions.length) {
// filter out installed and known extensions
const identifiers = [...knownIdentifiers, ...allDependenciesAndPacks.map(r => r.gallery.identifier)];
const names = dependenciesAndPackExtensions.filter(id => identifiers.every(galleryIdentifier => !areSameExtensions(galleryIdentifier, { id })));
if (names.length) {
const galleryResult = await this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length }, CancellationToken.None);
for (const galleryExtension of galleryResult.firstPage) {
const ids = dependenciesAndPackExtensions.filter(id => identifiers.every(galleryIdentifier => !areSameExtensions(galleryIdentifier, { id })));
if (ids.length) {
const galleryExtensions = await this.galleryService.getExtensions(ids.map(id => ({ id, preRelease: installPreRelease })), CancellationToken.None);
for (const galleryExtension of galleryExtensions) {
if (identifiers.find(identifier => areSameExtensions(identifier, galleryExtension.identifier))) {
continue;
}
const isDependency = dependecies.some(id => areSameExtensions({ id }, galleryExtension.identifier));
if (!isDependency && !await this.canInstall(galleryExtension)) {
this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id);
continue;
let compatible;
try {
compatible = await this.checkAndGetCompatibleVersion(galleryExtension, true, installPreRelease);
} catch (error) {
if (error instanceof ExtensionManagementError && error.code === ExtensionManagementErrorCode.IncompatibleTargetPlatform && !isDependency) {
this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id);
continue;
} else {
throw error;
}
}
const compatibleExtension = await this.checkAndGetCompatibleVersion(galleryExtension, true);
const manifest = await this.galleryService.getManifest(compatibleExtension, CancellationToken.None);
if (manifest === null) {
throw new ExtensionManagementError(`Missing manifest for extension ${compatibleExtension.identifier.id}`, ExtensionManagementErrorCode.Invalid);
}
allDependenciesAndPacks.push({ gallery: compatibleExtension, manifest });
await collectDependenciesAndPackExtensionsToInstall(compatibleExtension.identifier, manifest);
allDependenciesAndPacks.push({ gallery: compatible.extension, manifest: compatible.manifest });
await collectDependenciesAndPackExtensionsToInstall(compatible.extension.identifier, compatible.manifest);
}
}
}
@@ -381,38 +357,68 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
return allDependenciesAndPacks.filter(e => !installed.some(i => areSameExtensions(i.identifier, e.gallery.identifier)));
}
private async checkAndGetCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean): Promise<IGalleryExtension> {
if (await this.isMalicious(extension)) {
private async checkAndGetCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean, installPreRelease: boolean): Promise<{ extension: IGalleryExtension; manifest: IExtensionManifest }> {
const report = await this.getExtensionsControlManifest();
if (getMaliciousExtensionsSet(report).has(extension.identifier.id)) {
throw new ExtensionManagementError(nls.localize('malicious extension', "Can't install '{0}' extension since it was reported to be problematic.", extension.identifier.id), ExtensionManagementErrorCode.Malicious);
}
const compatibleExtension = await this.getCompatibleVersion(extension, fetchCompatibleVersion);
if (!compatibleExtension) {
if (!!report.unsupportedPreReleaseExtensions && !!report.unsupportedPreReleaseExtensions[extension.identifier.id]) {
throw new ExtensionManagementError(nls.localize('unsupported prerelease extension', "Can't install '{0}' extension because it is no longer supported. It is now part of the '{1}' extension as a pre-release version.", extension.identifier.id, report.unsupportedPreReleaseExtensions[extension.identifier.id].displayName), ExtensionManagementErrorCode.UnsupportedPreRelease);
}
if (!await this.canInstall(extension)) {
const targetPlatform = await this.getTargetPlatform();
throw new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.IncompatibleTargetPlatform);
}
const compatibleExtension = await this.getCompatibleVersion(extension, fetchCompatibleVersion, installPreRelease);
if (compatibleExtension) {
if (installPreRelease && fetchCompatibleVersion && extension.hasPreReleaseVersion && !compatibleExtension.properties.isPreReleaseVersion) {
throw new ExtensionManagementError(nls.localize('notFoundCompatiblePrereleaseDependency', "Can't install pre-release version of '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.IncompatiblePreRelease);
}
} else {
/** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */
if (!installPreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) {
throw new ExtensionManagementError(nls.localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound);
}
throw new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible);
}
return compatibleExtension;
this.logService.info('Getting Manifest...', compatibleExtension.identifier.id);
const manifest = await this.galleryService.getManifest(compatibleExtension, CancellationToken.None);
if (manifest === null) {
throw new ExtensionManagementError(`Missing manifest for extension ${extension.identifier.id}`, ExtensionManagementErrorCode.Invalid);
}
/* {{SQL CARBON EDIT}} Remove this check as we don't want to enforce the manifest versions matching since those are often coming directly from the main branch
if (manifest.version !== compatibleExtension.version) {
throw new ExtensionManagementError(`Cannot install '${extension.identifier.id}' extension because of version mismatch in Marketplace`, ExtensionManagementErrorCode.Invalid);
}
*/
return { extension: compatibleExtension, manifest };
}
protected async getCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean): Promise<IGalleryExtension | null> {
protected async getCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean, includePreRelease: boolean): Promise<IGalleryExtension | null> {
const targetPlatform = await this.getTargetPlatform();
let compatibleExtension: IGalleryExtension | null = null;
if (await this.galleryService.isExtensionCompatible(extension, targetPlatform)) {
if (fetchCompatibleVersion && extension.hasPreReleaseVersion && extension.properties.isPreReleaseVersion !== includePreRelease) {
compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: includePreRelease }], { targetPlatform, compatible: true }, CancellationToken.None))[0] || null;
}
if (!compatibleExtension && await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform)) {
compatibleExtension = extension;
}
if (!compatibleExtension && fetchCompatibleVersion) {
compatibleExtension = await this.galleryService.getCompatibleExtension(extension, targetPlatform);
compatibleExtension = await this.galleryService.getCompatibleExtension(extension, includePreRelease, targetPlatform);
}
return compatibleExtension;
}
private async isMalicious(extension: IGalleryExtension): Promise<boolean> {
const report = await this.getExtensionsReport();
return getMaliciousExtensionsSet(report).has(extension.identifier.id);
}
private async unininstallExtension(extension: ILocalExtension, options: UninstallOptions): Promise<void> {
const uninstallExtensionTask = this.uninstallingExtensions.get(extension.identifier.id.toLowerCase());
if (uninstallExtensionTask) {
@@ -434,7 +440,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
} else {
this.logService.info('Successfully uninstalled extension:', extension.identifier.id);
}
reportTelemetry(this.telemetryService, 'extensionGallery:uninstall', getLocalExtensionTelemetryData(extension), undefined, error);
reportTelemetry(this.telemetryService, 'extensionGallery:uninstall', { extensionData: getLocalExtensionTelemetryData(extension), error });
this._onDidUninstallExtension.fire({ identifier: extension.identifier, error: error?.code });
};
@@ -567,33 +573,15 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.some(id => areSameExtensions({ id }, extension.identifier)));
}
private async findGalleryExtension(local: ILocalExtension): Promise<IGalleryExtension> {
if (local.identifier.uuid) {
const galleryExtension = await this.findGalleryExtensionById(local.identifier.uuid);
return galleryExtension ? galleryExtension : this.findGalleryExtensionByName(local.identifier.id);
}
return this.findGalleryExtensionByName(local.identifier.id);
}
private async findGalleryExtensionById(uuid: string): Promise<IGalleryExtension> {
const galleryResult = await this.galleryService.query({ ids: [uuid], pageSize: 1 }, CancellationToken.None);
return galleryResult.firstPage[0];
}
private async findGalleryExtensionByName(name: string): Promise<IGalleryExtension> {
const galleryResult = await this.galleryService.query({ names: [name], pageSize: 1 }, CancellationToken.None);
return galleryResult.firstPage[0];
}
private async updateReportCache(): Promise<IReportedExtension[]> {
private async updateControlCache(): Promise<IExtensionsControlManifest> {
try {
this.logService.trace('ExtensionManagementService.refreshReportedCache');
const result = await this.galleryService.getExtensionsReport();
this.logService.trace(`ExtensionManagementService.refreshReportedCache - got ${result.length} reported extensions from service`);
return result;
const manifest = await this.galleryService.getExtensionsControlManifest();
this.logService.trace(`ExtensionManagementService.refreshControlCache`, manifest);
return manifest;
} catch (err) {
this.logService.trace('ExtensionManagementService.refreshReportedCache - failed to get extension report');
return [];
this.logService.trace('ExtensionManagementService.refreshControlCache - failed to get extension control manifest');
return { malicious: [] };
}
}
@@ -621,12 +609,22 @@ export function joinErrors(errorOrErrors: (Error | string) | (Array<Error | stri
}, new Error(''));
}
export function reportTelemetry(telemetryService: ITelemetryService, eventName: string, extensionData: any, duration?: number, error?: Error): void {
function toExtensionManagementError(error: Error): ExtensionManagementError {
if (error instanceof ExtensionManagementError) {
return error;
}
const e = new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Internal);
e.stack = error.stack;
return e;
}
export function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, duration, error, durationSinceUpdate }: { extensionData: any; duration?: number; durationSinceUpdate?: number; error?: Error }): void {
const errorcode = error ? error instanceof ExtensionManagementError ? error.code : ExtensionManagementErrorCode.Internal : undefined;
/* __GDPR__
"extensionGallery:install" : {
"success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"durationSinceUpdate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
"recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"${include}": [
@@ -654,7 +652,7 @@ export function reportTelemetry(telemetryService: ITelemetryService, eventName:
]
}
*/
telemetryService.publicLogError(eventName, { ...extensionData, success: !error, duration, errorcode });
telemetryService.publicLog(eventName, { ...extensionData, success: !error, duration, errorcode, durationSinceUpdate });
}
export abstract class AbstractExtensionTask<T> {
@@ -681,7 +679,7 @@ export abstract class AbstractExtensionTask<T> {
return new Promise((c, e) => {
const disposable = token.onCancellationRequested(() => {
disposable.dispose();
e(canceled());
e(new CancellationError());
});
});
});

View File

@@ -6,7 +6,7 @@
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { DISABLED_EXTENSIONS_STORAGE_PATH, IExtensionIdentifier, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { DISABLED_EXTENSIONS_STORAGE_PATH, IExtensionIdentifier, IExtensionManagementService, IGlobalExtensionEnablementService, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
@@ -14,16 +14,22 @@ export class GlobalExtensionEnablementService extends Disposable implements IGlo
declare readonly _serviceBrand: undefined;
private _onDidChangeEnablement = new Emitter<{ readonly extensions: IExtensionIdentifier[], readonly source?: string }>();
readonly onDidChangeEnablement: Event<{ readonly extensions: IExtensionIdentifier[], readonly source?: string }> = this._onDidChangeEnablement.event;
private _onDidChangeEnablement = new Emitter<{ readonly extensions: IExtensionIdentifier[]; readonly source?: string }>();
readonly onDidChangeEnablement: Event<{ readonly extensions: IExtensionIdentifier[]; readonly source?: string }> = this._onDidChangeEnablement.event;
private readonly storageManger: StorageManager;
constructor(
@IStorageService storageService: IStorageService,
@IExtensionManagementService extensionManagementService: IExtensionManagementService,
) {
super();
this.storageManger = this._register(new StorageManager(storageService));
this._register(this.storageManger.onDidChange(extensions => this._onDidChangeEnablement.fire({ extensions, source: 'storage' })));
this._register(extensionManagementService.onDidInstallExtensions(e => e.forEach(({ local, operation }) => {
if (local && operation === InstallOperation.Migrate) {
this._removeFromDisabledExtensions(local.identifier); /* Reset migrated extensions */
}
})));
}
async enableExtension(extension: IExtensionIdentifier, source?: string): Promise<boolean> {

View File

@@ -5,27 +5,28 @@
import { distinct } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation';
import { canceled, getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors';
import { IStringDictionary } from 'vs/base/common/collections';
import { CancellationError, getErrorMessage, isCancellationError } from 'vs/base/common/errors';
import { getOrDefault } from 'vs/base/common/objects';
import { IPager } from 'vs/base/common/paging';
import { isWeb, platform } from 'vs/base/common/platform';
import { arch } from 'vs/base/common/process';
import { isBoolean } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request';
import { IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { DefaultIconPath, getFallbackTargetPlarforms, getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionIdentifierWithVersion, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IReportedExtension, isIExtensionIdentifier, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, TargetPlatform, toTargetPlatform, WEB_EXTENSION_TAG } from 'vs/platform/extensionManagement/common/extensionManagement';
import { getFallbackTargetPlarforms, getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionsPolicy, ExtensionsPolicyKey, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; // {{SQL CARBON EDIT}} Add ExtensionsPolicy and ExtensionsPolicyKey
import { ExtensionsPolicy, ExtensionsPolicyKey, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { asJson, asText, IRequestService } from 'vs/platform/request/common/request'; // {{SQL CARBON EDIT}} Remove unused
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
import { getTelemetryLevel, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils';
import { asJson, asTextOrError, IRequestService } from 'vs/platform/request/common/request'; // {{SQL CARBON EDIT}} - remove unused
import { resolveMarketplaceHeaders } from 'vs/platform/externalServices/common/marketplace';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
const CURRENT_TARGET_PLATFORM = isWeb ? TargetPlatform.WEB : getTargetPlatform(platform, arch);
@@ -87,23 +88,88 @@ interface IRawGalleryQueryResult {
readonly name: string;
readonly count: number;
}[];
}[]
}[];
}[];
}
enum Flags {
/**
* None is used to retrieve only the basic extension details.
*/
None = 0x0,
/**
* IncludeVersions will return version information for extensions returned
*/
IncludeVersions = 0x1,
/**
* IncludeFiles will return information about which files were found
* within the extension that were stored independent of the manifest.
* When asking for files, versions will be included as well since files
* are returned as a property of the versions.
* These files can be retrieved using the path to the file without
* requiring the entire manifest be downloaded.
*/
IncludeFiles = 0x2,
/**
* Include the Categories and Tags that were added to the extension definition.
*/
IncludeCategoryAndTags = 0x4,
/**
* Include the details about which accounts the extension has been shared
* with if the extension is a private extension.
*/
IncludeSharedAccounts = 0x8,
/**
* Include properties associated with versions of the extension
*/
IncludeVersionProperties = 0x10,
/**
* Excluding non-validated extensions will remove any extension versions that
* either are in the process of being validated or have failed validation.
*/
ExcludeNonValidated = 0x20,
/**
* Include the set of installation targets the extension has requested.
*/
IncludeInstallationTargets = 0x40,
/**
* Include the base uri for assets of this extension
*/
IncludeAssetUri = 0x80,
/**
* Include the statistics associated with this extension
*/
IncludeStatistics = 0x100,
/**
* When retrieving versions from a query, only include the latest
* version of the extensions that matched. This is useful when the
* caller doesn't need all the published versions. It will save a
* significant size in the returned payload.
*/
IncludeLatestVersionOnly = 0x200,
Unpublished = 0x1000
/**
* This flag switches the asset uri to use GetAssetByName instead of CDN
* When this is used, values of base asset uri and base asset uri fallback are switched
* When this is used, source of asset files are pointed to Gallery service always even if CDN is available
*/
Unpublished = 0x1000,
/**
* Include the details if an extension is in conflict list or not
*/
IncludeNameConflictInfo = 0x8000,
}
function flagsToString(...flags: Flags[]): string {
@@ -137,6 +203,7 @@ const PropertyType = {
Dependency: 'Microsoft.VisualStudio.Code.ExtensionDependencies',
ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack',
Engine: 'Microsoft.VisualStudio.Code.Engine',
PreRelease: 'Microsoft.VisualStudio.Code.PreRelease',
// {{SQL CARBON EDIT}}
AzDataEngine: 'Microsoft.AzDataEngine',
LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages',
@@ -158,6 +225,7 @@ interface IQueryState {
readonly flags: Flags;
readonly criteria: ICriterium[];
readonly assetTypes: string[];
readonly source?: string;
}
const DefaultQueryState: IQueryState = {
@@ -172,23 +240,29 @@ const DefaultQueryState: IQueryState = {
/* {{SQL CARBON EDIT}} Remove unused
type GalleryServiceQueryClassification = {
readonly filterTypes: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly sortBy: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly sortOrder: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly duration: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', 'isMeasurement': true };
readonly success: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly requestBodySize: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly responseBodySize?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly statusCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly errorCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly count?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
readonly filterTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
readonly flags: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
readonly sortBy: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
readonly sortOrder: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
readonly pageNumber: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
readonly duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true };
readonly success: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
readonly requestBodySize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
readonly responseBodySize?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
readonly statusCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
readonly errorCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
readonly count?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
readonly source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
};
*/
type QueryTelemetryData = {
readonly flags: number;
readonly filterTypes: string[];
readonly sortBy: string;
readonly sortOrder: string;
readonly pageNumber: string;
readonly source?: string;
};
/* {{SQL CARBON EDIT}} Remove unused
@@ -201,7 +275,23 @@ type GalleryServiceQueryEvent = QueryTelemetryData & {
readonly errorCode?: string;
readonly count?: string;
};
type GalleryServiceAdditionalQueryClassification = {
readonly duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true };
readonly count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
};
type GalleryServiceAdditionalQueryEvent = {
readonly duration: number;
readonly count: number;
};
*/
interface IExtensionCriteria {
readonly targetPlatform: TargetPlatform;
readonly compatible: boolean;
readonly includePreRelease: boolean | (IExtensionIdentifier & { includePreRelease: boolean })[];
readonly versions?: (IExtensionIdentifier & { version: string })[];
}
class Query {
@@ -244,6 +334,10 @@ class Query {
return new Query({ ...this.state, assetTypes });
}
withSource(source: string): Query {
return new Query({ ...this.state, source });
}
get raw(): any {
const { criteria, pageNumber, pageSize, sortBy, sortOrder, flags, assetTypes } = this.state;
const filters = [{ criteria, pageNumber, pageSize, sortBy, sortOrder }];
@@ -258,8 +352,11 @@ class Query {
get telemetryData(): QueryTelemetryData {
return {
filterTypes: this.state.criteria.map(criterium => String(criterium.filterType)),
flags: this.state.flags,
sortBy: String(this.sortBy),
sortOrder: String(this.sortOrder)
sortOrder: String(this.sortOrder),
pageNumber: String(this.pageNumber),
source: this.state.source
};
}
}
@@ -306,15 +403,6 @@ function getDownloadAsset(version: IRawGalleryExtensionVersion): IGalleryExtensi
};
}
function getIconAsset(version: IRawGalleryExtensionVersion): IGalleryExtensionAsset {
const asset = getVersionAsset(version, AssetType.Icon);
if (asset) {
return asset;
}
const uri = DefaultIconPath;
return { uri, fallbackUri: uri };
}
function getVersionAsset(version: IRawGalleryExtensionVersion, type: string): IGalleryExtensionAsset | null {
const result = version.files.filter(f => f.assetType === type)[0];
@@ -358,6 +446,11 @@ function getAzureDataStudioEngine(version: IRawGalleryExtensionVersion): string
return (values.length > 0 && values[0].value) || '';
}
function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.PreRelease) : [];
return values.length > 0 && values[0].value === 'true';
}
function getLocalizedLanguages(version: IRawGalleryExtensionVersion): string[] {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.LocalizedLanguages) : [];
const value = (values.length > 0 && values[0].value) || '';
@@ -420,14 +513,18 @@ export function sortExtensionVersions(versions: IRawGalleryExtensionVersion[], p
return versions;
}
function toExtensionWithLatestVersion(galleryExtension: IRawGalleryExtension, index: number, query: Query, querySource: string | undefined, targetPlatform: TargetPlatform): IGalleryExtension {
const allTargetPlatforms = getAllTargetPlatforms(galleryExtension);
let latestVersion = galleryExtension.versions[0];
latestVersion = galleryExtension.versions.find(version => version.version === latestVersion.version && isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(version), allTargetPlatforms, targetPlatform)) || latestVersion;
return toExtension(galleryExtension, latestVersion, allTargetPlatforms, index, query, querySource);
function setTelemetry(extension: IGalleryExtension, index: number, querySource?: string): void {
/* __GDPR__FRAGMENT__
"GalleryExtensionTelemetryData2" : {
"index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
extension.telemetryData = { index, querySource };
}
function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], index: number, query: Query, querySource?: string): IGalleryExtension {
function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[]): IGalleryExtension {
const latestVersion = galleryExtension.versions[0];
const assets = <IGalleryExtensionAssets>{
manifest: getVersionAsset(version, AssetType.Manifest),
readme: getVersionAsset(version, AssetType.Details),
@@ -437,7 +534,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
download: getDownloadAsset(version),
// {{SQL CARBON EDIT}} - Add downloadPage
downloadPage: getVersionAsset(version, AssetType.DownloadPage),
icon: getIconAsset(version),
icon: getVersionAsset(version, AssetType.Icon),
coreTranslations: getCoreTranslationAssets(version)
};
@@ -471,24 +568,19 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
azDataEngine: getAzureDataStudioEngine(version),
localizedLanguages: getLocalizedLanguages(version),
targetPlatform: getTargetPlatformForExtensionVersion(version),
isPreReleaseVersion: isPreReleaseVersion(version)
},
hasPreReleaseVersion: isPreReleaseVersion(latestVersion),
hasReleaseVersion: true,
preview: getIsPreview(galleryExtension.flags),
/* __GDPR__FRAGMENT__
"GalleryExtensionTelemetryData2" : {
"index" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
telemetryData: {
index: ((query.pageNumber - 1) * query.pageSize) + index,
querySource
},
};
}
interface IRawExtensionsReport {
type PreReleaseMigrationInfo = { id: string; displayName: string; migrateStorage?: boolean; engine?: string };
interface IRawExtensionsControlManifest {
malicious: string[];
slow: string[];
unsupported?: IStringDictionary<boolean | { preReleaseExtension: { id: string; displayName: string } }>;
migrateToPreRelease?: IStringDictionary<PreReleaseMigrationInfo>;
}
abstract class AbstractExtensionGalleryService implements IExtensionGalleryService {
@@ -498,7 +590,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
private extensionsGalleryUrl: string | undefined;
private extensionsControlUrl: string | undefined;
private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>;
private readonly commonHeadersPromise: Promise<{ [key: string]: string }>;
constructor(
storageService: IStorageService | undefined,
@@ -525,45 +617,54 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
return !!this.extensionsGalleryUrl;
}
async getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, token: CancellationToken): Promise<IGalleryExtension[]> {
const result: IGalleryExtension[] = [];
let query = new Query()
.withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, identifiers.length)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code')
.withFilter(FilterType.ExtensionName, ...identifiers.map(({ id }) => id.toLowerCase()));
if (identifiers.every(identifier => !(<IExtensionIdentifierWithVersion>identifier).version)) {
query = query.withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties, Flags.IncludeLatestVersionOnly);
}
const { galleryExtensions } = await this.queryGallery(query, CURRENT_TARGET_PLATFORM, CancellationToken.None);
for (let index = 0; index < galleryExtensions.length; index++) {
const galleryExtension = galleryExtensions[index];
if (!galleryExtension.versions.length) {
continue;
}
const id = getGalleryExtensionId(galleryExtension.publisher.publisherName, galleryExtension.extensionName);
const version = (<IExtensionIdentifierWithVersion | undefined>identifiers.find(identifier => areSameExtensions(identifier, { id })))?.version;
if (version) {
const versionAsset = galleryExtension.versions.find(v => v.version === version);
if (versionAsset) {
result.push(toExtension(galleryExtension, versionAsset, getAllTargetPlatforms(galleryExtension), index, query));
}
getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, token: CancellationToken): Promise<IGalleryExtension[]>;
getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, options: IExtensionQueryOptions, token: CancellationToken): Promise<IGalleryExtension[]>;
async getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, arg1: any, arg2?: any): Promise<IGalleryExtension[]> {
const options = CancellationToken.isCancellationToken(arg1) ? {} : arg1 as IExtensionQueryOptions;
const token = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2 as CancellationToken;
const names: string[] = []; const ids: string[] = [], includePreReleases: (IExtensionIdentifier & { includePreRelease: boolean })[] = [], versions: (IExtensionIdentifier & { version: string })[] = [];
let isQueryForReleaseVersionFromPreReleaseVersion = true;
for (const extensionInfo of extensionInfos) {
if (extensionInfo.uuid) {
ids.push(extensionInfo.uuid);
} else {
result.push(toExtensionWithLatestVersion(galleryExtension, index, query, undefined, CURRENT_TARGET_PLATFORM));
names.push(extensionInfo.id);
}
// Set includePreRelease to true if version is set, because the version can be a pre-release version
const includePreRelease = !!(extensionInfo.version || extensionInfo.preRelease);
includePreReleases.push({ id: extensionInfo.id, uuid: extensionInfo.uuid, includePreRelease });
if (extensionInfo.version) {
versions.push({ id: extensionInfo.id, uuid: extensionInfo.uuid, version: extensionInfo.version });
}
isQueryForReleaseVersionFromPreReleaseVersion = isQueryForReleaseVersionFromPreReleaseVersion && (!!extensionInfo.hasPreRelease && !includePreRelease);
}
return result;
if (!ids.length && !names.length) {
return [];
}
let query = new Query().withPage(1, extensionInfos.length);
if (ids.length) {
query = query.withFilter(FilterType.ExtensionId, ...ids);
}
if (names.length) {
query = query.withFilter(FilterType.ExtensionName, ...names);
}
if (options.queryAllVersions || isQueryForReleaseVersionFromPreReleaseVersion /* Inlcude all versions if every requested extension is for release version and has pre-release version */) {
query = query.withFlags(query.flags, Flags.IncludeVersions);
}
if (options.source) {
query = query.withSource(options.source);
}
const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, includePreRelease: includePreReleases, versions, compatible: !!options.compatible }, token);
if (options.source) {
extensions.forEach((e, index) => setTelemetry(e, index, options.source));
}
return extensions;
}
async getCompatibleExtension(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise<IGalleryExtension | null> {
return this.getCompatibleExtensionByEngine(arg1, version);
}
private async getCompatibleExtensionByEngine(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise<IGalleryExtension | null> {
const extension: IGalleryExtension | null = isIExtensionIdentifier(arg1) ? null : arg1;
async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null> {
// {{SQL CARBON EDIT}}
// Change to original version: removed the extension version validation
// Reason: This method is used to find the matching gallery extension for the locally installed extension,
@@ -572,49 +673,19 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
if (extension) {
return Promise.resolve(extension);
}
const { id, uuid } = <IExtensionIdentifier>arg1; // {{SQL CARBON EDIT}} @anthonydresser remove extension ? extension.identifier
let query = new Query()
.withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, 1)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code');
if (uuid) {
query = query.withFilter(FilterType.ExtensionId, uuid);
} else {
query = query.withFilter(FilterType.ExtensionName, id);
}
const { galleryExtensions } = await this.queryGallery(query, CURRENT_TARGET_PLATFORM, CancellationToken.None);
const [rawExtension] = galleryExtensions;
if (!rawExtension || !rawExtension.versions.length) {
return null;
}
const allTargetPlatforms = getAllTargetPlatforms(rawExtension);
if (version) {
const versionAsset = rawExtension.versions.filter(v => v.version === version)[0];
if (versionAsset) {
const extension = toExtension(rawExtension, versionAsset, allTargetPlatforms, 0, query);
if (extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version, this.productService.date)) {
return extension;
}
}
return null;
}
const rawVersion = await this.getLastValidExtensionVersion(rawExtension, rawExtension.versions);
if (rawVersion) {
return toExtension(rawExtension, rawVersion, allTargetPlatforms, 0, query);
}
return null;
}
async isExtensionCompatible(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<boolean> {
async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<boolean> {
if (!isTargetPlatformCompatible(extension.properties.targetPlatform, extension.allTargetPlatforms, targetPlatform)) {
return false;
}
if (!includePreRelease && extension.properties.isPreReleaseVersion) {
// Pre-releases are not allowed when include pre-release flag is not set
return false;
}
let engine = extension.properties.engine;
if (!engine) {
const manifest = await this.getManifest(extension, CancellationToken.None);
@@ -626,47 +697,35 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
return isEngineValid(engine, this.productService.version, this.productService.date);
}
private async isRawExtensionVersionCompatible(rawExtensionVersion: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform): Promise<boolean> {
if (!isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(rawExtensionVersion), allTargetPlatforms, targetPlatform)) {
private async isValidVersion(rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform): Promise<boolean> {
if (!isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion), allTargetPlatforms, targetPlatform)) {
return false;
}
const engine = await this.getEngine(rawExtensionVersion);
return isEngineValid(engine, this.productService.version, this.productService.date);
if (versionType !== 'any' && isPreReleaseVersion(rawGalleryExtensionVersion) !== (versionType === 'prerelease')) {
return false;
}
if (compatible) {
const engine = await this.getEngine(rawGalleryExtensionVersion);
if (!isEngineValid(engine, this.productService.version, this.productService.date)) {
return false;
}
}
return true;
}
query(token: CancellationToken): Promise<IPager<IGalleryExtension>>;
query(options: IQueryOptions, token: CancellationToken): Promise<IPager<IGalleryExtension>>;
async query(arg1: any, arg2?: any): Promise<IPager<IGalleryExtension>> {
const options: IQueryOptions = CancellationToken.isCancellationToken(arg1) ? {} : arg1;
const token: CancellationToken = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2;
async query(options: IQueryOptions, token: CancellationToken): Promise<IPager<IGalleryExtension>> {
if (!this.isEnabled()) {
throw new Error('No extension gallery service configured.');
}
const type = options.names ? 'ids' : (options.text ? 'text' : 'all');
let text = options.text || '';
const pageSize = getOrDefault(options, o => o.pageSize, 50);
type GalleryServiceQueryClassification = {
type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
text: { classification: 'CustomerContent', purpose: 'FeatureInsight' };
};
type GalleryServiceQueryEvent = {
type: string;
text: string;
};
this.telemetryService.publicLog2<GalleryServiceQueryEvent, GalleryServiceQueryClassification>('galleryService:query', { type, text });
let query = new Query()
.withFlags(Flags.IncludeLatestVersionOnly, Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, pageSize)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code');
if (options.excludeFlags) {
query = query.withFilter(FilterType.ExcludeWithFlags, options.excludeFlags); // {{SQL CARBON EDIT}} exclude extensions matching excludeFlags options
}
.withPage(1, pageSize);
if (text) {
// Use category filter instead of "category:themes"
@@ -711,27 +770,161 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
query = query.withSortOrder(options.sortOrder);
}
const { galleryExtensions, total } = await this.queryGallery(query, CURRENT_TARGET_PLATFORM, token);
const extensions = galleryExtensions.map((e, index) => toExtensionWithLatestVersion(e, index, query, options.source, CURRENT_TARGET_PLATFORM));
if (options.source) {
query = query.withSource(options.source);
}
const runQuery = async (query: Query, token: CancellationToken) => {
const { extensions, total } = await this.queryGalleryExtensions(query, { targetPlatform: CURRENT_TARGET_PLATFORM, compatible: false, includePreRelease: !!options.includePreRelease }, token);
extensions.forEach((e, index) => setTelemetry(e, ((query.pageNumber - 1) * query.pageSize) + index, options.source));
return { extensions, total };
};
const { extensions, total } = await runQuery(query, token);
const getPage = async (pageIndex: number, ct: CancellationToken) => {
if (ct.isCancellationRequested) {
throw canceled();
throw new CancellationError();
}
const nextPageQuery = query.withPage(pageIndex + 1);
const { galleryExtensions } = await this.queryGallery(nextPageQuery, CURRENT_TARGET_PLATFORM, ct);
return galleryExtensions.map((e, index) => toExtensionWithLatestVersion(e, index, nextPageQuery, options.source, CURRENT_TARGET_PLATFORM));
const { extensions } = await runQuery(query.withPage(pageIndex + 1), ct);
return extensions;
};
// {{SQL CARBON EDIT}}
return { firstPage: extensions, total, pageSize: extensions.length, getPage } as IPager<IGalleryExtension>;
}
private async queryGalleryExtensions(query: Query, criteria: IExtensionCriteria, token: CancellationToken): Promise<{ extensions: IGalleryExtension[]; total: number }> {
const flags = query.flags;
/**
* If both version flags (IncludeLatestVersionOnly and IncludeVersions) are included, then only include latest versions (IncludeLatestVersionOnly) flag.
*/
if (!!(query.flags & Flags.IncludeLatestVersionOnly) && !!(query.flags & Flags.IncludeVersions)) {
query = query.withFlags(query.flags & ~Flags.IncludeVersions, Flags.IncludeLatestVersionOnly);
}
/**
* If version flags (IncludeLatestVersionOnly and IncludeVersions) are not included, default is to query for latest versions (IncludeLatestVersionOnly).
*/
if (!(query.flags & Flags.IncludeLatestVersionOnly) && !(query.flags & Flags.IncludeVersions)) {
query = query.withFlags(query.flags, Flags.IncludeLatestVersionOnly);
}
/**
* If versions criteria exist, then remove IncludeLatestVersionOnly flag and add IncludeVersions flag.
*/
if (criteria.versions?.length) {
query = query.withFlags(query.flags & ~Flags.IncludeLatestVersionOnly, Flags.IncludeVersions);
}
/**
* Add necessary extension flags
*/
query = query.withFlags(query.flags, Flags.IncludeAssetUri, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeStatistics, Flags.IncludeVersionProperties);
const { galleryExtensions: rawGalleryExtensions, total } = await this.queryRawGalleryExtensions(query, token);
const hasAllVersions: boolean = !(query.flags & Flags.IncludeLatestVersionOnly);
if (hasAllVersions) {
const extensions: IGalleryExtension[] = [];
for (const rawGalleryExtension of rawGalleryExtensions) {
const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria);
if (extension) {
extensions.push(extension);
}
}
return { extensions, total };
}
const result: [number, IGalleryExtension][] = [];
const needAllVersions = new Map<string, number>();
for (let index = 0; index < rawGalleryExtensions.length; index++) {
const rawGalleryExtension = rawGalleryExtensions[index];
const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId };
const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease;
if (criteria.compatible && isNotWebExtensionInWebTargetPlatform(getAllTargetPlatforms(rawGalleryExtension), criteria.targetPlatform)) {
/** Skip if requested for a web-compatible extension and it is not a web extension.
* All versions are not needed in this case
*/
continue;
}
const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria);
if (!extension
/** Need all versions if the extension is a pre-release version but
* - the query is to look for a release version or
* - the extension has no release version
* Get all versions to get or check the release version
*/
|| (extension.properties.isPreReleaseVersion && (!includePreRelease || !extension.hasReleaseVersion))
/**
* Need all versions if the extension is a release version with a different target platform than requested and also has a pre-release version
* Because, this is a platform specific extension and can have a newer release version supporting this platform.
* See https://github.com/microsoft/vscode/issues/139628
*/
|| (!extension.properties.isPreReleaseVersion && extension.properties.targetPlatform !== criteria.targetPlatform && extension.hasPreReleaseVersion)
) {
needAllVersions.set(rawGalleryExtension.extensionId, index);
} else {
result.push([index, extension]);
}
}
if (needAllVersions.size) {
const query = new Query()
.withFlags(flags & ~Flags.IncludeLatestVersionOnly, Flags.IncludeVersions)
.withPage(1, needAllVersions.size)
.withFilter(FilterType.ExtensionId, ...needAllVersions.keys());
const { extensions } = await this.queryGalleryExtensions(query, criteria, token);
for (const extension of extensions) {
const index = needAllVersions.get(extension.identifier.uuid)!;
result.push([index, extension]);
}
}
return { extensions: result.sort((a, b) => a[0] - b[0]).map(([, extension]) => extension), total };
}
private async toGalleryExtensionWithCriteria(rawGalleryExtension: IRawGalleryExtension, criteria: IExtensionCriteria): Promise<IGalleryExtension | null> {
const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId };
const version = criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version;
const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease;
const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension);
const rawGalleryExtensionVersions = sortExtensionVersions(rawGalleryExtension.versions, criteria.targetPlatform);
if (criteria.compatible && isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, criteria.targetPlatform)) {
return null;
}
for (let index = 0; index < rawGalleryExtensionVersions.length; index++) {
const rawGalleryExtensionVersion = rawGalleryExtensionVersions[index];
if (version && rawGalleryExtensionVersion.version !== version) {
continue;
}
// Allow any version if includePreRelease flag is set otherwise only release versions are allowed
if (await this.isValidVersion(rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform)) {
return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms);
}
if (version && rawGalleryExtensionVersion.version === version) {
return null;
}
}
if (version || criteria.compatible) {
return null;
}
/**
* Fallback: Return the latest version
* This can happen when the extension does not have a release version or does not have a version compatible with the given target platform.
*/
return toExtension(rawGalleryExtension, rawGalleryExtension.versions[0], allTargetPlatforms);
}
// {{SQL CARBON EDIT}}
/**
* The result of querying the gallery returns all the extensions because it's only reading a static file.
* So this method should apply all the filters and return the actual result
*/
private createQueryResult(query: Query, galleryExtensions: IRawGalleryExtension[]): { galleryExtensions: IRawGalleryExtension[], total: number; } {
private createQueryResult(query: Query, galleryExtensions: IRawGalleryExtension[]): { galleryExtensions: IRawGalleryExtension[]; total: number } {
// Filtering
let filteredExtensions = galleryExtensions;
@@ -752,7 +945,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
// we only have 1 version for our extensions in the gallery file, so this should always be the case
if (e.versions.length === 1) {
const allTargetPlatforms = getAllTargetPlatforms(e);
const extension = toExtension(e, e.versions[0], allTargetPlatforms, 0, query);
const extension = toExtension(e, e.versions[0], allTargetPlatforms);
return extension.properties.localizedLanguages && extension.properties.localizedLanguages.length > 0;
}
return false;
@@ -841,13 +1034,15 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
return a[fieldName] < b[fieldName] ? -1 : 1;
}
private async queryGallery(query: Query, targetPlatform: TargetPlatform, token: CancellationToken): Promise<{ galleryExtensions: IRawGalleryExtension[], total: number; }> {
private async queryRawGalleryExtensions(query: Query, token: CancellationToken): Promise<{ galleryExtensions: IRawGalleryExtension[]; total: number }> {
if (!this.isEnabled()) {
throw new Error('No extension gallery service configured.');
}
// Always exclude non validated and unpublished extensions
query = query
/* Always exclude non validated extensions */
.withFlags(query.flags, Flags.ExcludeNonValidated)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code')
/* Always exclude unpublished extensions */
.withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished));
const commonHeaders = await this.commonHeadersPromise;
@@ -878,13 +1073,13 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
if (result) {
const r = result.results[0];
const galleryExtensions = r.extensions;
galleryExtensions.forEach(e => sortExtensionVersions(e.versions, targetPlatform));
// {{SQL CARBON TODO}}
galleryExtensions.forEach(e => sortExtensionVersions(e.versions, TargetPlatform.UNIVERSAL));
// const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0]; {{SQL CARBON EDIT}} comment out for no unused
// const total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0; {{SQL CARBON EDIT}} comment out for no unused
// {{SQL CARBON EDIT}}
let filteredExtensionsResult = this.createQueryResult(query, galleryExtensions);
return { galleryExtensions: filteredExtensionsResult.galleryExtensions, total: filteredExtensionsResult.total };
// {{SQL CARBON EDIT}} - End
}
@@ -940,7 +1135,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise<string> {
if (extension.assets.readme) {
const context = await this.getAsset(extension.assets.readme, {}, token);
const content = await asText(context);
const content = await asTextOrError(context);
return content || '';
}
return '';
@@ -949,7 +1144,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
async getManifest(extension: IGalleryExtension, token: CancellationToken): Promise<IExtensionManifest | null> {
if (extension.assets.manifest) {
const context = await this.getAsset(extension.assets.manifest, {}, token);
const text = await asText(context);
const text = await asTextOrError(context);
return text ? JSON.parse(text) : null;
}
return null;
@@ -969,7 +1164,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
const asset = extension.assets.coreTranslations.filter(t => t[0] === languageId.toUpperCase())[0];
if (asset) {
const context = await this.getAsset(asset[1]);
const text = await asText(context);
const text = await asTextOrError(context);
return text ? JSON.parse(text) : null;
}
return null;
@@ -978,17 +1173,16 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
async getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise<string> {
if (extension.assets.changelog) {
const context = await this.getAsset(extension.assets.changelog, {}, token);
const content = await asText(context);
const content = await asTextOrError(context);
return content || '';
}
return '';
}
async getAllCompatibleVersions(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]> {
async getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]> {
let query = new Query()
.withFlags(Flags.IncludeVersions, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, 1)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code');
.withPage(1, 1);
if (extension.identifier.uuid) {
query = query.withFilter(FilterType.ExtensionId, extension.identifier.uuid);
@@ -996,7 +1190,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
query = query.withFilter(FilterType.ExtensionName, extension.identifier.id);
}
const { galleryExtensions } = await this.queryGallery(query, targetPlatform, CancellationToken.None);
const { galleryExtensions } = await this.queryRawGalleryExtensions(query, CancellationToken.None);
if (!galleryExtensions.length) {
return [];
}
@@ -1006,14 +1200,24 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
return [];
}
const result: IGalleryExtensionVersion[] = [];
for (const version of galleryExtensions[0].versions) {
const validVersions: IRawGalleryExtensionVersion[] = [];
await Promise.all(galleryExtensions[0].versions.map(async (version) => {
try {
if (result[result.length - 1]?.version !== version.version && await this.isRawExtensionVersionCompatible(version, allTargetPlatforms, targetPlatform)) {
result.push({ version: version.version, date: version.lastUpdated });
if (await this.isValidVersion(version, includePreRelease ? 'any' : 'release', true, allTargetPlatforms, targetPlatform)) {
validVersions.push(version);
}
} catch (error) { /* Ignore error and skip version */ }
}));
const result: IGalleryExtensionVersion[] = [];
const seen = new Set<string>();
for (const version of sortExtensionVersions(validVersions, targetPlatform)) {
if (!seen.has(version.version)) {
seen.add(version.version);
result.push({ version: version.version, date: version.lastUpdated, isPreReleaseVersion: isPreReleaseVersion(version) });
}
}
return result;
}
@@ -1032,17 +1236,17 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
if (context.res.statusCode === 200) {
return context;
}
const message = await asText(context);
const message = await asTextOrError(context);
throw new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`);
} catch (err) {
if (isPromiseCanceledError(err)) {
if (isCancellationError(err)) {
throw err;
}
const message = getErrorMessage(err);
type GalleryServiceCDNFallbackClassification = {
url: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
message: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
url: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
message: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
};
type GalleryServiceCDNFallbackEvent = {
url: string;
@@ -1054,31 +1258,31 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
return this.requestService.request(fallbackOptions, token);
}
}
private async getLastValidExtensionVersion(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): Promise<IRawGalleryExtensionVersion | null> {
const version = this.getLastValidExtensionVersionFromProperties(extension, versions);
if (version) {
return version;
}
return this.getLastValidExtensionVersionRecursively(extension, versions);
}
// private async getLastValidExtensionVersion(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): Promise<IRawGalleryExtensionVersion | null> {
// const version = this.getLastValidExtensionVersionFromProperties(extension, versions);
// if (version) {
// return version;
// }
// return this.getLastValidExtensionVersionRecursively(extension, versions);
// }
private getLastValidExtensionVersionFromProperties(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): IRawGalleryExtensionVersion | null {
for (const version of versions) {
// {{SQL CARBON EDIT}}
const vsCodeEngine = getEngine(version);
const azDataEngine = getAzureDataStudioEngine(version);
// Require at least one engine version
if (!vsCodeEngine && !azDataEngine) {
return null;
}
const vsCodeEngineValid = !vsCodeEngine || (vsCodeEngine && isEngineValid(vsCodeEngine, this.productService.vscodeVersion, this.productService.date));
const azDataEngineValid = !azDataEngine || (azDataEngine && isEngineValid(azDataEngine, this.productService.version, this.productService.date));
if (vsCodeEngineValid && azDataEngineValid) {
return version;
}
}
return null;
}
// private getLastValidExtensionVersionFromProperties(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): IRawGalleryExtensionVersion | null {
// for (const version of versions) {
// // {{SQL CARBON EDIT}}
// const vsCodeEngine = getEngine(version);
// const azDataEngine = getAzureDataStudioEngine(version);
// // Require at least one engine version
// if (!vsCodeEngine && !azDataEngine) {
// return null;
// }
// const vsCodeEngineValid = !vsCodeEngine || (vsCodeEngine && isEngineValid(vsCodeEngine, this.productService.vscodeVersion, this.productService.date));
// const azDataEngineValid = !azDataEngine || (azDataEngine && isEngineValid(azDataEngine, this.productService.version, this.productService.date));
// if (vsCodeEngineValid && azDataEngineValid) {
// return version;
// }
// }
// return null;
// }
private async getEngine(rawExtensionVersion: IRawGalleryExtensionVersion): Promise<string> {
let engine = getEngine(rawExtensionVersion);
@@ -1092,30 +1296,30 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
return engine;
}
private async getLastValidExtensionVersionRecursively(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): Promise<IRawGalleryExtensionVersion | null> {
if (!versions.length) {
return null;
}
// private async getLastValidExtensionVersionRecursively(extension: IRawGalleryExtension, versions: IRawGalleryExtensionVersion[]): Promise<IRawGalleryExtensionVersion | null> {
// if (!versions.length) {
// return null;
// }
const version = versions[0];
const engine = await this.getEngine(version);
if (!isEngineValid(engine, this.productService.version, this.productService.date)) {
return this.getLastValidExtensionVersionRecursively(extension, versions.slice(1));
}
// const version = versions[0];
// const engine = await this.getEngine(version);
// if (!isEngineValid(engine, this.productService.version, this.productService.date)) {
// return this.getLastValidExtensionVersionRecursively(extension, versions.slice(1));
// }
return {
...version,
properties: [...(version.properties || []), { key: PropertyType.Engine, value: engine }]
};
}
// return {
// ...version,
// properties: [...(version.properties || []), { key: PropertyType.Engine, value: engine }]
// };
// }
async getExtensionsReport(): Promise<IReportedExtension[]> {
async getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
if (!this.isEnabled()) {
throw new Error('No extension gallery service configured.');
}
if (!this.extensionsControlUrl) {
return [];
return { malicious: [] };
}
const context = await this.requestService.request({ type: 'GET', url: this.extensionsControlUrl }, CancellationToken.None);
@@ -1123,18 +1327,32 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
throw new Error('Could not get extensions report.');
}
const result = await asJson<IRawExtensionsReport>(context);
const map = new Map<string, IReportedExtension>();
const result = await asJson<IRawExtensionsControlManifest>(context);
const malicious: IExtensionIdentifier[] = [];
const unsupportedPreReleaseExtensions: IStringDictionary<{ id: string; displayName: string; migrateStorage?: boolean }> = {};
if (result) {
for (const id of result.malicious) {
const ext = map.get(id) || { id: { id }, malicious: true, slow: false };
ext.malicious = true;
map.set(id, ext);
malicious.push({ id });
}
if (result.unsupported) {
for (const extensionId of Object.keys(result.unsupported)) {
const value = result.unsupported[extensionId];
if (!isBoolean(value)) {
unsupportedPreReleaseExtensions[extensionId.toLowerCase()] = value.preReleaseExtension;
}
}
}
if (result.migrateToPreRelease) {
for (const [unsupportedPreReleaseExtensionId, preReleaseExtensionInfo] of Object.entries(result.migrateToPreRelease)) {
if (!preReleaseExtensionInfo.engine || isEngineValid(preReleaseExtensionInfo.engine, this.productService.version, this.productService.date)) {
unsupportedPreReleaseExtensions[unsupportedPreReleaseExtensionId.toLowerCase()] = preReleaseExtensionInfo;
}
}
}
}
return [...map.values()];
return { malicious, unsupportedPreReleaseExtensions };
}
}
@@ -1168,18 +1386,3 @@ export class ExtensionGalleryServiceWithNoStorageService extends AbstractExtensi
super(undefined, requestService, logService, environmentService, telemetryService, fileService, productService, configurationService);
}
}
export async function resolveMarketplaceHeaders(version: string, productService: IProductService, environmentService: IEnvironmentService, configurationService: IConfigurationService, fileService: IFileService, storageService: {
get: (key: string, scope: StorageScope) => string | undefined,
store: (key: string, value: string, scope: StorageScope, target: StorageTarget) => void
} | undefined): Promise<{ [key: string]: string; }> {
const headers: IHeaders = {
'X-Market-Client-Id': `VSCode ${version}`,
'User-Agent': `VSCode ${version}`
};
const uuid = await getServiceMachineId(environmentService, fileService, storageService);
if (supportsTelemetry(productService, environmentService) && getTelemetryLevel(configurationService) === TelemetryLevel.USAGE) {
headers['X-Market-User-Id'] = uuid;
}
return headers;
}

View File

@@ -4,41 +4,19 @@
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStringDictionary } from 'vs/base/common/collections';
import { Event } from 'vs/base/common/event';
import { FileAccess } from 'vs/base/common/network';
import { IPager } from 'vs/base/common/paging';
import { Platform } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { ExtensionType, IExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { ExtensionType, IExtension, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const EXTENSION_IDENTIFIER_PATTERN = '^([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$';
export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTERN);
export const WEB_EXTENSION_TAG = '__web_extension';
export const enum TargetPlatform {
WIN32_X64 = 'win32-x64',
WIN32_IA32 = 'win32-ia32',
WIN32_ARM64 = 'win32-arm64',
LINUX_X64 = 'linux-x64',
LINUX_ARM64 = 'linux-arm64',
LINUX_ARMHF = 'linux-armhf',
ALPINE_X64 = 'alpine-x64',
ALPINE_ARM64 = 'alpine-arm64',
DARWIN_X64 = 'darwin-x64',
DARWIN_ARM64 = 'darwin-arm64',
WEB = 'web',
UNIVERSAL = 'universal',
UNKNOWN = 'unknown',
UNDEFINED = 'undefined',
}
export function TargetPlatformToString(targetPlatform: TargetPlatform) {
switch (targetPlatform) {
case TargetPlatform.WIN32_X64: return 'Windows 64 bit';
@@ -186,6 +164,7 @@ export interface IGalleryExtensionProperties {
azDataEngine?: string;
localizedLanguages?: string[];
targetPlatform: TargetPlatform;
isPreReleaseVersion: boolean;
}
export interface IGalleryExtensionAsset {
@@ -202,7 +181,7 @@ export interface IGalleryExtensionAssets {
download: IGalleryExtensionAsset;
// {{SQL CARBON EDIT}}
downloadPage?: IGalleryExtensionAsset;
icon: IGalleryExtensionAsset;
icon: IGalleryExtensionAsset | null;
coreTranslations: [string, IGalleryExtensionAsset][];
}
@@ -213,23 +192,11 @@ export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifie
&& (!thing.uuid || typeof thing.uuid === 'string');
}
/* __GDPR__FRAGMENT__
"ExtensionIdentifier" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"uuid": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
export interface IExtensionIdentifier {
id: string;
uuid?: string;
}
export interface IExtensionIdentifierWithVersion extends IExtensionIdentifier {
id: string;
uuid?: string;
version: string;
}
export interface IGalleryExtensionIdentifier extends IExtensionIdentifier {
uuid: string;
}
@@ -237,6 +204,7 @@ export interface IGalleryExtensionIdentifier extends IExtensionIdentifier {
export interface IGalleryExtensionVersion {
version: string;
date: string;
isPreReleaseVersion: boolean;
}
export interface IGalleryExtension {
@@ -247,7 +215,7 @@ export interface IGalleryExtension {
publisherId: string;
publisher: string;
publisherDisplayName: string;
publisherDomain?: { link: string, verified: boolean };
publisherDomain?: { link: string; verified: boolean };
description: string;
installCount: number;
rating: number;
@@ -257,23 +225,32 @@ export interface IGalleryExtension {
releaseDate: number;
lastUpdated: number;
preview: boolean;
hasPreReleaseVersion: boolean;
hasReleaseVersion: boolean;
allTargetPlatforms: TargetPlatform[];
assets: IGalleryExtensionAssets;
properties: IGalleryExtensionProperties;
telemetryData: any;
telemetryData?: any;
}
export interface IGalleryMetadata {
id: string;
publisherId: string;
publisherDisplayName: string;
isPreReleaseVersion: boolean;
targetPlatform?: TargetPlatform;
}
export type Metadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean; isSystem: boolean; updated: boolean; preRelease: boolean; installedTimestamp: number }>;
export interface ILocalExtension extends IExtension {
isMachineScoped: boolean;
publisherId: string | null;
publisherDisplayName: string | null;
installedTimestamp?: number;
isPreReleaseVersion: boolean;
preRelease: boolean;
updated: boolean;
}
export const enum SortBy {
@@ -301,6 +278,7 @@ export interface IQueryOptions {
sortBy?: SortBy;
sortOrder?: SortOrder;
source?: string;
includePreRelease?: boolean;
// {{SQL CARBON EDIT}} do not show extensions matching excludeFlags in the marketplace
// This field only supports an exact match of a single flag. It doesn't currently
// support setting multiple flags such as "hidden,preview" since this functionality isn't
@@ -313,39 +291,52 @@ export const enum StatisticType {
Uninstall = 'uninstall'
}
export interface IReportedExtension {
id: IExtensionIdentifier;
malicious: boolean;
export interface IExtensionsControlManifest {
malicious: IExtensionIdentifier[];
unsupportedPreReleaseExtensions?: IStringDictionary<{ id: string; displayName: string; migrateStorage?: boolean }>;
}
export const enum InstallOperation {
None = 0,
None = 1,
Install,
Update
Update,
Migrate,
}
export interface ITranslation {
contents: { [key: string]: {} };
}
export interface IExtensionInfo extends IExtensionIdentifier {
version?: string;
preRelease?: boolean;
hasPreRelease?: boolean;
}
export interface IExtensionQueryOptions {
targetPlatform?: TargetPlatform;
compatible?: boolean;
queryAllVersions?: boolean;
source?: string;
}
export const IExtensionGalleryService = createDecorator<IExtensionGalleryService>('extensionGalleryService');
export interface IExtensionGalleryService {
readonly _serviceBrand: undefined;
isEnabled(): boolean;
query(token: CancellationToken): Promise<IPager<IGalleryExtension>>;
query(options: IQueryOptions, token: CancellationToken): Promise<IPager<IGalleryExtension>>;
getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, token: CancellationToken): Promise<IGalleryExtension[]>;
getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, token: CancellationToken): Promise<IGalleryExtension[]>;
getExtensions(extensionInfos: ReadonlyArray<IExtensionInfo>, options: IExtensionQueryOptions, token: CancellationToken): Promise<IGalleryExtension[]>;
isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<boolean>;
getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]>;
download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise<void>;
reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise<void>;
getReadme(extension: IGalleryExtension, token: CancellationToken): Promise<string>;
getManifest(extension: IGalleryExtension, token: CancellationToken): Promise<IExtensionManifest | null>;
getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise<string>;
getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise<ITranslation | null>;
getExtensionsReport(): Promise<IReportedExtension[]>;
isExtensionCompatible(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<boolean>;
getCompatibleExtension(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
getCompatibleExtension(id: IExtensionIdentifier, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
getAllCompatibleVersions(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]>;
getExtensionsControlManifest(): Promise<IExtensionsControlManifest>;
}
export interface InstallExtensionEvent {
@@ -367,8 +358,12 @@ export interface DidUninstallExtensionEvent {
export enum ExtensionManagementErrorCode {
Unsupported = 'Unsupported',
UnsupportedPreRelease = 'UnsupportedPreRelease',
Malicious = 'Malicious',
Incompatible = 'Incompatible',
IncompatiblePreRelease = 'IncompatiblePreRelease',
IncompatibleTargetPlatform = 'IncompatibleTargetPlatform',
ReleaseVersionNotFound = 'ReleaseVersionNotFound',
Invalid = 'Invalid',
Download = 'Download',
Extract = 'Extract',
@@ -386,9 +381,9 @@ export class ExtensionManagementError extends Error {
}
}
export type InstallOptions = { isBuiltin?: boolean, isMachineScoped?: boolean, donotIncludePackAndDependencies?: boolean, installGivenVersion?: boolean };
export type InstallOptions = { isBuiltin?: boolean; isMachineScoped?: boolean; donotIncludePackAndDependencies?: boolean; installGivenVersion?: boolean; installPreReleaseVersion?: boolean; operation?: InstallOperation };
export type InstallVSIXOptions = Omit<InstallOptions, 'installGivenVersion'> & { installOnlyNewlyAddedFromExtensionPack?: boolean };
export type UninstallOptions = { donotIncludePack?: boolean, donotCheckDependents?: boolean };
export type UninstallOptions = { donotIncludePack?: boolean; donotCheckDependents?: boolean };
export interface IExtensionManagementParticipant {
postInstall(local: ILocalExtension, source: URI | IGalleryExtension, options: InstallOptions | InstallVSIXOptions, token: CancellationToken): Promise<void>;
@@ -412,8 +407,8 @@ export interface IExtensionManagementService {
installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise<ILocalExtension>;
uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise<void>;
reinstallFromGallery(extension: ILocalExtension): Promise<void>;
getInstalled(type?: ExtensionType, donotIgnoreInvalidExtensions?: boolean): Promise<ILocalExtension[]>;
getExtensionsReport(): Promise<IReportedExtension[]>;
getInstalled(type?: ExtensionType): Promise<ILocalExtension[]>;
getExtensionsControlManifest(): Promise<IExtensionsControlManifest>;
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension>;
updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise<ILocalExtension>;
@@ -428,7 +423,7 @@ export const IGlobalExtensionEnablementService = createDecorator<IGlobalExtensio
export interface IGlobalExtensionEnablementService {
readonly _serviceBrand: undefined;
readonly onDidChangeEnablement: Event<{ readonly extensions: IExtensionIdentifier[], readonly source?: string }>;
readonly onDidChangeEnablement: Event<{ readonly extensions: IExtensionIdentifier[]; readonly source?: string }>;
getDisabledExtensions(): IExtensionIdentifier[];
enableExtension(extension: IExtensionIdentifier, source?: string): Promise<boolean>;
@@ -437,23 +432,25 @@ export interface IGlobalExtensionEnablementService {
}
export type IConfigBasedExtensionTip = {
readonly extensionId: string,
readonly extensionName: string,
readonly isExtensionPack: boolean,
readonly configName: string,
readonly important: boolean,
readonly extensionId: string;
readonly extensionName: string;
readonly isExtensionPack: boolean;
readonly configName: string;
readonly important: boolean;
readonly whenNotInstalled?: string[];
};
export type IExecutableBasedExtensionTip = {
readonly extensionId: string,
readonly extensionName: string,
readonly isExtensionPack: boolean,
readonly exeName: string,
readonly exeFriendlyName: string,
readonly windowsPath?: string,
readonly extensionId: string;
readonly extensionName: string;
readonly isExtensionPack: boolean;
readonly exeName: string;
readonly exeFriendlyName: string;
readonly windowsPath?: string;
readonly whenNotInstalled?: string[];
};
export type IWorkspaceTips = { readonly remoteSet: string[]; readonly recommendations: string[]; };
export type IWorkspaceTips = { readonly remoteSet: string[]; readonly recommendations: string[] };
export const IExtensionTipsService = createDecorator<IExtensionTipsService>('IExtensionTipsService');
export interface IExtensionTipsService {
@@ -466,7 +463,6 @@ export interface IExtensionTipsService {
}
export const DefaultIconPath = FileAccess.asBrowserUri('./media/defaultIcon.png', require).toString(true);
export const ExtensionsLabel = localize('extensions', "Extensions");
export const ExtensionsLocalizedLabel = { value: ExtensionsLabel, original: 'Extensions' };
export const ExtensionsChannelId = 'extensions';
@@ -484,7 +480,7 @@ export interface IExtensionManagementCLIService {
readonly _serviceBrand: undefined;
listExtensions(showVersions: boolean, category?: string, output?: CLIOutput): Promise<void>;
installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output?: CLIOutput): Promise<void>;
installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], installOptions: InstallOptions, force: boolean, output?: CLIOutput): Promise<void>;
uninstallExtensions(extensions: (string | URI)[], force: boolean, output?: CLIOutput): Promise<void>;
locateExtension(extensions: string[], output?: CLIOutput): Promise<void>;
}

View File

@@ -4,14 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { isCancellationError } from 'vs/base/common/errors';
import { getBaseLabel } from 'vs/base/common/labels';
import { Schemas } from 'vs/base/common/network';
import { gt } from 'vs/base/common/semver/semver';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { CLIOutput, IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { areSameExtensions, getGalleryExtensionId, getIdAndVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
@@ -27,17 +27,7 @@ function getId(manifest: IExtensionManifest, withVersion?: boolean): string {
}
}
const EXTENSION_ID_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/;
export function getIdAndVersion(id: string): [string, string | undefined] {
const matches = EXTENSION_ID_REGEX.exec(id);
if (matches && matches[1]) {
return [adoptToGalleryExtensionId(matches[1]), matches[2]];
}
return [adoptToGalleryExtensionId(id), undefined];
}
type InstallExtensionInfo = { id: string, version?: string, installOptions: InstallOptions };
type InstallExtensionInfo = { id: string; version?: string; installOptions: InstallOptions };
export class ExtensionManagementCLIService implements IExtensionManagementCLIService {
@@ -89,7 +79,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
}
}
public async installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output: CLIOutput = console): Promise<void> {
public async installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], installOptions: InstallOptions, force: boolean, output: CLIOutput = console): Promise<void> {
const failed: string[] = [];
const installedExtensionsManifests: IExtensionManifest[] = [];
if (extensions.length) {
@@ -100,7 +90,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
const checkIfNotInstalled = (id: string, version?: string): boolean => {
const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id }));
if (installedExtension) {
if (!version && !force) {
if (!force && (!version || (version === 'prerelease' && installedExtension.preRelease))) {
output.log(localize('alreadyInstalled-checkAndUpdate', "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@<version>' to install a specific version, for example: '{2}@1.2.3'.", id, installedExtension.manifest.version, id));
return false;
}
@@ -111,6 +101,9 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
}
return true;
};
const addInstallExtensionInfo = (id: string, version: string | undefined, isBuiltin: boolean) => {
installExtensionInfos.push({ id, version: version !== 'prerelease' ? version : undefined, installOptions: { ...installOptions, isBuiltin, installPreReleaseVersion: version === 'prerelease' || installOptions.installPreReleaseVersion } });
};
const vsixs: URI[] = [];
const installExtensionInfos: InstallExtensionInfo[] = [];
for (const extension of extensions) {
@@ -119,21 +112,21 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
} else {
const [id, version] = getIdAndVersion(extension);
if (checkIfNotInstalled(id, version)) {
installExtensionInfos.push({ id, version, installOptions: { isBuiltin: false, isMachineScoped } });
addInstallExtensionInfo(id, version, false);
}
}
}
for (const extension of builtinExtensionIds) {
const [id, version] = getIdAndVersion(extension);
if (checkIfNotInstalled(id, version)) {
installExtensionInfos.push({ id, version, installOptions: { isBuiltin: true, isMachineScoped: false } });
addInstallExtensionInfo(id, version, true);
}
}
if (vsixs.length) {
await Promise.all(vsixs.map(async vsix => {
try {
const manifest = await this.installVSIX(vsix, { isBuiltin: false, isMachineScoped }, force, output);
const manifest = await this.installVSIX(vsix, { ...installOptions, isBuiltin: false }, force, output);
if (manifest) {
installedExtensionsManifests.push(manifest);
}
@@ -187,7 +180,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
output.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(vsix)));
return manifest;
} catch (error) {
if (isPromiseCanceledError(error)) {
if (isCancellationError(error)) {
output.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", getBaseLabel(vsix)));
return null;
} else {
@@ -200,7 +193,8 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise<Map<string, IGalleryExtension>> {
const galleryExtensions = new Map<string, IGalleryExtension>();
const result = await this.extensionGalleryService.getExtensions(extensions, CancellationToken.None);
const preRelease = extensions.some(e => e.installOptions.installPreReleaseVersion);
const result = await this.extensionGalleryService.getExtensions(extensions.map(e => ({ ...e, preRelease })), CancellationToken.None);
for (const extension of result) {
galleryExtensions.set(extension.identifier.id.toLowerCase(), extension);
}
@@ -233,7 +227,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
output.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, galleryExtension.version));
return manifest;
} catch (error) {
if (isPromiseCanceledError(error)) {
if (isCancellationError(error)) {
output.log(localize('cancelInstall', "Cancelled installing extension '{0}'.", id));
return null;
} else {

View File

@@ -9,8 +9,8 @@ import { cloneAndChange } from 'vs/base/common/objects';
import { URI, UriComponents } from 'vs/base/common/uri';
import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc';
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { DidUninstallExtensionEvent, IExtensionIdentifier, IExtensionManagementService, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOptions, InstallVSIXOptions, IReportedExtension, isTargetPlatformCompatible, TargetPlatform, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { DidUninstallExtensionEvent, IExtensionIdentifier, IExtensionManagementService, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOptions, InstallVSIXOptions, IExtensionsControlManifest, isTargetPlatformCompatible, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI {
return URI.revive(transformer ? transformer.transformIncoming(uri) : uri);
@@ -72,7 +72,7 @@ export class ExtensionManagementChannel implements IServerChannel {
case 'getInstalled': return this.service.getInstalled(args[0]).then(extensions => extensions.map(e => transformOutgoingExtension(e, uriTransformer)));
case 'updateMetadata': return this.service.updateMetadata(transformIncomingExtension(args[0], uriTransformer), args[1]).then(e => transformOutgoingExtension(e, uriTransformer));
case 'updateExtensionScope': return this.service.updateExtensionScope(transformIncomingExtension(args[0], uriTransformer), args[1]).then(e => transformOutgoingExtension(e, uriTransformer));
case 'getExtensionsReport': return this.service.getExtensionsReport();
case 'getExtensionsControlManifest': return this.service.getExtensionsControlManifest();
}
throw new Error('Invalid call');
@@ -113,7 +113,7 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt
typeof (<any>thing).scheme === 'string';
}
private _targetPlatformPromise: Promise<TargetPlatform> | undefined;
protected _targetPlatformPromise: Promise<TargetPlatform> | undefined;
getTargetPlatform(): Promise<TargetPlatform> {
if (!this._targetPlatformPromise) {
this._targetPlatformPromise = this.channel.call<TargetPlatform>('getTargetPlatform');
@@ -169,8 +169,8 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt
.then(extension => transformIncomingExtension(extension, null));
}
getExtensionsReport(): Promise<IReportedExtension[]> {
return Promise.resolve(this.channel.call('getExtensionsReport'));
getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
return Promise.resolve(this.channel.call('getExtensionsControlManifest'));
}
registerParticipant() { throw new Error('Not Supported'); }

View File

@@ -4,8 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import { compareIgnoreCase } from 'vs/base/common/strings';
import { IExtensionIdentifier, IExtensionIdentifierWithVersion, IGalleryExtension, ILocalExtension, IReportedExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionIdentifier, IExtension } from 'vs/platform/extensions/common/extensions';
import { IExtensionIdentifier, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, getTargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionIdentifier, IExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { isLinux, platform } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { getErrorMessage } from 'vs/base/common/errors';
import { ILogService } from 'vs/platform/log/common/log';
import { arch } from 'vs/base/common/process';
export function areSameExtensions(a: IExtensionIdentifier, b: IExtensionIdentifier): boolean {
if (a.uuid && b.uuid) {
@@ -17,31 +23,52 @@ export function areSameExtensions(a: IExtensionIdentifier, b: IExtensionIdentifi
return compareIgnoreCase(a.id, b.id) === 0;
}
export class ExtensionIdentifierWithVersion implements IExtensionIdentifierWithVersion {
const ExtensionKeyRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)(-(.+))?$/;
export class ExtensionKey {
static create(extension: IExtension | IGalleryExtension): ExtensionKey {
const version = (extension as IExtension).manifest ? (extension as IExtension).manifest.version : (extension as IGalleryExtension).version;
const targetPlatform = (extension as IExtension).manifest ? (extension as IExtension).targetPlatform : (extension as IGalleryExtension).properties.targetPlatform;
return new ExtensionKey(extension.identifier, version, targetPlatform);
}
static parse(key: string): ExtensionKey | null {
const matches = ExtensionKeyRegex.exec(key);
return matches && matches[1] && matches[2] ? new ExtensionKey({ id: matches[1] }, matches[2], matches[4] as TargetPlatform || undefined) : null;
}
readonly id: string;
readonly uuid?: string;
constructor(
identifier: IExtensionIdentifier,
readonly version: string
readonly version: string,
readonly targetPlatform: TargetPlatform = TargetPlatform.UNDEFINED,
) {
this.id = identifier.id;
this.uuid = identifier.uuid;
}
key(): string {
return `${this.id}-${this.version}`;
toString(): string {
return `${this.id}-${this.version}${this.targetPlatform !== TargetPlatform.UNDEFINED ? `-${this.targetPlatform}` : ''}`;
}
equals(o: any): boolean {
if (!(o instanceof ExtensionIdentifierWithVersion)) {
if (!(o instanceof ExtensionKey)) {
return false;
}
return areSameExtensions(this, o) && this.version === o.version;
return areSameExtensions(this, o) && this.version === o.version && this.targetPlatform === o.targetPlatform;
}
}
const EXTENSION_IDENTIFIER_WITH_VERSION_REGEX = /^([^.]+\..+)@((prerelease)|(\d+\.\d+\.\d+(-.*)?))$/;
export function getIdAndVersion(id: string): [string, string | undefined] {
const matches = EXTENSION_IDENTIFIER_WITH_VERSION_REGEX.exec(id);
if (matches && matches[1]) {
return [adoptToGalleryExtensionId(matches[1]), matches[2]];
}
return [adoptToGalleryExtensionId(id), undefined];
}
export function getExtensionId(publisher: string, name: string): string {
return `${publisher}.${name}`;
}
@@ -97,6 +124,7 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension): any
"publisherId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"publisherName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"publisherDisplayName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"isPreReleaseVersion": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"dependencies": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"${include}": [
"${GalleryExtensionTelemetryData2}"
@@ -111,6 +139,7 @@ export function getGalleryExtensionTelemetryData(extension: IGalleryExtension):
publisherId: extension.publisherId,
publisherName: extension.publisher,
publisherDisplayName: extension.publisherDisplayName,
isPreReleaseVersion: extension.properties.isPreReleaseVersion,
dependencies: !!(extension.properties.dependencies && extension.properties.dependencies.length > 0),
// {{SQL CARBON EDIT}}
extensionVersion: extension.version,
@@ -120,12 +149,12 @@ export function getGalleryExtensionTelemetryData(extension: IGalleryExtension):
export const BetterMergeId = new ExtensionIdentifier('pprice.better-merge');
export function getMaliciousExtensionsSet(report: IReportedExtension[]): Set<string> {
export function getMaliciousExtensionsSet(manifest: IExtensionsControlManifest): Set<string> {
const result = new Set<string>();
for (const extension of report) {
if (extension.malicious) {
result.add(extension.id.id);
if (manifest.malicious) {
for (const extension of manifest.malicious) {
result.add(extension.id);
}
}
@@ -150,3 +179,30 @@ export function getExtensionDependencies(installedExtensions: ReadonlyArray<IExt
return dependencies;
}
export async function isAlpineLinux(fileService: IFileService, logService: ILogService): Promise<boolean> {
if (!isLinux) {
return false;
}
let content: string | undefined;
try {
const fileContent = await fileService.readFile(URI.file('/etc/os-release'));
content = fileContent.value.toString();
} catch (error) {
try {
const fileContent = await fileService.readFile(URI.file('/usr/lib/os-release'));
content = fileContent.value.toString();
} catch (error) {
/* Ignore */
logService.debug(`Error while getting the os-release file.`, getErrorMessage(error));
}
}
return !!content && (content.match(/^ID=([^\u001b\r\n]*)/m) || [])[1] === 'alpine';
}
export async function computeTargetPlatform(fileService: IFileService, logService: ILogService): Promise<TargetPlatform> {
const alpineLinux = await isAlpineLinux(fileService, logService);
const targetPlatform = getTargetPlatform(alpineLinux ? 'alpine' : platform, arch);
logService.debug('ComputeTargetPlatform:', targetPlatform);
return targetPlatform;
}

View File

@@ -0,0 +1,214 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { adoptToGalleryExtensionId, areSameExtensions, getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IProductService } from 'vs/platform/product/common/productService';
import { distinct } from 'vs/base/common/arrays';
import { ILogService } from 'vs/platform/log/common/log';
import { IExtension } from 'vs/platform/extensions/common/extensions';
import { isArray, isString } from 'vs/base/common/types';
import { IStringDictionary } from 'vs/base/common/collections';
import { IExtensionManagementService, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
export interface IExtensionIdWithVersion {
id: string;
version: string;
}
export const IExtensionStorageService = createDecorator<IExtensionStorageService>('IExtensionStorageService');
export interface IExtensionStorageService {
readonly _serviceBrand: undefined;
getExtensionState(extension: IExtension | IGalleryExtension | string, global: boolean): IStringDictionary<any> | undefined;
setExtensionState(extension: IExtension | IGalleryExtension | string, state: IStringDictionary<any> | undefined, global: boolean): void;
readonly onDidChangeExtensionStorageToSync: Event<void>;
setKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion, keys: string[]): void;
getKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion): string[] | undefined;
addToMigrationList(from: string, to: string): void;
getSourceExtensionToMigrate(target: string): string | undefined;
}
const EXTENSION_KEYS_ID_VERSION_REGEX = /^extensionKeys\/([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/;
export class ExtensionStorageService extends Disposable implements IExtensionStorageService {
readonly _serviceBrand: undefined;
private static toKey(extension: IExtensionIdWithVersion): string {
return `extensionKeys/${adoptToGalleryExtensionId(extension.id)}@${extension.version}`;
}
private static fromKey(key: string): IExtensionIdWithVersion | undefined {
const matches = EXTENSION_KEYS_ID_VERSION_REGEX.exec(key);
if (matches && matches[1]) {
return { id: matches[1], version: matches[2] };
}
return undefined;
}
static async removeOutdatedExtensionVersions(extensionManagementService: IExtensionManagementService, storageService: IStorageService): Promise<void> {
const extensions = await extensionManagementService.getInstalled();
const extensionVersionsToRemove: string[] = [];
for (const [id, versions] of ExtensionStorageService.readAllExtensionsWithKeysForSync(storageService)) {
const extensionVersion = extensions.find(e => areSameExtensions(e.identifier, { id }))?.manifest.version;
for (const version of versions) {
if (extensionVersion !== version) {
extensionVersionsToRemove.push(ExtensionStorageService.toKey({ id, version }));
}
}
}
for (const key of extensionVersionsToRemove) {
storageService.remove(key, StorageScope.GLOBAL);
}
}
private static readAllExtensionsWithKeysForSync(storageService: IStorageService): Map<string, string[]> {
const extensionsWithKeysForSync = new Map<string, string[]>();
const keys = storageService.keys(StorageScope.GLOBAL, StorageTarget.MACHINE);
for (const key of keys) {
const extensionIdWithVersion = ExtensionStorageService.fromKey(key);
if (extensionIdWithVersion) {
let versions = extensionsWithKeysForSync.get(extensionIdWithVersion.id.toLowerCase());
if (!versions) {
extensionsWithKeysForSync.set(extensionIdWithVersion.id.toLowerCase(), versions = []);
}
versions.push(extensionIdWithVersion.version);
}
}
return extensionsWithKeysForSync;
}
private readonly _onDidChangeExtensionStorageToSync = this._register(new Emitter<void>());
readonly onDidChangeExtensionStorageToSync = this._onDidChangeExtensionStorageToSync.event;
private readonly extensionsWithKeysForSync: Map<string, string[]>;
constructor(
@IStorageService private readonly storageService: IStorageService,
@IProductService private readonly productService: IProductService,
@ILogService private readonly logService: ILogService,
) {
super();
this.extensionsWithKeysForSync = ExtensionStorageService.readAllExtensionsWithKeysForSync(storageService);
this._register(this.storageService.onDidChangeValue(e => this.onDidChangeStorageValue(e)));
}
private onDidChangeStorageValue(e: IStorageValueChangeEvent): void {
if (e.scope !== StorageScope.GLOBAL) {
return;
}
// State of extension with keys for sync has changed
if (this.extensionsWithKeysForSync.has(e.key.toLowerCase())) {
this._onDidChangeExtensionStorageToSync.fire();
return;
}
// Keys for sync of an extension has changed
const extensionIdWithVersion = ExtensionStorageService.fromKey(e.key);
if (extensionIdWithVersion) {
if (this.storageService.get(e.key, StorageScope.GLOBAL) === undefined) {
this.extensionsWithKeysForSync.delete(extensionIdWithVersion.id.toLowerCase());
} else {
let versions = this.extensionsWithKeysForSync.get(extensionIdWithVersion.id.toLowerCase());
if (!versions) {
this.extensionsWithKeysForSync.set(extensionIdWithVersion.id.toLowerCase(), versions = []);
}
versions.push(extensionIdWithVersion.version);
this._onDidChangeExtensionStorageToSync.fire();
}
return;
}
}
private getExtensionId(extension: IExtension | IGalleryExtension | string): string {
if (isString(extension)) {
return extension;
}
const publisher = (extension as IExtension).manifest ? (extension as IExtension).manifest.publisher : (extension as IGalleryExtension).publisher;
const name = (extension as IExtension).manifest ? (extension as IExtension).manifest.name : (extension as IGalleryExtension).name;
return getExtensionId(publisher, name);
}
getExtensionState(extension: IExtension | IGalleryExtension | string, global: boolean): IStringDictionary<any> | undefined {
const extensionId = this.getExtensionId(extension);
const jsonValue = this.storageService.get(extensionId, global ? StorageScope.GLOBAL : StorageScope.WORKSPACE);
if (jsonValue) {
try {
return JSON.parse(jsonValue);
} catch (error) {
// Do not fail this call but log it for diagnostics
// https://github.com/microsoft/vscode/issues/132777
this.logService.error(`[mainThreadStorage] unexpected error parsing storage contents (extensionId: ${extensionId}, global: ${global}): ${error}`);
}
}
return undefined;
}
setExtensionState(extension: IExtension | IGalleryExtension | string, state: IStringDictionary<any> | undefined, global: boolean): void {
const extensionId = this.getExtensionId(extension);
if (state === undefined) {
this.storageService.remove(extensionId, global ? StorageScope.GLOBAL : StorageScope.WORKSPACE);
} else {
this.storageService.store(extensionId, JSON.stringify(state), global ? StorageScope.GLOBAL : StorageScope.WORKSPACE, StorageTarget.MACHINE /* Extension state is synced separately through extensions */);
}
}
setKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion, keys: string[]): void {
this.storageService.store(ExtensionStorageService.toKey(extensionIdWithVersion), JSON.stringify(keys), StorageScope.GLOBAL, StorageTarget.MACHINE);
}
getKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion): string[] | undefined {
const extensionKeysForSyncFromProduct = this.productService.extensionSyncedKeys?.[extensionIdWithVersion.id.toLowerCase()];
const extensionKeysForSyncFromStorageValue = this.storageService.get(ExtensionStorageService.toKey(extensionIdWithVersion), StorageScope.GLOBAL);
const extensionKeysForSyncFromStorage = extensionKeysForSyncFromStorageValue ? JSON.parse(extensionKeysForSyncFromStorageValue) : undefined;
return extensionKeysForSyncFromStorage && extensionKeysForSyncFromProduct
? distinct([...extensionKeysForSyncFromStorage, ...extensionKeysForSyncFromProduct])
: (extensionKeysForSyncFromStorage || extensionKeysForSyncFromProduct);
}
addToMigrationList(from: string, to: string): void {
if (from !== to) {
// remove the duplicates
const migrationList: [string, string][] = this.migrationList.filter(entry => !entry.includes(from) && !entry.includes(to));
migrationList.push([from, to]);
this.migrationList = migrationList;
}
}
getSourceExtensionToMigrate(toExtensionId: string): string | undefined {
const entry = this.migrationList.find(([, to]) => toExtensionId === to);
return entry ? entry[0] : undefined;
}
private get migrationList(): [string, string][] {
const value = this.storageService.get('extensionStorage.migrationList', StorageScope.GLOBAL, '[]');
try {
const migrationList = JSON.parse(value);
if (isArray(migrationList)) {
return migrationList;
}
} catch (error) { /* ignore */ }
return [];
}
private set migrationList(migrationList: [string, string][]) {
if (migrationList.length) {
this.storageService.store('extensionStorage.migrationList', JSON.stringify(migrationList), StorageScope.GLOBAL, StorageTarget.MACHINE);
} else {
this.storageService.remove('extensionStorage.migrationList', StorageScope.GLOBAL);
}
}
}

View File

@@ -68,7 +68,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
extensionName: value.name,
configName: tip.configName,
important: !!value.important,
isExtensionPack: !!value.isExtensionPack
isExtensionPack: !!value.isExtensionPack,
whenNotInstalled: value.whenNotInstalled
});
}
} else {
@@ -77,7 +78,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
extensionName: value.name,
configName: tip.configName,
important: !!value.important,
isExtensionPack: !!value.isExtensionPack
isExtensionPack: !!value.isExtensionPack,
whenNotInstalled: value.whenNotInstalled
});
}
});

View File

@@ -0,0 +1,875 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { coalesce } from 'vs/base/common/arrays';
import { ThrottledDelayer } from 'vs/base/common/async';
import * as objects from 'vs/base/common/objects';
import { VSBuffer } from 'vs/base/common/buffer';
import { IStringDictionary } from 'vs/base/common/collections';
import { getErrorMessage } from 'vs/base/common/errors';
import { getNodeType, parse, ParseError } from 'vs/base/common/json';
import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
import { Disposable } from 'vs/base/common/lifecycle';
import { FileAccess, Schemas } from 'vs/base/common/network';
import * as path from 'vs/base/common/path';
import * as platform from 'vs/base/common/platform';
import { basename, isEqual, joinPath } from 'vs/base/common/resources';
import * as semver from 'vs/base/common/semver/semver';
import Severity from 'vs/base/common/severity';
import { isArray, isObject, isString } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER } from 'vs/platform/extensions/common/extensions';
import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator';
import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { Emitter, Event } from 'vs/base/common/event';
import { revive } from 'vs/base/common/marshalling';
export type IScannedExtensionManifest = IRelaxedExtensionManifest & { __metadata?: Metadata };
interface IRelaxedScannedExtension {
type: ExtensionType;
isBuiltin: boolean;
identifier: IExtensionIdentifier;
manifest: IRelaxedExtensionManifest;
location: URI;
targetPlatform: TargetPlatform;
metadata: Metadata | undefined;
isValid: boolean;
validations: readonly [Severity, string][];
}
export type IScannedExtension = Readonly<IRelaxedScannedExtension> & { manifest: IExtensionManifest };
export interface Translations {
[id: string]: string;
}
export namespace Translations {
export function equals(a: Translations, b: Translations): boolean {
if (a === b) {
return true;
}
let aKeys = Object.keys(a);
let bKeys: Set<string> = new Set<string>();
for (let key of Object.keys(b)) {
bKeys.add(key);
}
if (aKeys.length !== bKeys.size) {
return false;
}
for (let key of aKeys) {
if (a[key] !== b[key]) {
return false;
}
bKeys.delete(key);
}
return bKeys.size === 0;
}
}
interface MessageBag {
[key: string]: string | { message: string; comment: string[] };
}
interface TranslationBundle {
contents: {
package: MessageBag;
};
}
interface LocalizedMessages {
values: MessageBag | undefined;
default: URI | null;
}
interface IBuiltInExtensionControl {
[name: string]: 'marketplace' | 'disabled' | string;
}
export type ScanOptions = {
readonly includeInvalid?: boolean;
readonly includeAllVersions?: boolean;
readonly includeUninstalled?: boolean;
readonly checkControlFile?: boolean;
readonly language?: string;
readonly useCache?: boolean;
};
export const IExtensionsScannerService = createDecorator<IExtensionsScannerService>('IExtensionsScannerService');
export interface IExtensionsScannerService {
readonly _serviceBrand: undefined;
readonly systemExtensionsLocation: URI;
readonly userExtensionsLocation: URI;
readonly onDidChangeCache: Event<ExtensionType>;
getTargetPlatform(): Promise<TargetPlatform>;
scanAllExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]>;
scanSystemExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]>;
scanUserExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]>;
scanExtensionsUnderDevelopment(scanOptions: ScanOptions, existingExtensions: IScannedExtension[]): Promise<IScannedExtension[]>;
scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension | null>;
scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension[]>;
updateMetadata(extensionLocation: URI, metadata: Partial<Metadata>): Promise<void>;
}
export abstract class AbstractExtensionsScannerService extends Disposable implements IExtensionsScannerService {
readonly _serviceBrand: undefined;
protected abstract getTranslations(language: string): Promise<Translations>;
private readonly _onDidChangeCache = this._register(new Emitter<ExtensionType>());
readonly onDidChangeCache = this._onDidChangeCache.event;
private readonly systemExtensionsCachedScanner = this._register(new CachedExtensionsScanner(joinPath(this.cacheLocation, BUILTIN_MANIFEST_CACHE_FILE), this.fileService, this.logService));
private readonly userExtensionsCachedScanner = this._register(new CachedExtensionsScanner(joinPath(this.cacheLocation, USER_MANIFEST_CACHE_FILE), this.fileService, this.logService));
private readonly extensionsScanner = this._register(new ExtensionsScanner(this.fileService, this.logService));
constructor(
readonly systemExtensionsLocation: URI,
readonly userExtensionsLocation: URI,
private readonly extensionsControlLocation: URI,
private readonly cacheLocation: URI,
@IFileService protected readonly fileService: IFileService,
@ILogService protected readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IProductService private readonly productService: IProductService,
) {
super();
this._register(this.systemExtensionsCachedScanner.onDidChangeCache(() => this._onDidChangeCache.fire(ExtensionType.System)));
this._register(this.userExtensionsCachedScanner.onDidChangeCache(() => this._onDidChangeCache.fire(ExtensionType.User)));
}
private _targetPlatformPromise: Promise<TargetPlatform> | undefined;
getTargetPlatform(): Promise<TargetPlatform> {
if (!this._targetPlatformPromise) {
this._targetPlatformPromise = computeTargetPlatform(this.fileService, this.logService);
}
return this._targetPlatformPromise;
}
async scanAllExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]> {
const [system, user] = await Promise.all([
this.scanSystemExtensions(scanOptions),
this.scanUserExtensions(scanOptions),
]);
const development = await this.scanExtensionsUnderDevelopment(scanOptions, [...system, ...user]);
return this.dedupExtensions([...system, ...user, ...development], await this.getTargetPlatform(), true);
}
async scanSystemExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]> {
const promises: Promise<IRelaxedScannedExtension[]>[] = [];
promises.push(this.scanDefaultSystemExtensions(!!scanOptions.useCache, scanOptions.language));
promises.push(this.scanDevSystemExtensions(scanOptions.language, !!scanOptions.checkControlFile));
const [defaultSystemExtensions, devSystemExtensions] = await Promise.all(promises);
return this.applyScanOptions([...defaultSystemExtensions, ...devSystemExtensions], scanOptions, false);
}
async scanUserExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]> {
this.logService.trace('Started scanning user extensions');
const extensionsScannerInput = await this.createExtensionScannerInput(this.userExtensionsLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language);
const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode && extensionsScannerInput.excludeObsolete ? this.userExtensionsCachedScanner : this.extensionsScanner;
let extensions = await extensionsScanner.scanExtensions(extensionsScannerInput);
extensions = await this.applyScanOptions(extensions, scanOptions, true);
this.logService.trace('Scanned user extensions:', extensions.length);
return extensions;
}
async scanExtensionsUnderDevelopment(scanOptions: ScanOptions, existingExtensions: IScannedExtension[]): Promise<IScannedExtension[]> {
if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) {
const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file)
.map(async extensionDevelopmentLocationURI => {
const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, ExtensionType.User, true, scanOptions.language, false /* do not validate */);
const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(input);
return extensions.map(extension => {
// Override the extension type from the existing extensions
extension.type = existingExtensions.find(e => areSameExtensions(e.identifier, extension.identifier))?.type ?? extension.type;
// Validate the extension
return this.extensionsScanner.validate(extension, input);
});
})))
.flat();
return this.applyScanOptions(extensions, scanOptions, true);
}
return [];
}
async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension | null> {
const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, extensionType, true, scanOptions.language);
const extension = await this.extensionsScanner.scanExtension(extensionsScannerInput);
if (!extension) {
return null;
}
if (!scanOptions.includeInvalid && !extension.isValid) {
return null;
}
return extension;
}
async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension[]> {
const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, extensionType, true, scanOptions.language);
const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput);
return this.applyScanOptions(extensions, scanOptions, true);
}
async updateMetadata(extensionLocation: URI, metaData: Partial<Metadata>): Promise<void> {
const manifestLocation = joinPath(extensionLocation, 'package.json');
const content = (await this.fileService.readFile(manifestLocation)).value.toString();
const manifest: IScannedExtensionManifest = JSON.parse(content);
// unset if false
metaData.isMachineScoped = metaData.isMachineScoped || undefined;
metaData.isBuiltin = metaData.isBuiltin || undefined;
metaData.installedTimestamp = metaData.installedTimestamp || undefined;
manifest.__metadata = { ...manifest.__metadata, ...metaData };
await this.fileService.writeFile(joinPath(extensionLocation, 'package.json'), VSBuffer.fromString(JSON.stringify(manifest, null, '\t')));
}
private async applyScanOptions(extensions: IRelaxedScannedExtension[], scanOptions: ScanOptions, pickLatest: boolean): Promise<IRelaxedScannedExtension[]> {
if (!scanOptions.includeAllVersions) {
extensions = this.dedupExtensions(extensions, await this.getTargetPlatform(), pickLatest);
}
if (!scanOptions.includeInvalid) {
extensions = extensions.filter(extension => extension.isValid);
}
return extensions.sort((a, b) => {
const aLastSegment = path.basename(a.location.fsPath);
const bLastSegment = path.basename(b.location.fsPath);
if (aLastSegment < bLastSegment) {
return -1;
}
if (aLastSegment > bLastSegment) {
return 1;
}
return 0;
});
}
private dedupExtensions(extensions: IRelaxedScannedExtension[], targetPlatform: TargetPlatform, pickLatest: boolean): IRelaxedScannedExtension[] {
const result = new Map<string, IRelaxedScannedExtension>();
for (const extension of extensions) {
const extensionKey = ExtensionIdentifier.toKey(extension.identifier.id);
const existing = result.get(extensionKey);
if (existing) {
if (existing.isValid && !extension.isValid) {
continue;
}
if (existing.isValid === extension.isValid) {
if (pickLatest && semver.gt(existing.manifest.version, extension.manifest.version)) {
this.logService.debug(`Skipping extension ${extension.location.path} with lower version ${extension.manifest.version}.`);
continue;
}
if (semver.eq(existing.manifest.version, extension.manifest.version) && existing.targetPlatform === targetPlatform) {
this.logService.debug(`Skipping extension ${extension.location.path} from different target platform ${extension.targetPlatform}`);
continue;
}
}
if (existing.type === ExtensionType.System) {
this.logService.debug(`Overwriting system extension ${existing.location.path} with ${extension.location.path}.`);
} else {
this.logService.warn(`Overwriting user extension ${existing.location.path} with ${extension.location.path}.`);
}
}
result.set(extensionKey, extension);
}
return [...result.values()];
}
private async scanDefaultSystemExtensions(useCache: boolean, language: string | undefined): Promise<IRelaxedScannedExtension[]> {
this.logService.trace('Started scanning system extensions');
const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, ExtensionType.System, true, language);
const extensionsScanner = useCache && !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner;
const result = await extensionsScanner.scanExtensions(extensionsScannerInput);
this.logService.trace('Scanned system extensions:', result.length);
return result;
}
private async scanDevSystemExtensions(language: string | undefined, checkControlFile: boolean): Promise<IRelaxedScannedExtension[]> {
const devSystemExtensionsList = this.environmentService.isBuilt ? [] : this.productService.builtInExtensions;
if (!devSystemExtensionsList?.length) {
return [];
}
this.logService.trace('Started scanning dev system extensions');
const builtinExtensionControl = checkControlFile ? await this.getBuiltInExtensionControl() : {};
const devSystemExtensionsLocations: URI[] = [];
const devSystemExtensionsLocation = URI.file(path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions')));
for (const extension of devSystemExtensionsList) {
const controlState = builtinExtensionControl[extension.name] || 'marketplace';
switch (controlState) {
case 'disabled':
break;
case 'marketplace':
devSystemExtensionsLocations.push(joinPath(devSystemExtensionsLocation, extension.name));
break;
default:
devSystemExtensionsLocations.push(URI.file(controlState));
break;
}
}
const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, ExtensionType.System, true, language)))));
this.logService.trace('Scanned dev system extensions:', result.length);
return coalesce(result);
}
private async getBuiltInExtensionControl(): Promise<IBuiltInExtensionControl> {
try {
const content = await this.fileService.readFile(this.extensionsControlLocation);
return JSON.parse(content.value.toString());
} catch (error) {
return {};
}
}
private async createExtensionScannerInput(location: URI, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean = true): Promise<ExtensionScannerInput> {
const translations = await this.getTranslations(language ?? platform.language);
let mtime: number | undefined;
try {
const folderStat = await this.fileService.stat(location);
if (typeof folderStat.mtime === 'number') {
mtime = folderStat.mtime;
}
} catch (err) {
// That's ok...
}
return new ExtensionScannerInput(
location,
mtime,
type,
excludeObsolete,
validate,
this.productService.version,
this.productService.date,
this.productService.commit,
!this.environmentService.isBuilt,
language,
translations,
);
}
}
class ExtensionScannerInput {
constructor(
public readonly location: URI,
public readonly mtime: number | undefined,
public readonly type: ExtensionType,
public readonly excludeObsolete: boolean,
public readonly validate: boolean,
public readonly productVersion: string,
public readonly productDate: string | undefined,
public readonly productCommit: string | undefined,
public readonly devMode: boolean,
public readonly language: string | undefined,
public readonly translations: Translations
) {
// Keep empty!! (JSON.parse)
}
public static createNlsConfiguration(input: ExtensionScannerInput): NlsConfiguration {
return {
language: input.language,
pseudo: input.language === 'pseudo',
devMode: input.devMode,
translations: input.translations
};
}
public static equals(a: ExtensionScannerInput, b: ExtensionScannerInput): boolean {
return (
isEqual(a.location, b.location)
&& a.mtime === b.mtime
&& a.type === b.type
&& a.excludeObsolete === b.excludeObsolete
&& a.validate === b.validate
&& a.productVersion === b.productVersion
&& a.productDate === b.productDate
&& a.productCommit === b.productCommit
&& a.devMode === b.devMode
&& a.language === b.language
&& Translations.equals(a.translations, b.translations)
);
}
}
type NlsConfiguration = {
language: string | undefined;
pseudo: boolean;
devMode: boolean;
translations: Translations;
};
class ExtensionsScanner extends Disposable {
constructor(
protected readonly fileService: IFileService,
protected readonly logService: ILogService
) {
super();
}
async scanExtensions(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {
const stat = await this.fileService.resolve(input.location);
if (stat.children) {
let obsolete: IStringDictionary<boolean> = {};
if (input.excludeObsolete && input.type === ExtensionType.User) {
try {
const raw = (await this.fileService.readFile(joinPath(input.location, '.obsolete'))).value.toString();
obsolete = JSON.parse(raw);
} catch (error) { /* ignore */ }
}
const extensions = await Promise.all<IRelaxedScannedExtension | null>(
stat.children.map(async c => {
if (!c.isDirectory) {
return null;
}
// Do not consider user extension folder starting with `.`
if (input.type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) {
return null;
}
const extensionScannerInput = new ExtensionScannerInput(c.resource, input.mtime, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations);
const extension = await this.scanExtension(extensionScannerInput);
return extension && !obsolete[ExtensionKey.create(extension).toString()] ? extension : null;
}));
return coalesce(extensions);
}
return [];
}
async scanOneOrMultipleExtensions(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {
try {
if (await this.fileService.exists(joinPath(input.location, 'package.json'))) {
const extension = await this.scanExtension(input);
return extension ? [extension] : [];
} else {
return await this.scanExtensions(input);
}
} catch (error) {
this.logService.error(`Error scanning extensions at ${input.location.path}:`, getErrorMessage(error));
return [];
}
}
async scanExtension(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension | null> {
try {
let manifest = await this.scanExtensionManifest(input.location);
if (manifest) {
// allow publisher to be undefined to make the initial extension authoring experience smoother
if (!manifest.publisher) {
manifest.publisher = UNDEFINED_PUBLISHER;
}
const metadata = manifest.__metadata;
delete manifest.__metadata;
const id = getGalleryExtensionId(manifest.publisher, manifest.name);
const identifier = metadata?.id ? { id, uuid: metadata.id } : { id };
const type = metadata?.isSystem ? ExtensionType.System : input.type;
const isBuiltin = type === ExtensionType.System || !!metadata?.isBuiltin;
manifest = await this.translateManifest(input.location, manifest, ExtensionScannerInput.createNlsConfiguration(input));
const extension = {
type,
identifier,
manifest,
location: input.location,
isBuiltin,
targetPlatform: metadata?.targetPlatform ?? TargetPlatform.UNDEFINED,
metadata,
isValid: true,
validations: []
};
return input.validate ? this.validate(extension, input) : extension;
}
} catch (e) {
if (input.type !== ExtensionType.System) {
this.logService.error(e);
}
}
return null;
}
validate(extension: IRelaxedScannedExtension, input: ExtensionScannerInput): IRelaxedScannedExtension {
let isValid = true;
const validations = validateExtensionManifest(input.productVersion, input.productDate, input.location, extension.manifest, extension.isBuiltin);
for (const [severity, message] of validations) {
if (severity === Severity.Error) {
isValid = false;
this.logService.error(this.formatMessage(input.location, message));
}
}
extension.isValid = isValid;
extension.validations = validations;
return extension;
}
private async scanExtensionManifest(extensionLocation: URI): Promise<IScannedExtensionManifest | null> {
const manifestLocation = joinPath(extensionLocation, 'package.json');
let content;
try {
content = (await this.fileService.readFile(manifestLocation)).value.toString();
} catch (error) {
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
this.logService.error(this.formatMessage(extensionLocation, localize('fileReadFail', "Cannot read file {0}: {1}.", manifestLocation.path, error.message)));
}
return null;
}
let manifest: IScannedExtensionManifest;
try {
manifest = JSON.parse(content);
} catch (err) {
// invalid JSON, let's get good errors
const errors: ParseError[] = [];
parse(content, errors);
for (const e of errors) {
this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseFail', "Failed to parse {0}: [{1}, {2}] {3}.", manifestLocation.path, e.offset, e.length, getParseErrorMessage(e.error))));
}
return null;
}
if (getNodeType(manifest) !== 'object') {
this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not an JSON object.", manifestLocation.path)));
return null;
}
return manifest;
}
private async translateManifest(extensionLocation: URI, extensionManifest: IExtensionManifest, nlsConfiguration: NlsConfiguration): Promise<IExtensionManifest> {
const localizedMessages = await this.getLocalizedMessages(extensionLocation, extensionManifest, nlsConfiguration);
if (localizedMessages) {
try {
const errors: ParseError[] = [];
// resolveOriginalMessageBundle returns null if localizedMessages.default === undefined;
const defaults = await this.resolveOriginalMessageBundle(localizedMessages.default, errors);
if (errors.length > 0) {
errors.forEach((error) => {
this.logService.error(this.formatMessage(extensionLocation, localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localizedMessages.default?.path, getParseErrorMessage(error.error))));
});
return extensionManifest;
} else if (getNodeType(localizedMessages) !== 'object') {
this.logService.error(this.formatMessage(extensionLocation, localize('jsonInvalidFormat', "Invalid format {0}: JSON object expected.", localizedMessages.default?.path)));
return extensionManifest;
}
const localized = localizedMessages.values || Object.create(null);
this.replaceNLStrings(nlsConfiguration.pseudo, extensionManifest, localized, defaults, extensionLocation);
} catch (error) {
/*Ignore Error*/
}
}
return extensionManifest;
}
private async getLocalizedMessages(extensionLocation: URI, extensionManifest: IExtensionManifest, nlsConfiguration: NlsConfiguration): Promise<LocalizedMessages | undefined> {
const defaultPackageNLS = joinPath(extensionLocation, 'package.nls.json');
const reportErrors = (localized: URI | null, errors: ParseError[]): void => {
errors.forEach((error) => {
this.logService.error(this.formatMessage(extensionLocation, localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localized?.path, getParseErrorMessage(error.error))));
});
};
const reportInvalidFormat = (localized: URI | null): void => {
this.logService.error(this.formatMessage(extensionLocation, localize('jsonInvalidFormat', "Invalid format {0}: JSON object expected.", localized?.path)));
};
const translationId = `${extensionManifest.publisher}.${extensionManifest.name}`;
const translationPath = nlsConfiguration.translations[translationId];
if (translationPath) {
try {
const translationResource = URI.file(translationPath);
const content = (await this.fileService.readFile(translationResource)).value.toString();
let errors: ParseError[] = [];
let translationBundle: TranslationBundle = parse(content, errors);
if (errors.length > 0) {
reportErrors(translationResource, errors);
return { values: undefined, default: defaultPackageNLS };
} else if (getNodeType(translationBundle) !== 'object') {
reportInvalidFormat(translationResource);
return { values: undefined, default: defaultPackageNLS };
} else {
let values = translationBundle.contents ? translationBundle.contents.package : undefined;
return { values: values, default: defaultPackageNLS };
}
} catch (error) {
return { values: undefined, default: defaultPackageNLS };
}
} else {
const exists = await this.fileService.exists(defaultPackageNLS);
if (!exists) {
return undefined;
}
let messageBundle;
try {
messageBundle = await this.findMessageBundles(extensionLocation, nlsConfiguration);
} catch (error) {
return undefined;
}
if (!messageBundle.localized) {
return { values: undefined, default: messageBundle.original };
}
try {
const messageBundleContent = (await this.fileService.readFile(messageBundle.localized)).value.toString();
let errors: ParseError[] = [];
let messages: MessageBag = parse(messageBundleContent, errors);
if (errors.length > 0) {
reportErrors(messageBundle.localized, errors);
return { values: undefined, default: messageBundle.original };
} else if (getNodeType(messages) !== 'object') {
reportInvalidFormat(messageBundle.localized);
return { values: undefined, default: messageBundle.original };
}
return { values: messages, default: messageBundle.original };
} catch (error) {
return { values: undefined, default: messageBundle.original };
}
}
}
/**
* Parses original message bundle, returns null if the original message bundle is null.
*/
private async resolveOriginalMessageBundle(originalMessageBundle: URI | null, errors: ParseError[]): Promise<{ [key: string]: string } | null> {
if (originalMessageBundle) {
try {
const originalBundleContent = (await this.fileService.readFile(originalMessageBundle)).value.toString();
return parse(originalBundleContent, errors);
} catch (error) {
/* Ignore Error */
return null;
}
} else {
return null;
}
}
/**
* Finds localized message bundle and the original (unlocalized) one.
* If the localized file is not present, returns null for the original and marks original as localized.
*/
private findMessageBundles(extensionLocation: URI, nlsConfiguration: NlsConfiguration): Promise<{ localized: URI; original: URI | null }> {
return new Promise<{ localized: URI; original: URI | null }>((c, e) => {
const loop = (locale: string): void => {
let toCheck = joinPath(extensionLocation, `package.nls.${locale}.json`);
this.fileService.exists(toCheck).then(exists => {
if (exists) {
c({ localized: toCheck, original: joinPath(extensionLocation, 'package.nls.json') });
}
let index = locale.lastIndexOf('-');
if (index === -1) {
c({ localized: joinPath(extensionLocation, 'package.nls.json'), original: null });
} else {
locale = locale.substring(0, index);
loop(locale);
}
});
};
if (nlsConfiguration.devMode || nlsConfiguration.pseudo || !nlsConfiguration.language) {
return c({ localized: joinPath(extensionLocation, 'package.nls.json'), original: null });
}
loop(nlsConfiguration.language);
});
}
/**
* This routine makes the following assumptions:
* The root element is an object literal
*/
private replaceNLStrings<T extends object>(pseudo: boolean, literal: T, messages: MessageBag, originalMessages: MessageBag | null, extensionLocation: URI): void {
const processEntry = (obj: any, key: string | number, command?: boolean) => {
const value = obj[key];
if (isString(value)) {
const str = <string>value;
const length = str.length;
if (length > 1 && str[0] === '%' && str[length - 1] === '%') {
const messageKey = str.substr(1, length - 2);
let translated = messages[messageKey];
// If the messages come from a language pack they might miss some keys
// Fill them from the original messages.
if (translated === undefined && originalMessages) {
translated = originalMessages[messageKey];
}
let message: string | undefined = typeof translated === 'string' ? translated : (typeof translated?.message === 'string' ? translated.message : undefined);
if (message !== undefined) {
if (pseudo) {
// FF3B and FF3D is the Unicode zenkaku representation for [ and ]
message = '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D';
}
obj[key] = command && (key === 'title' || key === 'category') && originalMessages ? { value: message, original: originalMessages[messageKey] } : message;
} else {
this.logService.warn(this.formatMessage(extensionLocation, localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey)));
}
}
} else if (isObject(value)) {
for (let k in value) {
if (value.hasOwnProperty(k)) {
k === 'commands' ? processEntry(value, k, true) : processEntry(value, k, command);
}
}
} else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
processEntry(value, i, command);
}
}
};
for (let key in literal) {
if (literal.hasOwnProperty(key)) {
processEntry(literal, key);
}
}
}
private formatMessage(extensionLocation: URI, message: string): string {
return `[${extensionLocation.path}]: ${message}`;
}
}
interface IExtensionCacheData {
input: ExtensionScannerInput;
result: IRelaxedScannedExtension[];
}
class CachedExtensionsScanner extends ExtensionsScanner {
private input: ExtensionScannerInput | undefined;
private readonly cacheValidatorThrottler: ThrottledDelayer<void> = this._register(new ThrottledDelayer(3000));
private readonly _onDidChangeCache = this._register(new Emitter<void>());
readonly onDidChangeCache = this._onDidChangeCache.event;
constructor(
private readonly cacheFile: URI,
fileService: IFileService,
logService: ILogService
) {
super(fileService, logService);
}
override async scanExtensions(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> {
const cacheContents = await this.readExtensionCache();
this.input = input;
if (cacheContents && cacheContents.input && ExtensionScannerInput.equals(cacheContents.input, this.input)) {
this.cacheValidatorThrottler.trigger(() => this.validateCache());
return cacheContents.result.map((extension) => {
// revive URI object
extension.location = URI.revive(extension.location);
return extension;
});
}
const result = await super.scanExtensions(input);
await this.writeExtensionCache({ input, result });
return result;
}
private async readExtensionCache(): Promise<IExtensionCacheData | null> {
try {
const cacheRawContents = await this.fileService.readFile(this.cacheFile);
const extensionCacheData: IExtensionCacheData = JSON.parse(cacheRawContents.value.toString());
return { result: extensionCacheData.result, input: revive(extensionCacheData.input) };
} catch (error) {
this.logService.debug('Error while reading the extension cache file:', this.cacheFile.path, getErrorMessage(error));
}
return null;
}
private async writeExtensionCache(cacheContents: IExtensionCacheData): Promise<void> {
try {
await this.fileService.writeFile(this.cacheFile, VSBuffer.fromString(JSON.stringify(cacheContents)));
} catch (error) {
this.logService.debug('Error while writing the extension cache file:', this.cacheFile.path, getErrorMessage(error));
}
}
private async validateCache(): Promise<void> {
if (!this.input) {
// Input has been unset by the time we get here, so skip validation
return;
}
const cacheContents = await this.readExtensionCache();
if (!cacheContents) {
// Cache has been deleted by someone else, which is perfectly fine...
return;
}
const actual = cacheContents.result;
const expected = JSON.parse(JSON.stringify(await super.scanExtensions(this.input)));
if (objects.equals(expected, actual)) {
// Cache is valid and running with it is perfectly fine...
return;
}
try {
// Cache is invalid, delete it
await this.fileService.del(this.cacheFile);
this._onDidChangeCache.fire();
} catch (error) {
this.logService.error(error);
}
}
}
export function toExtensionDescription(extension: IScannedExtension, isUnderDevelopment: boolean): IExtensionDescription {
const id = getExtensionId(extension.manifest.publisher, extension.manifest.name);
return {
id,
identifier: new ExtensionIdentifier(id),
isBuiltin: extension.type === ExtensionType.System,
isUserBuiltin: extension.type === ExtensionType.User && extension.isBuiltin,
isUnderDevelopment,
extensionLocation: extension.location,
uuid: extension.identifier.uuid,
targetPlatform: extension.targetPlatform,
...extension.manifest,
};
}
export class NativeExtensionsScannerService extends AbstractExtensionsScannerService implements IExtensionsScannerService {
private readonly translationsPromise: Promise<Translations>;
constructor(
systemExtensionsLocation: URI,
userExtensionsLocation: URI,
userHome: URI,
userDataPath: URI,
fileService: IFileService,
logService: ILogService,
environmentService: IEnvironmentService,
productService: IProductService,
) {
super(
systemExtensionsLocation,
userExtensionsLocation,
joinPath(userHome, '.vscode-oss-dev', 'extensions', 'control.json'),
joinPath(userDataPath, MANIFEST_CACHE_FOLDER),
fileService, logService, environmentService, productService);
this.translationsPromise = (async () => {
if (platform.translationsConfigFile) {
try {
const content = await this.fileService.readFile(URI.file(platform.translationsConfigFile));
return JSON.parse(content.value.toString());
} catch (err) { /* Ignore Error */ }
}
return Object.create(null);
})();
}
protected getTranslations(language: string): Promise<Translations> {
return this.translationsPromise;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage';
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
/**
* Migrates the installed unsupported nightly extension to a supported pre-release extension. It includes following:
* - Uninstall the Unsupported extension
* - Install (with optional storage migration) the Pre-release extension only if
* - the extension is not installed
* - or it is a release version and the unsupported extension is enabled.
*/
export async function migrateUnsupportedExtensions(extensionManagementService: IExtensionManagementService, galleryService: IExtensionGalleryService, extensionStorageService: IExtensionStorageService, extensionEnablementService: IGlobalExtensionEnablementService, logService: ILogService): Promise<void> {
try {
const extensionsControlManifest = await extensionManagementService.getExtensionsControlManifest();
if (!extensionsControlManifest.unsupportedPreReleaseExtensions) {
return;
}
const installed = await extensionManagementService.getInstalled(ExtensionType.User);
for (const [unsupportedExtensionId, { id: preReleaseExtensionId, migrateStorage }] of Object.entries(extensionsControlManifest.unsupportedPreReleaseExtensions)) {
const unsupportedExtension = installed.find(i => areSameExtensions(i.identifier, { id: unsupportedExtensionId }));
// Unsupported Extension is not installed
if (!unsupportedExtension) {
continue;
}
const gallery = (await galleryService.getExtensions([{ id: preReleaseExtensionId, preRelease: true }], { targetPlatform: await extensionManagementService.getTargetPlatform(), compatible: true }, CancellationToken.None))[0];
if (!gallery) {
logService.info(`Skipping migrating '${unsupportedExtension.identifier.id}' extension because, the comaptible target '${preReleaseExtensionId}' extension is not found`);
continue;
}
try {
logService.info(`Migrating '${unsupportedExtension.identifier.id}' extension to '${preReleaseExtensionId}' extension...`);
const isUnsupportedExtensionEnabled = !extensionEnablementService.getDisabledExtensions().some(e => areSameExtensions(e, unsupportedExtension.identifier));
await extensionManagementService.uninstall(unsupportedExtension);
logService.info(`Uninstalled the unsupported extension '${unsupportedExtension.identifier.id}'`);
let preReleaseExtension = installed.find(i => areSameExtensions(i.identifier, { id: preReleaseExtensionId }));
if (!preReleaseExtension || (!preReleaseExtension.isPreReleaseVersion && isUnsupportedExtensionEnabled)) {
preReleaseExtension = await extensionManagementService.installFromGallery(gallery, { installPreReleaseVersion: true, isMachineScoped: unsupportedExtension.isMachineScoped, operation: InstallOperation.Migrate });
logService.info(`Installed the pre-release extension '${preReleaseExtension.identifier.id}'`);
if (!isUnsupportedExtensionEnabled) {
await extensionEnablementService.disableExtension(preReleaseExtension.identifier);
logService.info(`Disabled the pre-release extension '${preReleaseExtension.identifier.id}' because the unsupported extension '${unsupportedExtension.identifier.id}' is disabled`);
}
if (migrateStorage) {
extensionStorageService.addToMigrationList(getExtensionId(unsupportedExtension.manifest.publisher, unsupportedExtension.manifest.name), getExtensionId(preReleaseExtension.manifest.publisher, preReleaseExtension.manifest.name));
logService.info(`Added pre-release extension to the storage migration list`);
}
}
logService.info(`Migrated '${unsupportedExtension.identifier.id}' extension to '${preReleaseExtensionId}' extension.`);
} catch (error) {
logService.error(error);
}
}
} catch (error) {
logService.error(error);
}
}

View File

@@ -14,8 +14,10 @@ import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { IExecutableBasedExtensionTip, IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionTipsService as BaseExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionTipsService';
import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
@@ -25,14 +27,14 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
type ExeExtensionRecommendationsClassification = {
extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
exeName: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
extensionId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' };
exeName: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' };
};
type IExeBasedExtensionTips = {
readonly exeFriendlyName: string,
readonly windowsPath?: string,
readonly recommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[];
readonly exeFriendlyName: string;
readonly windowsPath?: string;
readonly recommendations: { extensionId: string; extensionName: string; isExtensionPack: boolean; whenNotInstalled?: string[] }[];
};
const promptedExecutableTipsStorageKey = 'extensionTips/promptedExecutableTips';
@@ -64,9 +66,9 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
super(fileService, productService, requestService, logService);
if (productService.exeBasedExtensionTips) {
forEach(productService.exeBasedExtensionTips, ({ key, value: exeBasedExtensionTip }) => {
const highImportanceRecommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[] = [];
const mediumImportanceRecommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[] = [];
const otherRecommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[] = [];
const highImportanceRecommendations: { extensionId: string; extensionName: string; isExtensionPack: boolean }[] = [];
const mediumImportanceRecommendations: { extensionId: string; extensionName: string; isExtensionPack: boolean }[] = [];
const otherRecommendations: { extensionId: string; extensionName: string; isExtensionPack: boolean }[] = [];
forEach(exeBasedExtensionTip.recommendations, ({ key: extensionId, value }) => {
if (value.important) {
if (exeBasedExtensionTip.important) {
@@ -130,13 +132,13 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
for (const extensionId of installed) {
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: tip.exeName });
this.telemetryService.publicLog2<{ exeName: string; extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: tip.exeName });
}
}
for (const extensionId of recommendations) {
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: tip.exeName });
this.telemetryService.publicLog2<{ exeName: string; extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: tip.exeName });
}
}
@@ -175,15 +177,17 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
case RecommendationsNotificationResult.Ignored:
this.highImportanceTipsByExe.delete(exeName);
break;
case RecommendationsNotificationResult.IncompatibleWindow:
case RecommendationsNotificationResult.IncompatibleWindow: {
// Recommended in incompatible window. Schedule the prompt after active window change
const onActiveWindowChange = Event.once(Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow)));
this._register(onActiveWindowChange(() => this.promptHighImportanceExeBasedTip()));
break;
case RecommendationsNotificationResult.TooMany:
}
case RecommendationsNotificationResult.TooMany: {
// Too many notifications. Schedule the prompt after one hour
const disposable = this._register(disposableTimeout(() => { disposable.dispose(); this.promptHighImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */));
break;
}
}
});
}
@@ -209,7 +213,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
this.promptExeRecommendations(tips)
.then(result => {
switch (result) {
case RecommendationsNotificationResult.Accepted:
case RecommendationsNotificationResult.Accepted: {
// Accepted: Update the last prompted time and caches.
this.updateLastPromptedMediumExeTime(Date.now());
this.mediumImportanceTipsByExe.delete(exeName);
@@ -218,29 +222,33 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
// Schedule the next recommendation for next internval
const disposable1 = this._register(disposableTimeout(() => { disposable1.dispose(); this.promptMediumImportanceExeBasedTip(); }, promptInterval));
break;
}
case RecommendationsNotificationResult.Ignored:
// Ignored: Remove from the cache and prompt next recommendation
this.mediumImportanceTipsByExe.delete(exeName);
this.promptMediumImportanceExeBasedTip();
break;
case RecommendationsNotificationResult.IncompatibleWindow:
case RecommendationsNotificationResult.IncompatibleWindow: {
// Recommended in incompatible window. Schedule the prompt after active window change
const onActiveWindowChange = Event.once(Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow)));
this._register(onActiveWindowChange(() => this.promptMediumImportanceExeBasedTip()));
break;
case RecommendationsNotificationResult.TooMany:
}
case RecommendationsNotificationResult.TooMany: {
// Too many notifications. Schedule the prompt after one hour
const disposable2 = this._register(disposableTimeout(() => { disposable2.dispose(); this.promptMediumImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */));
break;
}
}
});
}
private promptExeRecommendations(tips: IExecutableBasedExtensionTip[]): Promise<RecommendationsNotificationResult> {
const extensionIds = tips.map(({ extensionId }) => extensionId.toLowerCase());
private async promptExeRecommendations(tips: IExecutableBasedExtensionTip[]): Promise<RecommendationsNotificationResult> {
const installed = await this.extensionManagementService.getInstalled(ExtensionType.User);
const extensionIds = tips
.filter(tip => !tip.whenNotInstalled || tip.whenNotInstalled.every(id => installed.every(local => !areSameExtensions(local.identifier, { id }))))
.map(({ extensionId }) => extensionId.toLowerCase());
const message = localize({ key: 'exeRecommended', comment: ['Placeholder string is the name of the software that is installed.'] }, "You have {0} installed on your system. Do you want to install the recommended extensions for it?", tips[0].exeFriendlyName);
return this.extensionRecommendationNotificationService.promptImportantExtensionsInstallNotification(extensionIds, message, `@exe:"${tips[0].exeName}"`, RecommendationSource.EXE);
}
@@ -268,7 +276,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
this.storageService.store(promptedExecutableTipsStorageKey, JSON.stringify(promptedExecutableTips), StorageScope.GLOBAL, StorageTarget.USER);
}
private groupByInstalled(recommendationsToSuggest: string[], local: ILocalExtension[]): { installed: string[], uninstalled: string[] } {
private groupByInstalled(recommendationsToSuggest: string[], local: ILocalExtension[]): { installed: string[]; uninstalled: string[] } {
const installed: string[] = [], uninstalled: string[] = [];
const installedExtensionsIds = local.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set<string>());
recommendationsToSuggest.forEach(id => {
@@ -313,7 +321,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
checkedExecutables.set(exePath, exists);
}
if (exists) {
for (const { extensionId, extensionName, isExtensionPack } of extensionTip.recommendations) {
for (const { extensionId, extensionName, isExtensionPack, whenNotInstalled } of extensionTip.recommendations) {
result.push({
extensionId,
extensionName,
@@ -321,6 +329,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
exeName,
exeFriendlyName: extensionTip.exeFriendlyName,
windowsPath: extensionTip.windowsPath,
whenNotInstalled: whenNotInstalled
});
}
}

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { IExtensionsScannerService, NativeExtensionsScannerService, } from 'vs/platform/extensionManagement/common/extensionsScannerService';
import { IFileService } from 'vs/platform/files/common/files';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
export class ExtensionsScannerService extends NativeExtensionsScannerService implements IExtensionsScannerService {
constructor(
@IFileService fileService: IFileService,
@ILogService logService: ILogService,
@INativeEnvironmentService environmentService: INativeEnvironmentService,
@IProductService productService: IProductService,
) {
super(
URI.file(environmentService.builtinExtensionsPath),
URI.file(environmentService.extensionsPath),
environmentService.userHome,
URI.file(environmentService.userDataPath),
fileService, logService, environmentService, productService);
}
}
registerSingleton(IExtensionsScannerService, ExtensionsScannerService);

View File

@@ -13,13 +13,11 @@ import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { Promises as FSPromises } from 'vs/base/node/pfs';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { IExtensionGalleryService, IGalleryExtension, InstallOperation, TargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionIdentifierWithVersion, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
const ExtensionIdVersionRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)$/;
export class ExtensionsDownloader extends Disposable {
private readonly extensionsDownloadDir: URI;
@@ -97,9 +95,9 @@ export class ExtensionsDownloader extends Disposable {
const folderStat = await this.fileService.resolve(this.extensionsDownloadDir, { resolveMetadata: true });
if (folderStat.children) {
const toDelete: URI[] = [];
const all: [ExtensionIdentifierWithVersion, IFileStatWithMetadata][] = [];
const all: [ExtensionKey, IFileStatWithMetadata][] = [];
for (const stat of folderStat.children) {
const extension = this.parse(stat.name);
const extension = ExtensionKey.parse(stat.name);
if (extension) {
all.push([extension, stat]);
}
@@ -124,11 +122,7 @@ export class ExtensionsDownloader extends Disposable {
}
private getName(extension: IGalleryExtension): string {
return this.cache ? `${new ExtensionIdentifierWithVersion(extension.identifier, extension.version).key().toLowerCase()}${extension.properties.targetPlatform !== TargetPlatform.UNDEFINED ? `-${extension.properties.targetPlatform}` : ''}` : generateUuid();
return this.cache ? ExtensionKey.create(extension).toString().toLowerCase() : generateUuid();
}
private parse(name: string): ExtensionIdentifierWithVersion | null {
const matches = ExtensionIdVersionRegex.exec(name);
return matches && matches[1] && matches[2] ? new ExtensionIdentifierWithVersion({ id: matches[1] }, matches[2]) : null;
}
}

View File

@@ -37,7 +37,7 @@ export class ExtensionsLifecycle extends Disposable {
return Promises.rm(this.getExtensionStoragePath(extension)).then(undefined, e => this.logService.error('Error while removing extension storage path', e));
}
private parseScript(extension: ILocalExtension, type: string): { script: string, args: string[] } | null {
private parseScript(extension: ILocalExtension, type: string): { script: string; args: string[] } | null {
const scriptKey = `vscode:${type}`;
if (extension.location.scheme === Schemas.file && extension.manifest && extension.manifest['scripts'] && typeof extension.manifest['scripts'][scriptKey] === 'string') {
const script = (<string>extension.manifest['scripts'][scriptKey]).split(' ');
@@ -97,7 +97,7 @@ export class ExtensionsLifecycle extends Disposable {
const extensionUninstallProcess = fork(uninstallHook, [`--type=extension-post-${lifecycleType}`, ...args], opts);
// Catch all output coming from the process
type Output = { data: string, format: string[] };
type Output = { data: string; format: string[] };
extensionUninstallProcess.stdout!.setEncoding('utf8');
extensionUninstallProcess.stderr!.setEncoding('utf8');

View File

@@ -4,46 +4,50 @@
*--------------------------------------------------------------------------------------------*/
import { extensionsWorkbenchServiceIncompatible } from 'sql/base/common/locConstants';
import { Promises, Queue } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStringDictionary } from 'vs/base/common/collections';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { getErrorMessage } from 'vs/base/common/errors';
import { Disposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import * as path from 'vs/base/common/path';
import { isLinux, isMacintosh, platform } from 'vs/base/common/platform';
import { arch } from 'vs/base/common/process';
import { isMacintosh, isWindows } from 'vs/base/common/platform';
import { joinPath } from 'vs/base/common/resources';
import * as semver from 'vs/base/common/semver/semver';
import { isBoolean, isUndefined } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import * as pfs from 'vs/base/node/pfs';
import { IFile, zip } from 'vs/base/node/zip';
import { extract, ExtractError, IFile, zip } from 'vs/base/node/zip';
import * as nls from 'vs/nls';
import { IDownloadService } from 'vs/platform/download/common/download';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, joinErrors, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService';
import {
ExtensionManagementError, ExtensionManagementErrorCode, getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, InstallOptions,
InstallVSIXOptions, TargetPlatform
ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, InstallOptions,
InstallVSIXOptions, Metadata
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IExtensionsScannerService, IScannedExtension, ScanOptions } from 'vs/platform/extensionManagement/common/extensionsScannerService';
import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader';
import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle';
import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache';
import { ExtensionsScanner, ILocalExtensionManifest, IMetadata } from 'vs/platform/extensionManagement/node/extensionsScanner';
import { ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher';
import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
import { IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
interface InstallableExtension {
zipPath: string;
identifierWithVersion: ExtensionIdentifierWithVersion;
metadata?: IMetadata;
key: ExtensionKey;
metadata?: Metadata;
}
export class ExtensionManagementService extends AbstractExtensionManagementService implements IExtensionManagementService {
@@ -60,14 +64,15 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
@IDownloadService private downloadService: IDownloadService,
@IInstantiationService instantiationService: IInstantiationService,
@IFileService private readonly fileService: IFileService,
@IProductService productService: IProductService
@IProductService productService: IProductService,
@IUriIdentityService uriIdentityService: IUriIdentityService
) {
super(galleryService, telemetryService, logService, productService);
const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle));
this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension)));
this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this));
this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader));
const extensionsWatcher = this._register(new ExtensionsWatcher(this, fileService, environmentService, logService));
const extensionsWatcher = this._register(new ExtensionsWatcher(this, fileService, environmentService, logService, uriIdentityService));
this._register(extensionsWatcher.onDidChangeExtensionsByAnotherSource(({ added, removed }) => {
if (added.length) {
@@ -80,36 +85,11 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
private _targetPlatformPromise: Promise<TargetPlatform> | undefined;
getTargetPlatform(): Promise<TargetPlatform> {
if (!this._targetPlatformPromise) {
this._targetPlatformPromise = (async () => {
const isAlpineLinux = await this.isAlpineLinux();
const targetPlatform = getTargetPlatform(isAlpineLinux ? 'alpine' : platform, arch);
this.logService.debug('ExtensionManagementService#TargetPlatform:', targetPlatform);
return targetPlatform;
})();
this._targetPlatformPromise = computeTargetPlatform(this.fileService, this.logService);
}
return this._targetPlatformPromise;
}
private async isAlpineLinux(): Promise<boolean> {
if (!isLinux) {
return false;
}
let content: string | undefined;
try {
const fileContent = await this.fileService.readFile(URI.file('/etc/os-release'));
content = fileContent.value.toString();
} catch (error) {
try {
const fileContent = await this.fileService.readFile(URI.file('/usr/lib/os-release'));
content = fileContent.value.toString();
} catch (error) {
/* Ignore */
this.logService.debug(`Error while getting the os-release file.`, getErrorMessage(error));
}
}
return !!content && (content.match(/^ID=([^\u001b\r\n]*)/m) || [])[1] === 'alpine';
}
async zip(extension: ILocalExtension): Promise<URI> {
this.logService.trace('ExtensionManagementService#zip', extension.identifier.id);
const files = await this.collectFiles(extension);
@@ -157,14 +137,18 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension> {
this.logService.trace('ExtensionManagementService#updateMetadata', local.identifier.id);
local = await this.extensionsScanner.saveMetadataForLocalExtension(local, { ...((<ILocalExtensionManifest>local.manifest).__metadata || {}), ...metadata });
const localMetadata: Metadata = { ...metadata };
if (metadata.isPreReleaseVersion) {
localMetadata.preRelease = true;
}
local = await this.extensionsScanner.updateMetadata(local, localMetadata);
this.manifestCache.invalidate();
return local;
}
async updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise<ILocalExtension> {
this.logService.trace('ExtensionManagementService#updateExtensionScope', local.identifier.id);
local = await this.extensionsScanner.saveMetadataForLocalExtension(local, { ...((<ILocalExtensionManifest>local.manifest).__metadata || {}), isMachineScoped });
local = await this.extensionsScanner.updateMetadata(local, { isMachineScoped });
this.manifestCache.invalidate();
return local;
}
@@ -217,14 +201,281 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
}
class ExtensionsScanner extends Disposable {
private readonly uninstalledPath: string;
private readonly uninstalledFileLimiter: Queue<any>;
constructor(
private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise<void>,
@IFileService private readonly fileService: IFileService,
@IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService,
@ILogService private readonly logService: ILogService,
) {
super();
this.uninstalledPath = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete').fsPath;
this.uninstalledFileLimiter = new Queue();
}
async cleanUp(): Promise<void> {
await this.removeUninstalledExtensions();
await this.removeOutdatedExtensions();
}
async scanExtensions(type: ExtensionType | null): Promise<ILocalExtension[]> {
const scannedOptions: ScanOptions = { includeInvalid: true };
let scannedExtensions: IScannedExtension[] = [];
if (type === null || type === ExtensionType.System) {
scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions(scannedOptions));
} else if (type === ExtensionType.User) {
scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(scannedOptions));
}
scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions;
return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension)));
}
async scanUserExtensions(excludeOutdated: boolean): Promise<ILocalExtension[]> {
const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true });
return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension)));
}
async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, metadata: Metadata | undefined, token: CancellationToken): Promise<ILocalExtension> {
const folderName = extensionKey.toString();
const tempPath = path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, `.${generateUuid()}`);
const extensionPath = path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, folderName);
try {
await pfs.Promises.rm(extensionPath);
} catch (error) {
throw new ExtensionManagementError(nls.localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, extensionKey.id), ExtensionManagementErrorCode.Delete);
}
await this.extractAtLocation(extensionKey, zipPath, tempPath, token);
await this.extensionsScannerService.updateMetadata(URI.file(tempPath), { ...metadata, installedTimestamp: Date.now() });
try {
await this.rename(extensionKey, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
this.logService.info('Renamed to', extensionPath);
} catch (error) {
try {
await pfs.Promises.rm(tempPath);
} catch (e) { /* ignore */ }
if (error.code === 'ENOTEMPTY') {
this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, extensionKey.id);
} else {
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempPath);
throw error;
}
}
return this.scanLocalExtension(URI.file(extensionPath), ExtensionType.User);
}
async updateMetadata(local: ILocalExtension, metadata: Partial<Metadata>): Promise<ILocalExtension> {
await this.extensionsScannerService.updateMetadata(local.location, metadata);
return this.scanLocalExtension(local.location, local.type);
}
getUninstalledExtensions(): Promise<IStringDictionary<boolean>> {
return this.withUninstalledExtensions();
}
async setUninstalled(...extensions: ILocalExtension[]): Promise<void> {
const extensionKeys: ExtensionKey[] = extensions.map(e => ExtensionKey.create(e));
await this.withUninstalledExtensions(uninstalled => {
extensionKeys.forEach(extensionKey => uninstalled[extensionKey.toString()] = true);
});
}
async setInstalled(extensionKey: ExtensionKey): Promise<ILocalExtension | null> {
await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]);
const userExtensions = await this.scanUserExtensions(true);
const localExtension = userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)) || null;
if (!localExtension) {
return null;
}
return this.updateMetadata(localExtension, { installedTimestamp: Date.now() });
}
async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise<void> {
this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath);
await pfs.Promises.rm(extension.location.fsPath);
this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath);
}
async removeUninstalledExtension(extension: ILocalExtension | IScannedExtension): Promise<void> {
await this.removeExtension(extension, 'uninstalled');
await this.withUninstalledExtensions(uninstalled => delete uninstalled[ExtensionKey.create(extension).toString()]);
}
private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary<boolean>) => void): Promise<IStringDictionary<boolean>> {
return this.uninstalledFileLimiter.queue(async () => {
let raw: string | undefined;
try {
raw = await pfs.Promises.readFile(this.uninstalledPath, 'utf8');
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
let uninstalled = {};
if (raw) {
try {
uninstalled = JSON.parse(raw);
} catch (e) { /* ignore */ }
}
if (updateFn) {
updateFn(uninstalled);
if (Object.keys(uninstalled).length) {
await pfs.Promises.writeFile(this.uninstalledPath, JSON.stringify(uninstalled));
} else {
await pfs.Promises.rm(this.uninstalledPath);
}
}
return uninstalled;
});
}
private async extractAtLocation(identifier: IExtensionIdentifier, zipPath: string, location: string, token: CancellationToken): Promise<void> {
this.logService.trace(`Started extracting the extension from ${zipPath} to ${location}`);
// Clean the location
try {
await pfs.Promises.rm(location);
} catch (e) {
throw new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Delete);
}
try {
await extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token);
this.logService.info(`Extracted extension to ${location}:`, identifier.id);
} catch (e) {
try { await pfs.Promises.rm(location); } catch (e) { /* Ignore */ }
let errorCode = ExtensionManagementErrorCode.Extract;
if (e instanceof ExtractError) {
if (e.type === 'CorruptZip') {
errorCode = ExtensionManagementErrorCode.CorruptZip;
} else if (e.type === 'Incomplete') {
errorCode = ExtensionManagementErrorCode.IncompleteZip;
}
}
throw new ExtensionManagementError(e.message, errorCode);
}
}
private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise<void> {
try {
await pfs.Promises.rename(extractPath, renamePath);
} catch (error) {
if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) {
this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id);
return this.rename(identifier, extractPath, renamePath, retryUntil);
}
throw new ExtensionManagementError(error.message || nls.localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || ExtensionManagementErrorCode.Rename);
}
}
private async scanLocalExtension(location: URI, type: ExtensionType): Promise<ILocalExtension> {
const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true });
if (scannedExtension) {
return this.toLocalExtension(scannedExtension);
}
throw new Error(nls.localize('cannot read', "Cannot read the extension from {0}", location.path));
}
private async toLocalExtension(extension: IScannedExtension): Promise<ILocalExtension> {
const stat = await this.fileService.resolve(extension.location);
let readmeUrl: URI | undefined;
let changelogUrl: URI | undefined;
if (stat.children) {
readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource;
changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource;
}
return {
identifier: extension.identifier,
type: extension.type,
isBuiltin: extension.isBuiltin || !!extension.metadata?.isBuiltin,
location: extension.location,
manifest: extension.manifest,
targetPlatform: extension.targetPlatform,
validations: extension.validations,
isValid: extension.isValid,
readmeUrl,
changelogUrl,
publisherDisplayName: extension.metadata?.publisherDisplayName || null,
publisherId: extension.metadata?.publisherId || null,
isMachineScoped: !!extension.metadata?.isMachineScoped,
isPreReleaseVersion: !!extension.metadata?.isPreReleaseVersion,
preRelease: !!extension.metadata?.preRelease,
installedTimestamp: extension.metadata?.installedTimestamp,
updated: !!extension.metadata?.updated,
};
}
private async removeUninstalledExtensions(): Promise<void> {
const uninstalled = await this.getUninstalledExtensions();
const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions
const installed: Set<string> = new Set<string>();
for (const e of extensions) {
if (!uninstalled[ExtensionKey.create(e).toString()]) {
installed.add(e.identifier.id.toLowerCase());
}
}
const byExtension = groupByExtension(extensions, e => e.identifier);
await Promises.settled(byExtension.map(async e => {
const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0];
if (!installed.has(latest.identifier.id.toLowerCase())) {
await this.beforeRemovingExtension(await this.toLocalExtension(latest));
}
}));
const toRemove = extensions.filter(e => uninstalled[ExtensionKey.create(e).toString()]);
await Promises.settled(toRemove.map(e => this.removeUninstalledExtension(e)));
}
private async removeOutdatedExtensions(): Promise<void> {
const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions
const toRemove: IScannedExtension[] = [];
// Outdated extensions
const targetPlatform = await this.extensionsScannerService.getTargetPlatform();
const byExtension = groupByExtension(extensions, e => e.identifier);
toRemove.push(...byExtension.map(p => p.sort((a, b) => {
const vcompare = semver.rcompare(a.manifest.version, b.manifest.version);
if (vcompare !== 0) {
return vcompare;
}
if (a.targetPlatform === targetPlatform) {
return -1;
}
return 1;
}).slice(1)).flat());
await Promises.settled(toRemove.map(extension => this.removeExtension(extension, 'outdated')));
}
private joinErrors(errorOrErrors: (Error | string) | (Array<Error | string>)): Error {
const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors];
if (errors.length === 1) {
return errors[0] instanceof Error ? <Error>errors[0] : new Error(<string>errors[0]);
}
return errors.reduce<Error>((previousValue: Error, currentValue: Error | string) => {
return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`);
}, new Error(''));
}
}
abstract class AbstractInstallExtensionTask extends AbstractExtensionTask<ILocalExtension> implements IInstallExtensionTask {
protected _operation = InstallOperation.Install;
get operation() { return this._operation; }
get operation() { return isUndefined(this.options.operation) ? this._operation : this.options.operation; }
constructor(
readonly identifier: IExtensionIdentifier,
readonly source: URI | IGalleryExtension,
protected readonly options: InstallOptions,
protected readonly extensionsScanner: ExtensionsScanner,
protected readonly logService: ILogService,
) {
@@ -233,9 +484,9 @@ abstract class AbstractInstallExtensionTask extends AbstractExtensionTask<ILocal
protected async installExtension(installableExtension: InstallableExtension, token: CancellationToken): Promise<ILocalExtension> {
try {
const local = await this.unsetUninstalledAndGetLocal(installableExtension.identifierWithVersion);
const local = await this.unsetUninstalledAndGetLocal(installableExtension.key);
if (local) {
return installableExtension.metadata ? this.extensionsScanner.saveMetadataForLocalExtension(local, installableExtension.metadata) : local;
return installableExtension.metadata ? this.extensionsScanner.updateMetadata(local, installableExtension.metadata) : local;
}
} catch (e) {
if (isMacintosh) {
@@ -247,28 +498,28 @@ abstract class AbstractInstallExtensionTask extends AbstractExtensionTask<ILocal
return this.extract(installableExtension, token);
}
protected async unsetUninstalledAndGetLocal(identifierWithVersion: ExtensionIdentifierWithVersion): Promise<ILocalExtension | null> {
const isUninstalled = await this.isUninstalled(identifierWithVersion);
protected async unsetUninstalledAndGetLocal(extensionKey: ExtensionKey): Promise<ILocalExtension | null> {
const isUninstalled = await this.isUninstalled(extensionKey);
if (!isUninstalled) {
return null;
}
this.logService.trace('Removing the extension from uninstalled list:', identifierWithVersion.id);
this.logService.trace('Removing the extension from uninstalled list:', extensionKey.id);
// If the same version of extension is marked as uninstalled, remove it from there and return the local.
const local = await this.extensionsScanner.setInstalled(identifierWithVersion);
this.logService.info('Removed the extension from uninstalled list:', identifierWithVersion.id);
const local = await this.extensionsScanner.setInstalled(extensionKey);
this.logService.info('Removed the extension from uninstalled list:', extensionKey.id);
return local;
}
private async isUninstalled(identifier: ExtensionIdentifierWithVersion): Promise<boolean> {
private async isUninstalled(extensionId: ExtensionKey): Promise<boolean> {
const uninstalled = await this.extensionsScanner.getUninstalledExtensions();
return !!uninstalled[identifier.key()];
return !!uninstalled[extensionId.toString()];
}
private async extract({ zipPath, identifierWithVersion, metadata }: InstallableExtension, token: CancellationToken): Promise<ILocalExtension> {
let local = await this.extensionsScanner.extractUserExtension(identifierWithVersion, zipPath, metadata, token);
this.logService.info('Extracting completed.', identifierWithVersion.id);
private async extract({ zipPath, key, metadata }: InstallableExtension, token: CancellationToken): Promise<ILocalExtension> {
let local = await this.extensionsScanner.extractUserExtension(key, zipPath, metadata, token);
this.logService.info('Extracting completed.', key.id);
return local;
}
@@ -278,12 +529,12 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask {
constructor(
private readonly gallery: IGalleryExtension,
private readonly options: InstallOptions,
options: InstallOptions,
private readonly extensionsDownloader: ExtensionsDownloader,
extensionsScanner: ExtensionsScanner,
logService: ILogService,
) {
super(gallery.identifier, gallery, extensionsScanner, logService);
super(gallery.identifier, gallery, options, extensionsScanner, logService);
}
protected async doRun(token: CancellationToken): Promise<ILocalExtension> {
@@ -296,10 +547,17 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask {
const installableExtension = await this.downloadInstallableExtension(this.gallery, this._operation);
installableExtension.metadata.isMachineScoped = this.options.isMachineScoped || existingExtension?.isMachineScoped;
installableExtension.metadata.isBuiltin = this.options.isBuiltin || existingExtension?.isBuiltin;
installableExtension.metadata.isSystem = existingExtension?.type === ExtensionType.System ? true : undefined;
installableExtension.metadata.updated = !!existingExtension;
installableExtension.metadata.isPreReleaseVersion = this.gallery.properties.isPreReleaseVersion;
installableExtension.metadata.preRelease = this.gallery.properties.isPreReleaseVersion ||
(isBoolean(this.options.installPreReleaseVersion)
? this.options.installPreReleaseVersion /* Respect the passed flag */
: existingExtension?.preRelease /* Respect the existing pre-release flag if it was set */);
try {
const local = await this.installExtension(installableExtension, token);
if (existingExtension && semver.neq(existingExtension.manifest.version, this.gallery.version)) {
if (existingExtension && (existingExtension.targetPlatform !== local.targetPlatform || semver.neq(existingExtension.manifest.version, local.manifest.version))) {
await this.extensionsScanner.setUninstalled(existingExtension);
}
return local;
@@ -323,6 +581,7 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask {
id: extension.identifier.uuid,
publisherId: extension.publisherId,
publisherDisplayName: extension.publisherDisplayName,
targetPlatform: extension.properties.targetPlatform
};
let zipPath: string | undefined;
@@ -335,8 +594,8 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask {
}
try {
const manifest = await getManifest(zipPath);
return (<Required<InstallableExtension>>{ zipPath, identifierWithVersion: new ExtensionIdentifierWithVersion(extension.identifier, manifest.version), metadata });
await getManifest(zipPath);
return (<Required<InstallableExtension>>{ zipPath, key: ExtensionKey.create(extension), metadata });
} catch (error) {
await this.deleteDownloadedVSIX(zipPath);
throw new ExtensionManagementError(joinErrors(error).message, ExtensionManagementErrorCode.Invalid);
@@ -349,25 +608,25 @@ class InstallVSIXTask extends AbstractInstallExtensionTask {
constructor(
private readonly manifest: IExtensionManifest,
private readonly location: URI,
private readonly options: InstallOptions,
options: InstallOptions,
private readonly galleryService: IExtensionGalleryService,
extensionsScanner: ExtensionsScanner,
logService: ILogService
) {
super({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, location, extensionsScanner, logService);
super({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, location, options, extensionsScanner, logService);
}
protected async doRun(token: CancellationToken): Promise<ILocalExtension> {
const identifierWithVersion = new ExtensionIdentifierWithVersion(this.identifier, this.manifest.version);
const extensionKey = new ExtensionKey(this.identifier, this.manifest.version);
const installedExtensions = await this.extensionsScanner.scanExtensions(ExtensionType.User);
const existing = installedExtensions.find(i => areSameExtensions(this.identifier, i.identifier));
const metadata = await this.getMetadata(this.identifier.id, token);
const metadata = await this.getMetadata(this.identifier.id, this.manifest.version, token);
metadata.isMachineScoped = this.options.isMachineScoped || existing?.isMachineScoped;
metadata.isBuiltin = this.options.isBuiltin || existing?.isBuiltin;
if (existing) {
this._operation = InstallOperation.Update;
if (identifierWithVersion.equals(new ExtensionIdentifierWithVersion(existing.identifier, existing.manifest.version))) {
if (extensionKey.equals(new ExtensionKey(existing.identifier, existing.manifest.version))) {
try {
await this.extensionsScanner.removeExtension(existing, 'existing');
} catch (e) {
@@ -379,7 +638,7 @@ class InstallVSIXTask extends AbstractInstallExtensionTask {
} else {
// Remove the extension with same version if it is already uninstalled.
// Installing a VSIX extension shall replace the existing extension always.
const existing = await this.unsetUninstalledAndGetLocal(identifierWithVersion);
const existing = await this.unsetUninstalledAndGetLocal(extensionKey);
if (existing) {
try {
await this.extensionsScanner.removeExtension(existing, 'existing');
@@ -389,14 +648,23 @@ class InstallVSIXTask extends AbstractInstallExtensionTask {
}
}
return this.installExtension({ zipPath: path.resolve(this.location.fsPath), identifierWithVersion, metadata }, token);
return this.installExtension({ zipPath: path.resolve(this.location.fsPath), key: extensionKey, metadata }, token);
}
private async getMetadata(name: string, token: CancellationToken): Promise<IMetadata> {
private async getMetadata(id: string, version: string, token: CancellationToken): Promise<Metadata> {
try {
const galleryExtension = (await this.galleryService.query({ names: [name], pageSize: 1 }, token)).firstPage[0];
let [galleryExtension] = await this.galleryService.getExtensions([{ id, version }], token);
if (!galleryExtension) {
[galleryExtension] = await this.galleryService.getExtensions([{ id }], token);
}
if (galleryExtension) {
return { id: galleryExtension.identifier.uuid, publisherDisplayName: galleryExtension.publisherDisplayName, publisherId: galleryExtension.publisherId };
return {
id: galleryExtension.identifier.uuid,
publisherDisplayName: galleryExtension.publisherDisplayName,
publisherId: galleryExtension.publisherId,
isPreReleaseVersion: galleryExtension.properties.isPreReleaseVersion,
preRelease: galleryExtension.properties.isPreReleaseVersion || this.options.installPreReleaseVersion
};
}
} catch (error) {
/* Ignore Error */
@@ -417,8 +685,8 @@ class UninstallExtensionTask extends AbstractExtensionTask<void> implements IUni
const toUninstall: ILocalExtension[] = [];
const userExtensions = await this.extensionsScanner.scanUserExtensions(false);
if (this.options.versionOnly) {
const extensionIdentifierWithVersion = new ExtensionIdentifierWithVersion(this.extension.identifier, this.extension.manifest.version);
toUninstall.push(...userExtensions.filter(u => extensionIdentifierWithVersion.equals(new ExtensionIdentifierWithVersion(u.identifier, u.manifest.version))));
const extensionKey = ExtensionKey.create(this.extension);
toUninstall.push(...userExtensions.filter(u => extensionKey.equals(ExtensionKey.create(u))));
} else {
toUninstall.push(...userExtensions.filter(u => areSameExtensions(u.identifier, this.extension.identifier)));
}

View File

@@ -1,117 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { join, } from 'vs/base/common/path';
import { IProductService } from 'vs/platform/product/common/productService';
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { env as processEnv } from 'vs/base/common/process';
import { IFileService } from 'vs/platform/files/common/files';
import { isWindows } from 'vs/base/common/platform';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IExecutableBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement';
import { forEach } from 'vs/base/common/collections';
import { IRequestService } from 'vs/platform/request/common/request';
import { ILogService } from 'vs/platform/log/common/log';
import { ExtensionTipsService as BaseExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionTipsService';
type IExeBasedExtensionTips = {
readonly exeFriendlyName: string,
readonly windowsPath?: string,
readonly recommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[];
};
export class ExtensionTipsService extends BaseExtensionTipsService {
private readonly allImportantExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
private readonly allOtherExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
constructor(
@IEnvironmentService private readonly environmentService: INativeEnvironmentService,
@IFileService fileService: IFileService,
@IProductService productService: IProductService,
@IRequestService requestService: IRequestService,
@ILogService logService: ILogService,
) {
super(fileService, productService, requestService, logService);
if (productService.exeBasedExtensionTips) {
forEach(productService.exeBasedExtensionTips, ({ key, value }) => {
const importantRecommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[] = [];
const otherRecommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[] = [];
forEach(value.recommendations, ({ key: extensionId, value }) => {
if (value.important) {
importantRecommendations.push({ extensionId, extensionName: value.name, isExtensionPack: !!value.isExtensionPack });
} else {
otherRecommendations.push({ extensionId, extensionName: value.name, isExtensionPack: !!value.isExtensionPack });
}
});
if (importantRecommendations.length) {
this.allImportantExecutableTips.set(key, { exeFriendlyName: value.friendlyName, windowsPath: value.windowsPath, recommendations: importantRecommendations });
}
if (otherRecommendations.length) {
this.allOtherExecutableTips.set(key, { exeFriendlyName: value.friendlyName, windowsPath: value.windowsPath, recommendations: otherRecommendations });
}
});
}
}
override getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return this.getValidExecutableBasedExtensionTips(this.allImportantExecutableTips);
}
override getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return this.getValidExecutableBasedExtensionTips(this.allOtherExecutableTips);
}
private async getValidExecutableBasedExtensionTips(executableTips: Map<string, IExeBasedExtensionTips>): Promise<IExecutableBasedExtensionTip[]> {
const result: IExecutableBasedExtensionTip[] = [];
const checkedExecutables: Map<string, boolean> = new Map<string, boolean>();
for (const exeName of executableTips.keys()) {
const extensionTip = executableTips.get(exeName);
if (!extensionTip || !isNonEmptyArray(extensionTip.recommendations)) {
continue;
}
const exePaths: string[] = [];
if (isWindows) {
if (extensionTip.windowsPath) {
exePaths.push(extensionTip.windowsPath.replace('%USERPROFILE%', processEnv['USERPROFILE']!)
.replace('%ProgramFiles(x86)%', processEnv['ProgramFiles(x86)']!)
.replace('%ProgramFiles%', processEnv['ProgramFiles']!)
.replace('%APPDATA%', processEnv['APPDATA']!)
.replace('%WINDIR%', processEnv['WINDIR']!));
}
} else {
exePaths.push(join('/usr/local/bin', exeName));
exePaths.push(join('/usr/bin', exeName));
exePaths.push(join(this.environmentService.userHome.fsPath, exeName));
}
for (const exePath of exePaths) {
let exists = checkedExecutables.get(exePath);
if (exists === undefined) {
exists = await this.fileService.exists(URI.file(exePath));
checkedExecutables.set(exePath, exists);
}
if (exists) {
for (const { extensionId, extensionName, isExtensionPack } of extensionTip.recommendations) {
result.push({
extensionId,
extensionName,
isExtensionPack,
exeName,
exeFriendlyName: extensionTip.exeFriendlyName,
windowsPath: extensionTip.windowsPath,
});
}
}
}
}
return result;
}
}

View File

@@ -1,422 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { flatten } from 'vs/base/common/arrays';
import { Limiter, Promises, Queue } from 'vs/base/common/async';
import { IStringDictionary } from 'vs/base/common/collections';
import { getErrorMessage } from 'vs/base/common/errors';
import { Disposable } from 'vs/base/common/lifecycle';
import { FileAccess } from 'vs/base/common/network';
import * as path from 'vs/base/common/path';
import { isWindows } from 'vs/base/common/platform';
import { basename } from 'vs/base/common/resources';
import * as semver from 'vs/base/common/semver/semver';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import * as pfs from 'vs/base/node/pfs';
import { extract, ExtractError } from 'vs/base/node/zip';
import { localize } from 'vs/nls';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { ExtensionManagementError, ExtensionManagementErrorCode, IGalleryMetadata, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls';
import { ExtensionType, IExtensionIdentifier, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { CancellationToken } from 'vscode';
export type IMetadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean; }>;
type IStoredMetadata = IMetadata & { installedTimestamp: number | undefined };
export type ILocalExtensionManifest = IExtensionManifest & { __metadata?: IMetadata };
type IRelaxedLocalExtension = Omit<ILocalExtension, 'isBuiltin'> & { isBuiltin: boolean };
export class ExtensionsScanner extends Disposable {
private readonly systemExtensionsPath: string;
private readonly extensionsPath: string;
private readonly uninstalledPath: string;
private readonly uninstalledFileLimiter: Queue<any>;
constructor(
private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise<void>,
@IFileService private readonly fileService: IFileService,
@ILogService private readonly logService: ILogService,
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
@IProductService private readonly productService: IProductService,
) {
super();
this.systemExtensionsPath = environmentService.builtinExtensionsPath;
this.extensionsPath = environmentService.extensionsPath;
this.uninstalledPath = path.join(this.extensionsPath, '.obsolete');
this.uninstalledFileLimiter = new Queue();
}
async cleanUp(): Promise<void> {
await this.removeUninstalledExtensions();
await this.removeOutdatedExtensions();
}
async scanExtensions(type: ExtensionType | null): Promise<ILocalExtension[]> {
const promises: Promise<ILocalExtension[]>[] = [];
if (type === null || type === ExtensionType.System) {
promises.push(this.scanSystemExtensions().then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Internal))));
}
if (type === null || type === ExtensionType.User) {
promises.push(this.scanUserExtensions(true).then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Internal))));
}
try {
const result = await Promise.all(promises);
return flatten(result);
} catch (error) {
throw this.joinErrors(error);
}
}
async scanUserExtensions(excludeOutdated: boolean): Promise<ILocalExtension[]> {
this.logService.trace('Started scanning user extensions');
let [uninstalled, extensions] = await Promise.all([this.getUninstalledExtensions(), this.scanAllUserExtensions()]);
extensions = extensions.filter(e => !uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]);
if (excludeOutdated) {
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier);
extensions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]);
}
this.logService.trace('Scanned user extensions:', extensions.length);
return extensions;
}
async scanAllUserExtensions(): Promise<ILocalExtension[]> {
return this.scanExtensionsInDir(this.extensionsPath, ExtensionType.User);
}
async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IMetadata | undefined, token: CancellationToken): Promise<ILocalExtension> {
const folderName = identifierWithVersion.key();
const tempPath = path.join(this.extensionsPath, `.${generateUuid()}`);
const extensionPath = path.join(this.extensionsPath, folderName);
try {
await pfs.Promises.rm(extensionPath);
} catch (error) {
try {
await pfs.Promises.rm(extensionPath);
} catch (e) { /* ignore */ }
throw new ExtensionManagementError(localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, identifierWithVersion.id), ExtensionManagementErrorCode.Delete);
}
await this.extractAtLocation(identifierWithVersion, zipPath, tempPath, token);
let local = await this.scanExtension(URI.file(tempPath), ExtensionType.User);
if (!local) {
throw new Error(localize('cannot read', "Cannot read the extension from {0}", tempPath));
}
await this.storeMetadata(local, { ...metadata, installedTimestamp: Date.now() });
try {
await this.rename(identifierWithVersion, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
this.logService.info('Renamed to', extensionPath);
} catch (error) {
try {
await pfs.Promises.rm(tempPath);
} catch (e) { /* ignore */ }
if (error.code === 'ENOTEMPTY') {
this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, identifierWithVersion.id);
} else {
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempPath);
throw error;
}
}
try {
local = await this.scanExtension(URI.file(extensionPath), ExtensionType.User);
} catch (e) { /*ignore */ }
if (local) {
return local;
}
throw new Error(localize('cannot read', "Cannot read the extension from {0}", this.extensionsPath));
}
async saveMetadataForLocalExtension(local: ILocalExtension, metadata: IMetadata): Promise<ILocalExtension> {
this.setMetadata(local, metadata);
await this.storeMetadata(local, { ...metadata, installedTimestamp: local.installedTimestamp });
return local;
}
private async storeMetadata(local: ILocalExtension, storedMetadata: IStoredMetadata): Promise<ILocalExtension> {
// unset if false
storedMetadata.isMachineScoped = storedMetadata.isMachineScoped || undefined;
storedMetadata.isBuiltin = storedMetadata.isBuiltin || undefined;
storedMetadata.installedTimestamp = storedMetadata.installedTimestamp || undefined;
const manifestPath = path.join(local.location.fsPath, 'package.json');
const raw = await pfs.Promises.readFile(manifestPath, 'utf8');
const { manifest } = await this.parseManifest(raw);
(manifest as ILocalExtensionManifest).__metadata = storedMetadata;
await pfs.Promises.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'));
return local;
}
getUninstalledExtensions(): Promise<IStringDictionary<boolean>> {
return this.withUninstalledExtensions();
}
async setUninstalled(...extensions: ILocalExtension[]): Promise<void> {
const ids: ExtensionIdentifierWithVersion[] = extensions.map(e => new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version));
await this.withUninstalledExtensions(uninstalled => {
ids.forEach(id => uninstalled[id.key()] = true);
});
}
async setInstalled(identifierWithVersion: ExtensionIdentifierWithVersion): Promise<ILocalExtension | null> {
await this.withUninstalledExtensions(uninstalled => delete uninstalled[identifierWithVersion.key()]);
const installed = await this.scanExtensions(ExtensionType.User);
const localExtension = installed.find(i => new ExtensionIdentifierWithVersion(i.identifier, i.manifest.version).equals(identifierWithVersion)) || null;
if (!localExtension) {
return null;
}
await this.storeMetadata(localExtension, { installedTimestamp: Date.now() });
return this.scanExtension(localExtension.location, ExtensionType.User);
}
private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary<boolean>) => void): Promise<IStringDictionary<boolean>> {
return this.uninstalledFileLimiter.queue(async () => {
let raw: string | undefined;
try {
raw = await pfs.Promises.readFile(this.uninstalledPath, 'utf8');
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
let uninstalled = {};
if (raw) {
try {
uninstalled = JSON.parse(raw);
} catch (e) { /* ignore */ }
}
if (updateFn) {
updateFn(uninstalled);
if (Object.keys(uninstalled).length) {
await pfs.Promises.writeFile(this.uninstalledPath, JSON.stringify(uninstalled));
} else {
await pfs.Promises.rm(this.uninstalledPath);
}
}
return uninstalled;
});
}
async removeExtension(extension: ILocalExtension, type: string): Promise<void> {
this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath);
await pfs.Promises.rm(extension.location.fsPath);
this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath);
}
async removeUninstalledExtension(extension: ILocalExtension): Promise<void> {
await this.removeExtension(extension, 'uninstalled');
await this.withUninstalledExtensions(uninstalled => delete uninstalled[new ExtensionIdentifierWithVersion(extension.identifier, extension.manifest.version).key()]);
}
private async extractAtLocation(identifier: IExtensionIdentifier, zipPath: string, location: string, token: CancellationToken): Promise<void> {
this.logService.trace(`Started extracting the extension from ${zipPath} to ${location}`);
// Clean the location
try {
await pfs.Promises.rm(location);
} catch (e) {
throw new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Delete);
}
try {
await extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token);
this.logService.info(`Extracted extension to ${location}:`, identifier.id);
} catch (e) {
try { await pfs.Promises.rm(location); } catch (e) { /* Ignore */ }
let errorCode = ExtensionManagementErrorCode.Extract;
if (e instanceof ExtractError) {
if (e.type === 'CorruptZip') {
errorCode = ExtensionManagementErrorCode.CorruptZip;
} else if (e.type === 'Incomplete') {
errorCode = ExtensionManagementErrorCode.IncompleteZip;
}
}
throw new ExtensionManagementError(e.message, errorCode);
}
}
private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise<void> {
try {
await pfs.Promises.rename(extractPath, renamePath);
} catch (error) {
if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) {
this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id);
return this.rename(identifier, extractPath, renamePath, retryUntil);
}
throw new ExtensionManagementError(error.message || localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || ExtensionManagementErrorCode.Rename);
}
}
private async scanSystemExtensions(): Promise<ILocalExtension[]> {
this.logService.trace('Started scanning system extensions');
const systemExtensionsPromise = this.scanDefaultSystemExtensions();
if (this.environmentService.isBuilt) {
return systemExtensionsPromise;
}
// Scan other system extensions during development
const devSystemExtensionsPromise = this.scanDevSystemExtensions();
const [systemExtensions, devSystemExtensions] = await Promise.all([systemExtensionsPromise, devSystemExtensionsPromise]);
return [...systemExtensions, ...devSystemExtensions];
}
private async scanExtensionsInDir(dir: string, type: ExtensionType): Promise<ILocalExtension[]> {
const limiter = new Limiter<any>(10);
const stat = await this.fileService.resolve(URI.file(dir));
if (stat.children) {
const extensions = await Promise.all<ILocalExtension>(stat.children.filter(c => c.isDirectory)
.map(c => limiter.queue(async () => {
if (type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) { // Do not consider user extension folder starting with `.`
return null;
}
return this.scanExtension(c.resource, type);
})));
return extensions.filter(e => e && e.identifier);
}
return [];
}
private async scanExtension(extensionLocation: URI, type: ExtensionType): Promise<ILocalExtension | null> {
try {
const stat = await this.fileService.resolve(extensionLocation);
if (stat.children) {
const { manifest, metadata } = await this.readManifest(extensionLocation.fsPath);
const readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource;
const changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource;
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
const local = <ILocalExtension>{ type, identifier, manifest, location: extensionLocation, readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System };
if (metadata) {
this.setMetadata(local, metadata);
local.installedTimestamp = metadata.installedTimestamp;
}
return local;
}
} catch (e) {
if (type !== ExtensionType.System) {
this.logService.trace(e);
}
}
return null;
}
private async scanDefaultSystemExtensions(): Promise<ILocalExtension[]> {
const result = await this.scanExtensionsInDir(this.systemExtensionsPath, ExtensionType.System);
this.logService.trace('Scanned system extensions:', result.length);
return result;
}
private async scanDevSystemExtensions(): Promise<ILocalExtension[]> {
const devSystemExtensionsList = this.getDevSystemExtensionsList();
if (devSystemExtensionsList.length) {
const result = await this.scanExtensionsInDir(this.devSystemExtensionsPath, ExtensionType.System);
this.logService.trace('Scanned dev system extensions:', result.length);
return result.filter(r => devSystemExtensionsList.some(id => areSameExtensions(r.identifier, { id })));
} else {
return [];
}
}
private setMetadata(local: IRelaxedLocalExtension, metadata: IMetadata): void {
local.publisherDisplayName = metadata.publisherDisplayName || null;
local.publisherId = metadata.publisherId || null;
local.identifier.uuid = metadata.id;
local.isMachineScoped = !!metadata.isMachineScoped;
local.isBuiltin = local.type === ExtensionType.System || !!metadata.isBuiltin;
}
private async removeUninstalledExtensions(): Promise<void> {
const uninstalled = await this.getUninstalledExtensions();
const extensions = await this.scanAllUserExtensions(); // All user extensions
const installed: Set<string> = new Set<string>();
for (const e of extensions) {
if (!uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]) {
installed.add(e.identifier.id.toLowerCase());
}
}
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier);
await Promises.settled(byExtension.map(async e => {
const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0];
if (!installed.has(latest.identifier.id.toLowerCase())) {
await this.beforeRemovingExtension(latest);
}
}));
const toRemove: ILocalExtension[] = extensions.filter(e => uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]);
await Promises.settled(toRemove.map(e => this.removeUninstalledExtension(e)));
}
private async removeOutdatedExtensions(): Promise<void> {
const extensions = await this.scanAllUserExtensions();
const toRemove: ILocalExtension[] = [];
// Outdated extensions
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier);
toRemove.push(...flatten(byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version)).slice(1))));
await Promises.settled(toRemove.map(extension => this.removeExtension(extension, 'outdated')));
}
private getDevSystemExtensionsList(): string[] {
return (this.productService.builtInExtensions || []).map(e => e.name);
}
private joinErrors(errorOrErrors: (Error | string) | (Array<Error | string>)): Error {
const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors];
if (errors.length === 1) {
return errors[0] instanceof Error ? <Error>errors[0] : new Error(<string>errors[0]);
}
return errors.reduce<Error>((previousValue: Error, currentValue: Error | string) => {
return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`);
}, new Error(''));
}
private _devSystemExtensionsPath: string | null = null;
private get devSystemExtensionsPath(): string {
if (!this._devSystemExtensionsPath) {
this._devSystemExtensionsPath = path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions'));
}
return this._devSystemExtensionsPath;
}
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IStoredMetadata | null; }> {
const promises = [
pfs.Promises.readFile(path.join(extensionPath, 'package.json'), 'utf8')
.then(raw => this.parseManifest(raw)),
pfs.Promises.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8')
.then(undefined, err => err.code !== 'ENOENT' ? Promise.reject<string>(err) : '{}')
.then(raw => JSON.parse(raw))
];
const [{ manifest, metadata }, translations] = await Promise.all(promises);
return {
manifest: localizeManifest(manifest, translations),
metadata
};
}
private parseManifest(raw: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> {
return new Promise((c, e) => {
try {
const manifest = JSON.parse(raw);
const metadata = manifest.__metadata || null;
c({ manifest, metadata });
} catch (err) {
e(new Error(localize('invalidManifest', "Extension invalid: package.json is not a JSON file.")));
}
});
}
}

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { IExtensionsScannerService, NativeExtensionsScannerService, } from 'vs/platform/extensionManagement/common/extensionsScannerService';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
export class ExtensionsScannerService extends NativeExtensionsScannerService implements IExtensionsScannerService {
constructor(
@IFileService fileService: IFileService,
@ILogService logService: ILogService,
@INativeEnvironmentService environmentService: INativeEnvironmentService,
@IProductService productService: IProductService,
) {
super(
URI.file(environmentService.builtinExtensionsPath),
URI.file(environmentService.extensionsPath),
environmentService.userHome,
URI.file(environmentService.userDataPath),
fileService, logService, environmentService, productService);
}
}

View File

@@ -5,18 +5,18 @@
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { ExtUri } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { DidUninstallExtensionEvent, IExtensionManagementService, ILocalExtension, InstallExtensionEvent, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { FileChangeType, FileSystemProviderCapabilities, IFileChange, IFileService } from 'vs/platform/files/common/files';
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { FileChangeType, IFileChange, IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
export class ExtensionsWatcher extends Disposable {
private readonly _onDidChangeExtensionsByAnotherSource = this._register(new Emitter<{ added: ILocalExtension[], removed: IExtensionIdentifier[] }>());
private readonly _onDidChangeExtensionsByAnotherSource = this._register(new Emitter<{ added: ILocalExtension[]; removed: IExtensionIdentifier[] }>());
readonly onDidChangeExtensionsByAnotherSource = this._onDidChangeExtensionsByAnotherSource.event;
private startTimestamp = 0;
@@ -28,9 +28,10 @@ export class ExtensionsWatcher extends Disposable {
@IFileService fileService: IFileService,
@INativeEnvironmentService environmentService: INativeEnvironmentService,
@ILogService private readonly logService: ILogService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
) {
super();
this.extensionsManagementService.getInstalled(ExtensionType.User).then(extensions => {
this.extensionsManagementService.getInstalled().then(extensions => {
this.installedExtensions = extensions.map(e => e.identifier);
this.startTimestamp = Date.now();
});
@@ -39,29 +40,28 @@ export class ExtensionsWatcher extends Disposable {
this._register(extensionsManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e)));
const extensionsResource = URI.file(environmentService.extensionsPath);
const extUri = new ExtUri(resource => !fileService.hasCapability(resource, FileSystemProviderCapabilities.PathCaseSensitive));
this._register(fileService.watch(extensionsResource));
this._register(Event.filter(fileService.onDidChangeFilesRaw, e => e.changes.some(change => this.doesChangeAffects(change, extensionsResource, extUri)))(() => this.onDidChange()));
this._register(Event.filter(fileService.onDidFilesChange, e => e.rawChanges.some(change => this.doesChangeAffects(change, extensionsResource)))(() => this.onDidChange()));
}
private doesChangeAffects(change: IFileChange, extensionsResource: URI, extUri: ExtUri): boolean {
// Is not immediate child of extensions resource
if (!extUri.isEqual(extUri.dirname(change.resource), extensionsResource)) {
return false;
}
// .obsolete file changed
if (extUri.isEqual(change.resource, extUri.joinPath(extensionsResource, '.obsolete'))) {
return true;
}
private doesChangeAffects(change: IFileChange, extensionsResource: URI): boolean {
// Only interested in added/deleted changes
if (change.type !== FileChangeType.ADDED && change.type !== FileChangeType.DELETED) {
return false;
}
// Ingore changes to files starting with `.`
if (extUri.basename(change.resource).startsWith('.')) {
// Is not immediate child of extensions resource
if (!this.uriIdentityService.extUri.isEqual(this.uriIdentityService.extUri.dirname(change.resource), extensionsResource)) {
return false;
}
// .obsolete file changed
if (this.uriIdentityService.extUri.isEqual(change.resource, this.uriIdentityService.extUri.joinPath(extensionsResource, '.obsolete'))) {
return true;
}
// Ignore changes to files starting with `.`
if (this.uriIdentityService.extUri.basename(change.resource).startsWith('.')) {
return false;
}
@@ -111,7 +111,7 @@ export class ExtensionsWatcher extends Disposable {
private async onDidChange(): Promise<void> {
if (this.installedExtensions) {
const extensions = await this.extensionsManagementService.getInstalled(ExtensionType.User);
const extensions = await this.extensionsManagementService.getInstalled();
const added = extensions.filter(e => {
if ([...this.installingExtensions, ...this.installedExtensions!].some(identifier => areSameExtensions(identifier, e.identifier))) {
return false;

View File

@@ -12,16 +12,17 @@ import { mock } from 'vs/base/test/common/mock';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IRawGalleryExtensionVersion, resolveMarketplaceHeaders, sortExtensionVersions } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { TargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IRawGalleryExtensionVersion, sortExtensionVersions } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { IFileService } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
import { NullLogService } from 'vs/platform/log/common/log';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
import { resolveMarketplaceHeaders } from 'vs/platform/externalServices/common/marketplace';
import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage';
import { TelemetryConfiguration, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry';
import { TargetPlatform } from 'vs/platform/extensions/common/extensions';
class EnvironmentServiceMock extends mock<IEnvironmentService>() {
override readonly serviceMachineIdResource: URI;

View File

@@ -4,6 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionKey } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { TargetPlatform } from 'vs/platform/extensions/common/extensions';
suite('Extension Identifier Pattern', () => {
@@ -26,4 +28,17 @@ suite('Extension Identifier Pattern', () => {
assert.strictEqual(false, regEx.test('publ_isher.name'));
assert.strictEqual(false, regEx.test('publisher._name'));
});
test('extension key', () => {
assert.strictEqual(new ExtensionKey({ id: 'pub.extension-name' }, '1.0.1').toString(), 'pub.extension-name-1.0.1');
assert.strictEqual(new ExtensionKey({ id: 'pub.extension-name' }, '1.0.1', TargetPlatform.UNDEFINED).toString(), 'pub.extension-name-1.0.1');
assert.strictEqual(new ExtensionKey({ id: 'pub.extension-name' }, '1.0.1', TargetPlatform.WIN32_IA32).toString(), `pub.extension-name-1.0.1-${TargetPlatform.WIN32_IA32}`);
});
test('extension key parsing', () => {
assert.strictEqual(ExtensionKey.parse('pub.extension-name'), null);
assert.strictEqual(ExtensionKey.parse('pub.extension-name@1.2.3'), null);
assert.strictEqual(ExtensionKey.parse('pub.extension-name-1.0.1')?.toString(), 'pub.extension-name-1.0.1');
assert.strictEqual(ExtensionKey.parse('pub.extension-name-1.0.1-win32-ia32')?.toString(), 'pub.extension-name-1.0.1-win32-ia32');
});
});

View File

@@ -0,0 +1,331 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { VSBuffer } from 'vs/base/common/buffer';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { dirname, joinPath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { AbstractExtensionsScannerService, IExtensionsScannerService, IScannedExtensionManifest, Translations } from 'vs/platform/extensionManagement/common/extensionsScannerService';
import { ExtensionType, IExtensionManifest, MANIFEST_CACHE_FOLDER, TargetPlatform } from 'vs/platform/extensions/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
let translations: Translations = Object.create(null);
const ROOT = URI.file('/ROOT');
class ExtensionsScannerService extends AbstractExtensionsScannerService implements IExtensionsScannerService {
constructor(
@IFileService fileService: IFileService,
@ILogService logService: ILogService,
@INativeEnvironmentService nativeEnvironmentService: INativeEnvironmentService,
@IProductService productService: IProductService,
) {
super(
URI.file(nativeEnvironmentService.builtinExtensionsPath),
URI.file(nativeEnvironmentService.extensionsPath),
joinPath(nativeEnvironmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json'),
joinPath(ROOT, MANIFEST_CACHE_FOLDER),
fileService, logService, nativeEnvironmentService, productService);
}
protected async getTranslations(language: string): Promise<Translations> {
return translations;
}
}
suite('NativeExtensionsScanerService Test', () => {
const disposables = new DisposableStore();
let instantiationService: TestInstantiationService;
setup(async () => {
translations = {};
instantiationService = new TestInstantiationService();
const logService = new NullLogService();
const fileService = disposables.add(new FileService(logService));
const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider());
fileService.registerProvider(ROOT.scheme, fileSystemProvider);
instantiationService.stub(ILogService, logService);
instantiationService.stub(IFileService, fileService);
const systemExtensionsLocation = joinPath(ROOT, 'system');
const userExtensionsLocation = joinPath(ROOT, 'extensions');
instantiationService.stub(INativeEnvironmentService, {
userHome: ROOT,
builtinExtensionsPath: systemExtensionsLocation.fsPath,
extensionsPath: userExtensionsLocation.fsPath,
});
instantiationService.stub(IProductService, { version: '1.66.0' });
await fileService.createFolder(systemExtensionsLocation);
await fileService.createFolder(userExtensionsLocation);
});
teardown(() => disposables.clear());
test('scan system extension', async () => {
const manifest: Partial<IExtensionManifest> = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' });
const extensionLocation = await aSystemExtension(manifest);
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
const actual = await testObject.scanSystemExtensions({});
assert.deepStrictEqual(actual.length, 1);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
assert.deepStrictEqual(actual[0].location.toString(), extensionLocation.toString());
assert.deepStrictEqual(actual[0].isBuiltin, true);
assert.deepStrictEqual(actual[0].type, ExtensionType.System);
assert.deepStrictEqual(actual[0].isValid, true);
assert.deepStrictEqual(actual[0].validations, []);
assert.deepStrictEqual(actual[0].metadata, undefined);
assert.deepStrictEqual(actual[0].targetPlatform, TargetPlatform.UNDEFINED);
assert.deepStrictEqual(actual[0].manifest, manifest);
});
test('scan user extension', async () => {
const manifest: Partial<IScannedExtensionManifest> = anExtensionManifest({ 'name': 'name', 'publisher': 'pub', __metadata: { id: 'uuid' } });
const extensionLocation = await aUserExtension(manifest);
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
const actual = await testObject.scanUserExtensions({});
assert.deepStrictEqual(actual.length, 1);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name', uuid: 'uuid' });
assert.deepStrictEqual(actual[0].location.toString(), extensionLocation.toString());
assert.deepStrictEqual(actual[0].isBuiltin, false);
assert.deepStrictEqual(actual[0].type, ExtensionType.User);
assert.deepStrictEqual(actual[0].isValid, true);
assert.deepStrictEqual(actual[0].validations, []);
assert.deepStrictEqual(actual[0].metadata, { id: 'uuid' });
assert.deepStrictEqual(actual[0].targetPlatform, TargetPlatform.UNDEFINED);
delete manifest.__metadata;
assert.deepStrictEqual(actual[0].manifest, manifest);
});
test('scan existing extension', async () => {
const manifest: Partial<IExtensionManifest> = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' });
const extensionLocation = await aUserExtension(manifest);
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
const actual = await testObject.scanExistingExtension(extensionLocation, ExtensionType.User, {});
assert.notEqual(actual, null);
assert.deepStrictEqual(actual!.identifier, { id: 'pub.name' });
assert.deepStrictEqual(actual!.location.toString(), extensionLocation.toString());
assert.deepStrictEqual(actual!.isBuiltin, false);
assert.deepStrictEqual(actual!.type, ExtensionType.User);
assert.deepStrictEqual(actual!.isValid, true);
assert.deepStrictEqual(actual!.validations, []);
assert.deepStrictEqual(actual!.metadata, undefined);
assert.deepStrictEqual(actual!.targetPlatform, TargetPlatform.UNDEFINED);
assert.deepStrictEqual(actual!.manifest, manifest);
});
test('scan single extension', async () => {
const manifest: Partial<IExtensionManifest> = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' });
const extensionLocation = await aUserExtension(manifest);
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
const actual = await testObject.scanOneOrMultipleExtensions(extensionLocation, ExtensionType.User, {});
assert.deepStrictEqual(actual.length, 1);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
assert.deepStrictEqual(actual[0].location.toString(), extensionLocation.toString());
assert.deepStrictEqual(actual[0].isBuiltin, false);
assert.deepStrictEqual(actual[0].type, ExtensionType.User);
assert.deepStrictEqual(actual[0].isValid, true);
assert.deepStrictEqual(actual[0].validations, []);
assert.deepStrictEqual(actual[0].metadata, undefined);
assert.deepStrictEqual(actual[0].targetPlatform, TargetPlatform.UNDEFINED);
assert.deepStrictEqual(actual[0].manifest, manifest);
});
test('scan multiple extensions', async () => {
const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }));
await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' }));
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
const actual = await testObject.scanOneOrMultipleExtensions(dirname(extensionLocation), ExtensionType.User, {});
assert.deepStrictEqual(actual.length, 2);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' });
});
test('scan user extension with different versions', async () => {
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' }));
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' }));
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
const actual = await testObject.scanUserExtensions({});
assert.deepStrictEqual(actual.length, 1);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
assert.deepStrictEqual(actual[0].manifest.version, '1.0.2');
});
test('scan user extension include all versions', async () => {
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' }));
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' }));
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
const actual = await testObject.scanUserExtensions({ includeAllVersions: true });
assert.deepStrictEqual(actual.length, 2);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
assert.deepStrictEqual(actual[0].manifest.version, '1.0.1');
assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name' });
assert.deepStrictEqual(actual[1].manifest.version, '1.0.2');
});
test.skip('scan user extension with different versions and higher version is not compatible', async () => {
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' }));
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2', engines: { vscode: '^1.67.0' } }));
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
const actual = await testObject.scanUserExtensions({});
assert.deepStrictEqual(actual.length, 1);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
assert.deepStrictEqual(actual[0].manifest.version, '1.0.1');
});
test.skip('scan exclude invalid extensions', async () => {
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }));
await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } }));
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
const actual = await testObject.scanUserExtensions({});
assert.deepStrictEqual(actual.length, 1);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
});
test('scan exclude uninstalled extensions', async () => {
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }));
await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' }));
await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true })));
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
const actual = await testObject.scanUserExtensions({});
assert.deepStrictEqual(actual.length, 1);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
});
test('scan include uninstalled extensions', async () => {
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }));
await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' }));
await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true })));
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
const actual = await testObject.scanUserExtensions({ includeUninstalled: true });
assert.deepStrictEqual(actual.length, 2);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' });
});
test('scan include invalid extensions', async () => {
await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }));
await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } }));
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
const actual = await testObject.scanUserExtensions({ includeInvalid: true });
assert.deepStrictEqual(actual.length, 2);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' });
});
test('scan system extensions include additional builtin extensions', async () => {
instantiationService.stub(IProductService, {
version: '1.66.0',
builtInExtensions: [
{ name: 'pub.name2', version: '', repo: '', metadata: undefined },
{ name: 'pub.name', version: '', repo: '', metadata: undefined }
]
});
await anExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' }), joinPath(ROOT, 'additional'));
const extensionLocation = await anExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }), joinPath(ROOT, 'additional'));
await aSystemExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' }));
await instantiationService.get(IFileService).writeFile(joinPath(instantiationService.get(INativeEnvironmentService).userHome, '.vscode-oss-dev', 'extensions', 'control.json'), VSBuffer.fromString(JSON.stringify({ 'pub.name2': 'disabled', 'pub.name': extensionLocation.fsPath })));
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
const actual = await testObject.scanSystemExtensions({ checkControlFile: true });
assert.deepStrictEqual(actual.length, 1);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
assert.deepStrictEqual(actual[0].manifest.version, '1.0.0');
});
test('scan extension with default nls replacements', async () => {
const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' }));
await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' })));
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
const actual = await testObject.scanUserExtensions({});
assert.deepStrictEqual(actual.length, 1);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World');
});
test('scan extension with en nls replacements', async () => {
const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' }));
await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' })));
const nlsLocation = joinPath(extensionLocation, 'package.en.json');
await instantiationService.get(IFileService).writeFile(nlsLocation, VSBuffer.fromString(JSON.stringify({ contents: { package: { displayName: 'Hello World EN' } } })));
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
translations = { 'pub.name': nlsLocation.fsPath };
const actual = await testObject.scanUserExtensions({ language: 'en' });
assert.deepStrictEqual(actual.length, 1);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World EN');
});
test('scan extension falls back to default nls replacements', async () => {
const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' }));
await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' })));
const nlsLocation = joinPath(extensionLocation, 'package.en.json');
await instantiationService.get(IFileService).writeFile(nlsLocation, VSBuffer.fromString(JSON.stringify({ contents: { package: { displayName: 'Hello World EN' } } })));
const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService);
translations = { 'pub.name2': nlsLocation.fsPath };
const actual = await testObject.scanUserExtensions({ language: 'en' });
assert.deepStrictEqual(actual.length, 1);
assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' });
assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World');
});
async function aUserExtension(manifest: Partial<IScannedExtensionManifest>): Promise<URI> {
const environmentService = instantiationService.get(INativeEnvironmentService);
return anExtension(manifest, URI.file(environmentService.extensionsPath));
}
async function aSystemExtension(manifest: Partial<IScannedExtensionManifest>): Promise<URI> {
const environmentService = instantiationService.get(INativeEnvironmentService);
return anExtension(manifest, URI.file(environmentService.builtinExtensionsPath));
}
async function anExtension(manifest: Partial<IScannedExtensionManifest>, root: URI): Promise<URI> {
const fileService = instantiationService.get(IFileService);
const extensionLocation = joinPath(root, `${manifest.publisher}.${manifest.name}-${manifest.version}-${manifest.__metadata?.targetPlatform ?? TargetPlatform.UNDEFINED}`);
await fileService.writeFile(joinPath(extensionLocation, 'package.json'), VSBuffer.fromString(JSON.stringify(manifest)));
return extensionLocation;
}
function anExtensionManifest(manifest: Partial<IScannedExtensionManifest>): Partial<IExtensionManifest> {
return { engines: { vscode: '^1.66.0' }, version: '1.0.0', main: 'main.js', activationEvents: ['*'], ...manifest };
}
});