Merge pull request #162285 from dtivel/dtivel/verify-packages

Add extension signature verification service
This commit is contained in:
Sandeep Somavarapu
2022-10-18 14:14:05 +02:00
committed by GitHub
22 changed files with 464 additions and 81 deletions
@@ -32,6 +32,7 @@ import { SharedProcessEnvironmentService } from 'vs/platform/sharedProcess/node/
import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService';
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { IExtensionGalleryService, IExtensionManagementService, IExtensionTipsService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService';
import { ExtensionManagementChannel, ExtensionTipsChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService';
import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
@@ -331,6 +332,7 @@ class SharedProcessMain extends Disposable {
// Extension Management
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true));
services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService, undefined, true));
services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService, undefined, true));
// Extension Gallery
+2
View File
@@ -24,6 +24,7 @@ import { INativeEnvironmentService } from 'vs/platform/environment/common/enviro
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { IExtensionGalleryService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService';
import { ExtensionManagementCLI } from 'vs/platform/extensionManagement/common/extensionManagementCLI';
import { ExtensionsProfileScannerService, IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService';
@@ -181,6 +182,7 @@ class CliMain extends Disposable {
// Extensions
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true));
services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService, undefined, true));
services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService, undefined, true));
services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService, undefined, true));
@@ -142,7 +142,7 @@ export interface INativeEnvironmentService extends IEnvironmentService {
// --- extensions
extensionsPath: string;
extensionsDownloadPath: string;
extensionsDownloadLocation: URI;
builtinExtensionsPath: string;
// --- use keytar for credentials
@@ -129,13 +129,13 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron
return normalize(join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions'));
}
get extensionsDownloadPath(): string {
get extensionsDownloadLocation(): URI {
const cliExtensionsDownloadDir = this.args['extensions-download-dir'];
if (cliExtensionsDownloadDir) {
return resolve(cliExtensionsDownloadDir);
return URI.file(resolve(cliExtensionsDownloadDir));
}
return join(this.userDataPath, 'CachedExtensionVSIXs');
return URI.file(join(this.userDataPath, 'CachedExtensionVSIXs'));
}
@memoize
@@ -28,6 +28,7 @@ export interface IInstallExtensionTask {
readonly identifier: IExtensionIdentifier;
readonly source: IGalleryExtension | URI;
readonly operation: InstallOperation;
readonly wasVerified?: boolean;
run(): Promise<{ local: ILocalExtension; metadata: Metadata }>;
waitUntilTaskIsFinished(): Promise<{ local: ILocalExtension; metadata: Metadata }>;
cancel(): void;
@@ -243,6 +244,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000;
reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', {
extensionData: getGalleryExtensionTelemetryData(task.source),
wasVerified: task.wasVerified,
duration: new Date().getTime() - startTime,
durationSinceUpdate
});
@@ -256,7 +258,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source, context: options.context, profileLocation: options.profileLocation, applicationScoped: local.isApplicationScoped });
} catch (error) {
if (!URI.isUri(task.source)) {
reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), duration: new Date().getTime() - startTime, error });
reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', {
extensionData: getGalleryExtensionTelemetryData(task.source),
wasVerified: task.wasVerified,
duration: new Date().getTime() - startTime,
error
});
}
this.logService.error('Error while installing the extension:', task.identifier.id);
throw error;
@@ -665,6 +672,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
abstract getManifest(vsix: URI): Promise<IExtensionManifest>;
abstract install(vsix: URI, options?: InstallVSIXOptions): Promise<ILocalExtension>;
abstract getInstalled(type?: ExtensionType, profileLocation?: URI): Promise<ILocalExtension[]>;
abstract download(extension: IGalleryExtension, operation: InstallOperation): Promise<URI>;
abstract getMetadata(extension: ILocalExtension): Promise<Metadata | undefined>;
abstract updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension>;
@@ -693,8 +701,22 @@ function toExtensionManagementError(error: Error): ExtensionManagementError {
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;
export function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, wasVerified, duration, error, durationSinceUpdate }: { extensionData: any; wasVerified?: boolean; duration?: number; durationSinceUpdate?: number; error?: Error }): void {
let errorcode: ExtensionManagementErrorCode | undefined;
let errorcodeDetail: string | undefined;
if (error) {
if (error instanceof ExtensionManagementError) {
errorcode = error.code;
if (error.code === ExtensionManagementErrorCode.Signature) {
errorcodeDetail = error.message;
}
} else {
errorcode = ExtensionManagementErrorCode.Internal;
}
}
/* __GDPR__
"extensionGallery:install" : {
"owner": "sandy081",
@@ -702,7 +724,9 @@ export function reportTelemetry(telemetryService: ITelemetryService, eventName:
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"durationSinceUpdate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
"errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
"recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"wasVerified" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"${include}": [
"${GalleryExtensionTelemetryData}"
]
@@ -725,12 +749,14 @@ export function reportTelemetry(telemetryService: ITelemetryService, eventName:
"success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
"errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" },
"wasVerified" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"${include}": [
"${GalleryExtensionTelemetryData}"
]
}
*/
telemetryService.publicLog(eventName, { ...extensionData, success: !error, duration, errorcode, durationSinceUpdate });
telemetryService.publicLog(eventName, { ...extensionData, wasVerified, success: !error, duration, errorcode, errorcodeDetail, durationSinceUpdate });
}
export abstract class AbstractExtensionTask<T> {
@@ -193,7 +193,8 @@ const AssetType = {
Manifest: 'Microsoft.VisualStudio.Code.Manifest',
VSIX: 'Microsoft.VisualStudio.Services.VSIXPackage',
License: 'Microsoft.VisualStudio.Services.Content.License',
Repository: 'Microsoft.VisualStudio.Services.Links.Source'
Repository: 'Microsoft.VisualStudio.Services.Links.Source',
Signature: 'Microsoft.VisualStudio.Services.VsixSignature'
};
const PropertyType = {
@@ -505,6 +506,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
repository: getRepositoryAsset(version),
download: getDownloadAsset(version),
icon: getVersionAsset(version, AssetType.Icon),
signature: getVersionAsset(version, AssetType.Signature),
coreTranslations: getCoreTranslationAssets(version)
};
@@ -542,6 +544,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
hasPreReleaseVersion: isPreReleaseVersion(latestVersion),
hasReleaseVersion: true,
preview: getIsPreview(galleryExtension.flags),
isSigned: !!assets.signature
};
}
@@ -1027,6 +1030,17 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
log(new Date().getTime() - startTime);
}
async downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise<void> {
if (!extension.assets.signature) {
throw new Error('No signature asset found');
}
this.logService.trace('ExtensionGalleryService#downloadSignatureArchive', extension.identifier.id);
const context = await this.getAsset(extension.assets.signature);
await this.fileService.writeFile(location, context.stream);
}
async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise<string> {
if (extension.assets.readme) {
const context = await this.getAsset(extension.assets.readme, {}, token);
@@ -178,6 +178,7 @@ export interface IGalleryExtensionAssets {
repository: IGalleryExtensionAsset | null;
download: IGalleryExtensionAsset;
icon: IGalleryExtensionAsset | null;
signature: IGalleryExtensionAsset | null;
coreTranslations: [string, IGalleryExtensionAsset][];
}
@@ -224,6 +225,7 @@ export interface IGalleryExtension {
preview: boolean;
hasPreReleaseVersion: boolean;
hasReleaseVersion: boolean;
isSigned: boolean;
allTargetPlatforms: TargetPlatform[];
assets: IGalleryExtensionAssets;
properties: IGalleryExtensionProperties;
@@ -335,6 +337,7 @@ export interface IExtensionGalleryService {
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>;
downloadSignatureArchive(extension: IGalleryExtension, location: URI): 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>;
@@ -390,6 +393,7 @@ export enum ExtensionManagementErrorCode {
CorruptZip = 'CorruptZip',
IncompleteZip = 'IncompleteZip',
Internal = 'Internal',
Signature = 'Signature'
}
export class ExtensionManagementError extends Error {
@@ -440,6 +444,7 @@ export interface IExtensionManagementService {
getInstalled(type?: ExtensionType, profileLocation?: URI): Promise<ILocalExtension[]>;
getExtensionsControlManifest(): Promise<IExtensionsControlManifest>;
download(extension: IGalleryExtension, operation: InstallOperation): Promise<URI>;
getMetadata(extension: ILocalExtension): Promise<Metadata | undefined>;
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension>;
updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise<ILocalExtension>;
@@ -10,7 +10,7 @@ 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 { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI {
@@ -75,6 +75,7 @@ export class ExtensionManagementChannel implements IServerChannel {
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 'getExtensionsControlManifest': return this.service.getExtensionsControlManifest();
case 'download': return this.service.download(args[0], args[1]);
}
throw new Error('Invalid call');
@@ -177,6 +178,11 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt
return Promise.resolve(this.channel.call<IExtensionsControlManifest>('getExtensionsControlManifest'));
}
async download(extension: IGalleryExtension, operation: InstallOperation): Promise<URI> {
const result = await this.channel.call<UriComponents>('download', [extension, operation]);
return URI.revive(result);
}
registerParticipant() { throw new Error('Not Supported'); }
}
@@ -125,6 +125,7 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension): any
"publisherDisplayName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"isPreReleaseVersion": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"dependencies": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"isSigned": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"${include}": [
"${GalleryExtensionTelemetryData2}"
]
@@ -140,6 +141,7 @@ export function getGalleryExtensionTelemetryData(extension: IGalleryExtension):
publisherDisplayName: extension.publisherDisplayName,
isPreReleaseVersion: extension.properties.isPreReleaseVersion,
dependencies: !!(extension.properties.dependencies && extension.properties.dependencies.length > 0),
isSigned: extension.isSigned,
...extension.telemetryData
};
}
@@ -6,20 +6,25 @@
import { Promises } from 'vs/base/common/async';
import { getErrorMessage } from 'vs/base/common/errors';
import { Disposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { isWindows } from 'vs/base/common/platform';
import { joinPath } 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 { Promises as FSPromises } from 'vs/base/node/pfs';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionSignatureVerificationError, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService';
import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
export class ExtensionsDownloader extends Disposable {
private static readonly SignatureArchiveExtension = '.sigzip';
readonly extensionsDownloadDir: URI;
private readonly cache: number;
private readonly cleanUpPromise: Promise<void>;
@@ -28,47 +33,83 @@ export class ExtensionsDownloader extends Disposable {
@INativeEnvironmentService environmentService: INativeEnvironmentService,
@IFileService private readonly fileService: IFileService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IExtensionSignatureVerificationService private readonly extensionSignatureVerificationService: IExtensionSignatureVerificationService,
@ILogService private readonly logService: ILogService,
) {
super();
this.extensionsDownloadDir = URI.file(environmentService.extensionsDownloadPath);
this.cache = 20; // Cache 20 downloads
this.extensionsDownloadDir = environmentService.extensionsDownloadLocation;
this.cache = 20; // Cache 20 downloaded VSIX files
this.cleanUpPromise = this.cleanUp();
}
async downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise<URI> {
async download(extension: IGalleryExtension, operation: InstallOperation): Promise<{ readonly location: URI; verified: boolean }> {
await this.cleanUpPromise;
const vsixName = this.getName(extension);
const location = joinPath(this.extensionsDownloadDir, vsixName);
// Download only if vsix does not exist
if (!await this.fileService.exists(location)) {
// Download to temporary location first only if vsix does not exist
const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`);
if (!await this.fileService.exists(tempLocation)) {
await this.extensionGalleryService.download(extension, tempLocation, operation);
}
try {
// Rename temp location to original
await this.rename(tempLocation, location, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
} catch (error) {
try {
await this.fileService.del(tempLocation);
} catch (e) { /* ignore */ }
if (error.code === 'ENOTEMPTY') {
this.logService.info(`Rename failed because vsix was downloaded by another source. So ignoring renaming.`, extension.identifier.id);
} else {
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the vsix from downloaded location`, tempLocation.path);
throw error;
}
}
const location = joinPath(this.extensionsDownloadDir, this.getName(extension));
try {
await this.downloadFile(extension, location, location => this.extensionGalleryService.download(extension, location, operation));
} catch (error) {
throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Download);
}
let verified: boolean = false;
if (extension.isSigned && this.configurationService.getValue('extensions.verifySignature') === true) {
const signatureArchiveLocation = await this.downloadSignatureArchive(extension);
try {
verified = await this.extensionSignatureVerificationService.verify(location.fsPath, signatureArchiveLocation.fsPath);
} catch (error) {
await this.delete(signatureArchiveLocation);
await this.delete(location);
throw new ExtensionManagementError((error as ExtensionSignatureVerificationError).code, ExtensionManagementErrorCode.Signature);
}
}
return { location, verified };
}
private async downloadSignatureArchive(extension: IGalleryExtension): Promise<URI> {
await this.cleanUpPromise;
const location = joinPath(this.extensionsDownloadDir, `${this.getName(extension)}${ExtensionsDownloader.SignatureArchiveExtension}`);
await this.downloadFile(extension, location, location => this.extensionGalleryService.downloadSignatureArchive(extension, location));
return location;
}
private async downloadFile(extension: IGalleryExtension, location: URI, downloadFn: (location: URI) => Promise<void>): Promise<void> {
// Do not download if exists
if (await this.fileService.exists(location)) {
return;
}
// Download directly if locaiton is not file scheme
if (location.scheme !== Schemas.file) {
await downloadFn(location);
return;
}
// Download to temporary location first only if file does not exist
const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`);
if (!await this.fileService.exists(tempLocation)) {
await downloadFn(tempLocation);
}
try {
// Rename temp location to original
await this.rename(tempLocation, location, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
} catch (error) {
try {
await this.fileService.del(tempLocation);
} catch (e) { /* ignore */ }
if (error.code === 'ENOTEMPTY') {
this.logService.info(`Rename failed because the file was downloaded by another source. So ignoring renaming.`, extension.identifier.id, location.path);
} else {
this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the file from downloaded location`, tempLocation.path);
throw error;
}
}
}
async delete(location: URI): Promise<void> {
await this.cleanUpPromise;
await this.fileService.del(location);
@@ -89,20 +130,27 @@ export class ExtensionsDownloader extends Disposable {
private async cleanUp(): Promise<void> {
try {
if (!(await this.fileService.exists(this.extensionsDownloadDir))) {
this.logService.trace('Extension VSIX downlads cache dir does not exist');
this.logService.trace('Extension VSIX downloads cache dir does not exist');
return;
}
const folderStat = await this.fileService.resolve(this.extensionsDownloadDir, { resolveMetadata: true });
if (folderStat.children) {
const toDelete: URI[] = [];
const all: [ExtensionKey, IFileStatWithMetadata][] = [];
const vsixs: [ExtensionKey, IFileStatWithMetadata][] = [];
const signatureArchives: URI[] = [];
for (const stat of folderStat.children) {
const extension = ExtensionKey.parse(stat.name);
if (extension) {
all.push([extension, stat]);
if (stat.name.endsWith(ExtensionsDownloader.SignatureArchiveExtension)) {
signatureArchives.push(stat.resource);
} else {
const extension = ExtensionKey.parse(stat.name);
if (extension) {
vsixs.push([extension, stat]);
}
}
}
const byExtension = groupByExtension(all, ([extension]) => extension);
const byExtension = groupByExtension(vsixs, ([extension]) => extension);
const distinct: IFileStatWithMetadata[] = [];
for (const p of byExtension) {
p.sort((a, b) => semver.rcompare(a[0].version, b[0].version));
@@ -111,8 +159,10 @@ export class ExtensionsDownloader extends Disposable {
}
distinct.sort((a, b) => a.mtime - b.mtime); // sort by modified time
toDelete.push(...distinct.slice(0, Math.max(0, distinct.length - this.cache)).map(s => s.resource)); // Retain minimum cacheSize and delete the rest
toDelete.push(...signatureArchives); // Delete all signature archives
await Promises.settled(toDelete.map(resource => {
this.logService.trace('Deleting vsix from cache', resource.path);
this.logService.trace('Deleting from cache', resource.path);
return this.fileService.del(resource);
}));
}
@@ -78,7 +78,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
@IFileService private readonly fileService: IFileService,
@IProductService productService: IProductService,
@IUriIdentityService uriIdentityService: IUriIdentityService,
@IUserDataProfilesService userDataProfilesService: IUserDataProfilesService,
@IUserDataProfilesService userDataProfilesService: IUserDataProfilesService
) {
super(galleryService, telemetryService, logService, productService, userDataProfilesService);
const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle));
@@ -177,6 +177,11 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
return this.extensionsScanner.cleanUp(removeOutdated);
}
async download(extension: IGalleryExtension, operation: InstallOperation): Promise<URI> {
const { location } = await this.extensionsDownloader.download(extension, operation);
return location;
}
private async downloadVsix(vsix: URI): Promise<{ location: URI; cleanup: () => Promise<void> }> {
if (vsix.scheme === Schemas.file) {
return { location: vsix, async cleanup() { } };
@@ -247,7 +252,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
}
class ExtensionsScanner extends Disposable {
export class ExtensionsScanner extends Disposable {
private readonly uninstalledPath: string;
private readonly uninstalledFileLimiter: Queue<any>;
@@ -463,6 +468,7 @@ class ExtensionsScanner extends Disposable {
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
@@ -528,6 +534,8 @@ class ExtensionsScanner extends Disposable {
abstract class InstallExtensionTask extends AbstractExtensionTask<{ local: ILocalExtension; metadata: Metadata }> implements IInstallExtensionTask {
public wasVerified: boolean = false;
protected _operation = InstallOperation.Install;
get operation() { return isUndefined(this.options.operation) ? this._operation : this.options.operation; }
@@ -584,7 +592,7 @@ abstract class InstallExtensionTask extends AbstractExtensionTask<{ local: ILoca
}
class InstallGalleryExtensionTask extends InstallExtensionTask {
export class InstallGalleryExtensionTask extends InstallExtensionTask {
constructor(
private readonly manifest: IExtensionManifest,
@@ -628,46 +636,34 @@ class InstallGalleryExtensionTask extends InstallExtensionTask {
return { local, metadata };
}
const zipPath = await this.downloadExtension(this.gallery, this._operation);
const { location, verified } = await this.extensionsDownloader.download(this.gallery, this._operation);
try {
const local = await this.installExtension({ zipPath, key: ExtensionKey.create(this.gallery), metadata }, token);
this.wasVerified = !!verified;
this.validateManifest(location.fsPath);
const local = await this.installExtension({ zipPath: location.fsPath, key: ExtensionKey.create(this.gallery), metadata }, token);
if (existingExtension && !this.options.profileLocation && (existingExtension.targetPlatform !== local.targetPlatform || semver.neq(existingExtension.manifest.version, local.manifest.version))) {
await this.extensionsScanner.setUninstalled(existingExtension);
}
return { local, metadata };
} catch (error) {
await this.deleteDownloadedVSIX(zipPath);
try {
await this.extensionsDownloader.delete(location);
} catch (error) {
/* Ignore */
this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(error));
}
throw error;
}
}
private async deleteDownloadedVSIX(vsix: string): Promise<void> {
try {
await this.extensionsDownloader.delete(URI.file(vsix));
} catch (error) {
/* Ignore */
this.logService.warn('Error while deleting the downloaded vsix', vsix.toString(), getErrorMessage(error));
}
}
private async downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise<string> {
let zipPath: string | undefined;
try {
this.logService.trace('Started downloading extension:', extension.identifier.id);
zipPath = (await this.extensionsDownloader.downloadExtension(extension, operation)).fsPath;
this.logService.info('Downloaded extension:', extension.identifier.id, zipPath);
} catch (error) {
throw new ExtensionManagementError(joinErrors(error).message, ExtensionManagementErrorCode.Download);
}
protected async validateManifest(zipPath: string): Promise<void> {
try {
await getManifest(zipPath);
return zipPath;
} catch (error) {
await this.deleteDownloadedVSIX(zipPath);
throw new ExtensionManagementError(joinErrors(error).message, ExtensionManagementErrorCode.Invalid);
}
}
}
class InstallVSIXTask extends InstallExtensionTask {
@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IExtensionSignatureVerificationService = createDecorator<IExtensionSignatureVerificationService>('IExtensionSignatureVerificationService');
/**
* A service for verifying signed extensions.
*/
export interface IExtensionSignatureVerificationService {
readonly _serviceBrand: undefined;
/**
* Verifies an extension file (.vsix) against a signature archive file.
* @param { string } vsixFilePath The extension file path.
* @param { string } signatureArchiveFilePath The signature archive file path.
* @returns { Promise<boolean> } A promise with `true` if the extension is validly signed and trusted;
* otherwise, `false` because verification is not enabled (e.g.: in the OSS version of VS Code).
* @throws { ExtensionVerificationError } An error with a code indicating the validity, integrity, or trust issue
* found during verification or a more fundamental issue (e.g.: a required dependency was not found).
*/
verify(vsixFilePath: string, signatureArchiveFilePath: string): Promise<boolean>;
}
declare module vsceSign {
export function verify(vsixFilePath: string, signatureArchiveFilePath: string): Promise<boolean>;
}
/**
* An error raised during extension signature verification.
*/
export interface ExtensionSignatureVerificationError extends Error {
readonly code: string;
}
export class ExtensionSignatureVerificationService implements IExtensionSignatureVerificationService {
declare readonly _serviceBrand: undefined;
private moduleLoadingPromise: Promise<typeof vsceSign> | undefined;
private vsceSign(): Promise<typeof vsceSign> {
if (!this.moduleLoadingPromise) {
this.moduleLoadingPromise = new Promise(
(resolve, reject) => require(
['vsce-sign'],
async (obj) => {
const instance = <typeof vsceSign>obj;
return resolve(instance);
}, reject));
}
return this.moduleLoadingPromise;
}
public async verify(vsixFilePath: string, signatureArchiveFilePath: string): Promise<boolean> {
let module: typeof vsceSign;
try {
module = await this.vsceSign();
} catch (error) {
return false;
}
return module.verify(vsixFilePath, signatureArchiveFilePath);
}
}
@@ -0,0 +1,194 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. 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 { platform } from 'vs/base/common/platform';
import { arch } from 'vs/base/common/process';
import { joinPath } from 'vs/base/common/resources';
import { isBoolean } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
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 { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { ExtensionManagementError, ExtensionManagementErrorCode, getTargetPlatform, IExtensionGalleryService, IGalleryExtension, IGalleryExtensionAssets, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader';
import { ExtensionsScanner, InstallGalleryExtensionTask } from 'vs/platform/extensionManagement/node/extensionManagementService';
import { IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService';
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';
const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' });
class TestExtensionsScanner extends mock<ExtensionsScanner>() {
override async scanExtensions(): Promise<ILocalExtension[]> { return []; }
}
class TestExtensionSignatureVerificationService extends mock<IExtensionSignatureVerificationService>() {
constructor(private readonly verificationResult: string | boolean) {
super();
}
override async verify(): Promise<boolean> {
if (isBoolean(this.verificationResult)) {
return this.verificationResult;
}
const error = Error(this.verificationResult);
(error as any).code = this.verificationResult;
throw error;
}
}
class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask {
installed = false;
constructor(
extension: IGalleryExtension,
extensionDownloader: ExtensionsDownloader,
) {
super(
{
name: extension.name,
publisher: extension.publisher,
version: extension.version,
engines: { vscode: '*' },
},
extension,
{},
extensionDownloader,
new TestExtensionsScanner(),
new NullLogService(),
);
}
protected override async installExtension(): Promise<ILocalExtension> {
this.installed = true;
return new class extends mock<ILocalExtension>() { };
}
protected override async validateManifest(): Promise<void> { }
}
suite('InstallGalleryExtensionTask Tests', () => {
const disposables = new DisposableStore();
teardown(() => disposables.clear());
test('if verification is disabled by default, the task skips verification', async () => {
const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader('error'));
await testObject.run();
assert.strictEqual(testObject.wasVerified, false);
assert.strictEqual(testObject.installed, true);
});
test('if verification is disabled by setting set to false, the task skips verification', async () => {
const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader('error', false));
await testObject.run();
assert.strictEqual(testObject.wasVerified, false);
assert.strictEqual(testObject.installed, true);
});
test('if verification is disabled because the module is not loaded, the task skips verification', async () => {
const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader(false, true));
await testObject.run();
assert.strictEqual(testObject.wasVerified, false);
assert.strictEqual(testObject.installed, true);
});
test('if verification fails, the task throws', async () => {
const errorCode = 'IntegrityCheckFailed';
const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader(errorCode, true));
try {
await testObject.run();
} catch (e) {
assert.ok(e instanceof ExtensionManagementError);
assert.strictEqual(e.code, ExtensionManagementErrorCode.Signature);
assert.strictEqual(e.message, errorCode);
assert.strictEqual(testObject.wasVerified, false);
assert.strictEqual(testObject.installed, false);
return;
}
assert.fail('It should have thrown.');
});
test('if verification succeeds, the task completes', async () => {
const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader(true, true));
await testObject.run();
assert.strictEqual(testObject.wasVerified, true);
assert.strictEqual(testObject.installed, true);
});
test('task completes for unsigned extension', async () => {
const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: false }), anExtensionsDownloader(true, true));
await testObject.run();
assert.strictEqual(testObject.wasVerified, false);
assert.strictEqual(testObject.installed, true);
});
test('task completes for an unsigned extension even when signature verification throws error', async () => {
const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: false }), anExtensionsDownloader('error', true));
await testObject.run();
assert.strictEqual(testObject.wasVerified, false);
assert.strictEqual(testObject.installed, true);
});
function anExtensionsDownloader(verificationResult: string | boolean, isSignatureVerificationEnabled?: boolean): ExtensionsDownloader {
const logService = new NullLogService();
const fileService = disposables.add(new FileService(logService));
const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider());
fileService.registerProvider(ROOT.scheme, fileSystemProvider);
const instantiationService = new TestInstantiationService();
instantiationService.stub(IFileService, fileService);
instantiationService.stub(ILogService, logService);
instantiationService.stub(INativeEnvironmentService, <Partial<INativeEnvironmentService>>{ extensionsDownloadLocation: joinPath(ROOT, 'CachedExtensionVSIXs') });
instantiationService.stub(IExtensionGalleryService, <Partial<IExtensionGalleryService>>{
async download(extension, location, operation) {
await fileService.writeFile(location, VSBuffer.fromString('extension vsix'));
},
async downloadSignatureArchive(extension, location) {
await fileService.writeFile(location, VSBuffer.fromString('extension signature'));
},
});
instantiationService.stub(IConfigurationService, new TestConfigurationService(isBoolean(isSignatureVerificationEnabled) ? { extensions: { verifySignature: isSignatureVerificationEnabled } } : undefined));
instantiationService.stub(IExtensionSignatureVerificationService, new TestExtensionSignatureVerificationService(verificationResult));
return instantiationService.createInstance(ExtensionsDownloader);
}
function aGalleryExtension(name: string, properties: Partial<IGalleryExtension> = {}, galleryExtensionProperties: any = {}, assets: Partial<IGalleryExtensionAssets> = {}): IGalleryExtension {
const targetPlatform = getTargetPlatform(platform, arch);
const galleryExtension = <IGalleryExtension>Object.create({ name, publisher: 'pub', version: '1.0.0', allTargetPlatforms: [targetPlatform], properties: {}, assets: {}, ...properties });
galleryExtension.properties = { ...galleryExtension.properties, dependencies: [], targetPlatform, ...galleryExtensionProperties };
galleryExtension.assets = { ...galleryExtension.assets, ...assets };
galleryExtension.identifier = { id: getGalleryExtensionId(galleryExtension.publisher, galleryExtension.name), uuid: generateUuid() };
return <IGalleryExtension>galleryExtension;
}
});
@@ -15,6 +15,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IExtensionGalleryService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import product from 'vs/platform/product/common/product';
@@ -110,6 +111,7 @@ class CliMain extends Disposable {
services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService));
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService));
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService));
services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService));
services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService));
services.set(ILanguagePackService, new SyncDescriptor(NativeLanguagePackService));
+2
View File
@@ -25,6 +25,7 @@ import { EncryptionMainService } from 'vs/platform/encryption/node/encryptionMai
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService';
import { ExtensionManagementCLI } from 'vs/platform/extensionManagement/common/extensionManagementCLI';
import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
@@ -172,6 +173,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService));
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService));
services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService));
services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService));
const instantiationService: IInstantiationService = new InstantiationService(services);
@@ -85,6 +85,7 @@ const mockExtensionGallery: IGalleryExtension[] = [
icon: { uri: 'uri:icon', fallbackUri: 'fallback:icon' },
license: { uri: 'uri:license', fallbackUri: 'fallback:license' },
repository: { uri: 'uri:repository', fallbackUri: 'fallback:repository' },
signature: { uri: 'uri:signature', fallbackUri: 'fallback:signature' },
coreTranslations: []
}),
aGalleryExtension('MockExtension2', {
@@ -107,6 +108,7 @@ const mockExtensionGallery: IGalleryExtension[] = [
icon: { uri: 'uri:icon', fallbackUri: 'fallback:icon' },
license: { uri: 'uri:license', fallbackUri: 'fallback:license' },
repository: { uri: 'uri:repository', fallbackUri: 'fallback:repository' },
signature: { uri: 'uri:signature', fallbackUri: 'fallback:signature' },
coreTranslations: []
})
];
@@ -166,6 +168,7 @@ const noAssets: IGalleryExtensionAssets = {
manifest: null,
readme: null,
repository: null,
signature: null,
coreTranslations: []
};
@@ -164,6 +164,7 @@ suite('ExtensionsWorkbenchServiceTest', () => {
icon: { uri: 'uri:icon', fallbackUri: 'fallback:icon' },
license: { uri: 'uri:license', fallbackUri: 'fallback:license' },
repository: { uri: 'uri:repository', fallbackUri: 'fallback:repository' },
signature: { uri: 'uri:signature', fallbackUri: 'fallback:signature' },
coreTranslations: []
});
@@ -314,6 +315,7 @@ suite('ExtensionsWorkbenchServiceTest', () => {
icon: { uri: 'uri:icon', fallbackUri: 'fallback:icon' },
license: { uri: 'uri:license', fallbackUri: 'fallback:license' },
repository: { uri: 'uri:repository', fallbackUri: 'fallback:repository' },
signature: { uri: 'uri:signature', fallbackUri: 'fallback:signature' },
coreTranslations: []
});
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local1, local2]);
@@ -1440,6 +1442,7 @@ suite('ExtensionsWorkbenchServiceTest', () => {
manifest: null,
readme: null,
repository: null,
signature: null,
coreTranslations: []
};
@@ -5,7 +5,7 @@
import { Event, EventMultiplexer } from 'vs/base/common/event';
import {
ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallVSIXOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata
ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallVSIXOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from 'vs/platform/extensions/common/extensions';
@@ -188,6 +188,13 @@ export class ExtensionManagementService extends Disposable implements IWorkbench
.map(({ extensionManagementService }) => extensionManagementService.unzip(zipLocation))).then(([extensionIdentifier]) => extensionIdentifier);
}
download(extension: IGalleryExtension, operation: InstallOperation): Promise<URI> {
if (this.extensionManagementServerService.localExtensionManagementServer) {
return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.download(extension, operation);
}
throw new Error('Cannot download extension');
}
async install(vsix: URI, options?: InstallVSIXOptions): Promise<ILocalExtension> {
const manifest = await this.getManifest(vsix);
return this.installVSIX(vsix, manifest, options);
@@ -147,6 +147,7 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe
unzip(zipLocation: URI): Promise<IExtensionIdentifier> { throw new Error('unsupported'); }
getManifest(vsix: URI): Promise<IExtensionManifest> { throw new Error('unsupported'); }
updateExtensionScope(): Promise<ILocalExtension> { throw new Error('unsupported'); }
download(): Promise<URI> { throw new Error('unsupported'); }
private async whenProfileChanged(e: DidChangeUserDataProfileEvent): Promise<void> {
const previousProfileLocation = e.previous.extensionsResource;
@@ -92,7 +92,7 @@ export class NativeExtensionManagementService extends ExtensionManagementChannel
return { location: vsix, async cleanup() { } };
}
this.logService.trace('Downloading extension from', vsix.toString());
const location = joinPath(URI.file(this.nativeEnvironmentService.extensionsDownloadPath), generateUuid());
const location = joinPath(this.nativeEnvironmentService.extensionsDownloadLocation, generateUuid());
await this.downloadService.download(vsix, location);
this.logService.info('Downloaded extension to', location.toString());
const cleanup = async () => {
@@ -16,10 +16,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { localize } from 'vs/nls';
import { IProductService } from 'vs/platform/product/common/productService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { generateUuid } from 'vs/base/common/uuid';
import { joinPath } from 'vs/base/common/resources';
import { IExtensionManagementServer, IProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
import { Promises } from 'vs/base/common/async';
import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService';
import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
@@ -40,7 +37,6 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IProductService private readonly productService: IProductService,
@INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService,
@IFileService private readonly fileService: IFileService,
@IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService,
) {
@@ -120,9 +116,8 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC
private async downloadCompatibleAndInstall(extension: IGalleryExtension, installed: ILocalExtension[], installOptions: InstallOptions): Promise<ILocalExtension> {
const compatible = await this.checkAndGetCompatible(extension, !!installOptions.installPreReleaseVersion);
const location = joinPath(URI.file(this.environmentService.extensionsDownloadPath), generateUuid());
this.logService.trace('Downloading extension:', compatible.identifier.id);
await this.galleryService.download(compatible, location, installed.filter(i => areSameExtensions(i.identifier, compatible.identifier))[0] ? InstallOperation.Update : InstallOperation.Install);
const location = await this.localExtensionManagementServer.extensionManagementService.download(compatible, installed.filter(i => areSameExtensions(i.identifier, compatible.identifier))[0] ? InstallOperation.Update : InstallOperation.Install);
this.logService.info('Downloaded extension:', compatible.identifier.id, location.path);
try {
const local = await super.install(location, installOptions);
@@ -2004,6 +2004,9 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens
async updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise<ILocalExtension> { return local; }
registerParticipant(pariticipant: IExtensionManagementParticipant): void { }
async getTargetPlatform(): Promise<TargetPlatform> { return TargetPlatform.UNDEFINED; }
download(): Promise<URI> {
throw new Error('Method not implemented.');
}
}
export class TestUserDataProfileService implements IUserDataProfileService {