mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-02 23:05:15 +01:00
Merge pull request #162285 from dtivel/dtivel/verify-packages
Add extension signature verification service
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+3
@@ -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: []
|
||||
};
|
||||
|
||||
|
||||
+3
@@ -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;
|
||||
|
||||
+1
-1
@@ -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 () => {
|
||||
|
||||
+1
-6
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user