UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

252 lines (251 loc) 9.47 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); }; }; import { Promises } from '@sussudio/base/common/async.mjs'; import { getErrorMessage } from '@sussudio/base/common/errors.mjs'; import { Disposable } from '@sussudio/base/common/lifecycle.mjs'; import { Schemas } from '@sussudio/base/common/network.mjs'; import { isWindows } from '@sussudio/base/common/platform.mjs'; import { joinPath } from '@sussudio/base/common/resources.mjs'; import * as semver from '@sussudio/base/common/semver/semver.mjs'; import { isBoolean } from '@sussudio/base/common/types.mjs'; import { generateUuid } from '@sussudio/base/common/uuid.mjs'; import { Promises as FSPromises } from '@sussudio/base/node/pfs.mjs'; import { IConfigurationService } from '../../configuration/common/configuration.mjs'; import { INativeEnvironmentService } from '../../environment/common/environment.mjs'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, } from '../common/extensionManagement.mjs'; import { ExtensionKey, groupByExtension } from '../common/extensionManagementUtil.mjs'; import { IExtensionSignatureVerificationService } from './extensionSignatureVerificationService.mjs'; import { IFileService } from '../../files/common/files.mjs'; import { ILogService } from '../../log/common/log.mjs'; import { IProductService } from '../../product/common/productService.mjs'; let ExtensionsDownloader = class ExtensionsDownloader extends Disposable { fileService; extensionGalleryService; configurationService; productService; extensionSignatureVerificationService; logService; static SignatureArchiveExtension = '.sigzip'; extensionsDownloadDir; cache; cleanUpPromise; constructor( environmentService, fileService, extensionGalleryService, configurationService, productService, extensionSignatureVerificationService, logService, ) { super(); this.fileService = fileService; this.extensionGalleryService = extensionGalleryService; this.configurationService = configurationService; this.productService = productService; this.extensionSignatureVerificationService = extensionSignatureVerificationService; this.logService = logService; this.extensionsDownloadDir = environmentService.extensionsDownloadLocation; this.cache = 20; // Cache 20 downloaded VSIX files this.cleanUpPromise = this.cleanUp(); } async download(extension, operation) { await this.cleanUpPromise; 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 verificationStatus = 'Unverified'; /* ExtensionVerificationStatus.Unverified */ if (await this.shouldVerifySignature(extension)) { const signatureArchiveLocation = await this.downloadSignatureArchive(extension); try { const verified = await this.extensionSignatureVerificationService.verify( location.fsPath, signatureArchiveLocation.fsPath, ); if (verified) { verificationStatus = 'Verified' /* ExtensionVerificationStatus.Verified */; } this.logService.info( `Extension signature verification: ${extension.identifier.id}. Verification status: ${verificationStatus}.`, ); } catch (error) { const code = error.code; if (code === 'UnknownError') { verificationStatus = 'UnknownError' /* ExtensionVerificationStatus.UnknownError */; this.logService.warn( `Extension signature verification: ${extension.identifier.id}. Verification status: ${verificationStatus}.`, ); } else { await this.delete(signatureArchiveLocation); await this.delete(location); throw new ExtensionManagementError(code, ExtensionManagementErrorCode.Signature); } } } return { location, verificationStatus }; } async shouldVerifySignature(extension) { if (!extension.isSigned) { return false; } const value = this.configurationService.getValue('extensions.verifySignature'); if (isBoolean(value)) { return value; } return this.productService.quality !== 'stable'; } async downloadSignatureArchive(extension) { 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; } async downloadFile(extension, location, downloadFn) { // 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) { await this.cleanUpPromise; await this.fileService.del(location); } async rename(from, to, retryUntil) { try { await FSPromises.rename(from.fsPath, to.fsPath); } catch (error) { if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { this.logService.info(`Failed renaming ${from} to ${to} with 'EPERM' error. Trying again...`); return this.rename(from, to, retryUntil); } throw error; } } async cleanUp() { try { if (!(await this.fileService.exists(this.extensionsDownloadDir))) { 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 = []; const vsixs = []; const signatureArchives = []; for (const stat of folderStat.children) { 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(vsixs, ([extension]) => extension); const distinct = []; for (const p of byExtension) { p.sort((a, b) => semver.rcompare(a[0].version, b[0].version)); toDelete.push(...p.slice(1).map((e) => e[1].resource)); // Delete outdated extensions distinct.push(p[0][1]); } 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 from cache', resource.path); return this.fileService.del(resource); }), ); } } catch (e) { this.logService.error(e); } } getName(extension) { return this.cache ? ExtensionKey.create(extension).toString().toLowerCase() : generateUuid(); } }; ExtensionsDownloader = __decorate( [ __param(0, INativeEnvironmentService), __param(1, IFileService), __param(2, IExtensionGalleryService), __param(3, IConfigurationService), __param(4, IProductService), __param(5, IExtensionSignatureVerificationService), __param(6, ILogService), ], ExtensionsDownloader, ); export { ExtensionsDownloader };