src/vs/platform/extensionManagement/node/extensionDownloader.ts

300 LOC · 170 covered · 130 uncovered · 27 ranges · 5 concepts · 5 introducers · 7 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts · 1141 LOCcommon/abstractExtension…src/vs/platform/extensionManagement/node/extensionManagementUtil.ts · 37 LOCnode/extensionManagement…src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts · 136 LOCnode/extensionSignatureV…src/vs/platform/files/common/inMemoryFilesystemProvider.ts · 358 LOCcommon/inMemoryFilesyste…extensionDownloader.ts ×10 · 52 introduced LOCextensionDownloader.ts ×…extensionDownloader.ts ×1 · 2 introduced LOCextensionDownloader.ts ×…extensionDownloader.ts ×1 · 2 introduced LOCextensionDownloader.ts ×…extensionDownloader.ts ×1 · 2 introduced LOCextensionDownloader.ts ×…abstractExtensionManagementService.ts ×32 · 395 introduced LOCabstractExtensionManagem…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully for an unsigned extension even when signature verification throws error|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully for an unsigned extension even when signature verification throws error|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully for unsigned extension|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully for unsigned extension|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification fails to execute|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification fails to execute|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification fails|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification fails|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification is disabled because the module is not loaded|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification is disabled because the module is not loaded|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification is disabled by options|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification is disabled by options|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification succeeds|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification succeeds|occurrence=1extensionDownloader.test…Focused file · src/vs/platform/extensionManagement/node/extensionDownloader.ts · 300 LOCnode/extensionDownloader…

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- abstractExtensionManagementService.ts ×32
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { Promises } from '../../../base/common/async.js';
7 > import { getErrorMessage } from '../../../base/common/errors.js';
8 > import { Disposable } from '../../../base/common/lifecycle.js';
9 > import { Schemas } from '../../../base/common/network.js';
10 > import { joinPath } from '../../../base/common/resources.js';
11 > import * as semver from '../../../base/common/semver/semver.js';
12 > import { URI } from '../../../base/common/uri.js';
13 > import { generateUuid } from '../../../base/common/uuid.js';
14 > import { Promises as FSPromises } from '../../../base/node/pfs.js';
15 > import { buffer, CorruptZipMessage } from '../../../base/node/zip.js';
16 > import { INativeEnvironmentService } from '../../environment/common/environment.js';
17 > import { toExtensionManagementError } from '../common/abstractExtensionManagementService.js';
18 > import { ExtensionManagementError, ExtensionManagementErrorCode, ExtensionSignatureVerificationCode, IExtensionGalleryService, IGalleryExtension, InstallOperation } from '../common/extensionManagement.js';
19 > import { ExtensionKey, groupByExtension } from '../common/extensionManagementUtil.js';
20 > import { fromExtractError } from './extensionManagementUtil.js';
21 > import { IExtensionSignatureVerificationService } from './extensionSignatureVerificationService.js';
22 > import { TargetPlatform } from '../../extensions/common/extensions.js';
23 > import { FileOperationResult, IFileService, IFileStatWithMetadata, toFileOperationResult } from '../../files/common/files.js';
24 > import { ILogService } from '../../log/common/log.js';
25 > import { ITelemetryService } from '../../telemetry/common/telemetry.js';
26 > import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
27 >
28 > type RetryDownloadClassification = {
29 > owner: 'sandy081';
30 > comment: 'Event reporting the retry of downloading';
31 > extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension Id' };
32 > attempts: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Number of Attempts' };
33 > };
34 > type RetryDownloadEvent = {
35 > extensionId: string;
36 > attempts: number;
37 > };
38 >
39 > export class ExtensionsDownloader extends Disposable {
40 >
41 > private static readonly SignatureArchiveExtension = '.sigzip';
42 >
43 > readonly extensionsDownloadDir: URI;
44 > private readonly extensionsTrashDir: URI;
45 > private readonly cache: number;
46 > private readonly cleanUpPromise: Promise<void>;
47 >
48 > constructor(
49 > @INativeEnvironmentService environmentService: INativeEnvironmentService,
50 > @IFileService private readonly fileService: IFileService,
51 > @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
52 > @IExtensionSignatureVerificationService private readonly extensionSignatureVerificationService: IExtensionSignatureVerificationService,
53 > @ITelemetryService private readonly telemetryService: ITelemetryService,
54 > @IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
55 > @ILogService private readonly logService: ILogService,
56 > ) {
57 > super();
58 > this.extensionsDownloadDir = environmentService.extensionsDownloadLocation;
59 > this.extensionsTrashDir = uriIdentityService.extUri.joinPath(environmentService.extensionsDownloadLocation, `.trash`);
60 > this.cache = 20; // Cache 20 downloaded VSIX files
61 > this.cleanUpPromise = this.cleanUp();
62 > }
63 >
64 > async download(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean, clientTargetPlatform?: TargetPlatform): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionSignatureVerificationCode | undefined }> {
65 > await this.cleanUpPromise;
66 >
67 > const location = await this.downloadVSIX(extension, operation);
68 >
69 > if (!verifySignature) {
70 > return { location, verificationStatus: undefined }; extensionDownloader.ts ×1
71 > }
73 > if (!extension.isSigned) {
74 > return { location, verificationStatus: ExtensionSignatureVerificationCode.NotSigned }; extensionDownloader.ts ×1
75 > }
77 > let signatureArchiveLocation;
78 > try {
79 > signatureArchiveLocation = await this.downloadSignatureArchive(extension);
80 > const verificationStatus = (await this.extensionSignatureVerificationService.verify(extension.identifier.id, extension.version, location.fsPath, signatureArchiveLocation.fsPath, clientTargetPlatform))?.code;
81 > if (verificationStatus === ExtensionSignatureVerificationCode.PackageIsInvalidZip || verificationStatus === ExtensionSignatureVerificationCode.SignatureArchiveIsInvalidZip) { abstractExtensionManagementService.ts ×32
82 try {
83 // Delete the downloaded vsix if VSIX or signature archive is invalid
84 await this.delete(location);
85 } catch (error) {
86 this.logService.error(error);
87 }
88 throw new ExtensionManagementError(CorruptZipMessage, ExtensionManagementErrorCode.CorruptZip);
89 }
90 > return { location, verificationStatus }; extensionDownloader.ts ×10
91 > } catch (error) {
92 try {
93 // Delete the downloaded VSIX if signature archive download fails
94 await this.delete(location);
95 } catch (error) {
96 this.logService.error(error);
97 }
98 throw error;
99 > } finally { extensionDownloader.ts ×10
100 > if (signatureArchiveLocation) {
101 > try {
102 > // Delete signature archive always
103 > await this.delete(signatureArchiveLocation);
104 > } catch (error) {
105 this.logService.error(error);
106 }
108 > }
110 >
111 > private async downloadVSIX(extension: IGalleryExtension, operation: InstallOperation): Promise<URI> {
112 > try {
113 > const location = joinPath(this.extensionsDownloadDir, this.getName(extension));
114 > const attempts = await this.doDownload(extension, 'vsix', async () => {
115 > await this.downloadFile(extension, location, location => this.extensionGalleryService.download(extension, location, operation));
116 > try {
117 > await this.validate(location.fsPath, 'extension/package.json');
118 > } catch (error) {
119 try {
120 await this.fileService.del(location);
121 } catch (e) {
122 this.logService.warn(`Error while deleting: ${location.path}`, getErrorMessage(e));
123 }
124 throw error;
125 }
127 >
128 > if (attempts > 1) {
129 this.telemetryService.publicLog2<RetryDownloadEvent, RetryDownloadClassification>('extensiongallery:downloadvsix:retry', {
130 extensionId: extension.identifier.id,
131 attempts
132 });
133 }
135 > return location;
136 > } catch (e) {
137 throw toExtensionManagementError(e, ExtensionManagementErrorCode.Download);
138 }
140 >
141 > private async downloadSignatureArchive(extension: IGalleryExtension): Promise<URI> {
143 > const location = joinPath(this.extensionsDownloadDir, `${this.getName(extension)}${ExtensionsDownloader.SignatureArchiveExtension}`);
144 > const attempts = await this.doDownload(extension, 'sigzip', async () => {
145 > await this.extensionGalleryService.downloadSignatureArchive(extension, location);
146 > try {
147 > await this.validate(location.fsPath, '.signature.p7s');
148 > } catch (error) {
149 try {
150 await this.fileService.del(location);
151 } catch (e) {
152 this.logService.warn(`Error while deleting: ${location.path}`, getErrorMessage(e));
153 }
154 throw error;
155 }
157 >
158 > if (attempts > 1) {
159 this.telemetryService.publicLog2<RetryDownloadEvent, RetryDownloadClassification>('extensiongallery:downloadsigzip:retry', {
160 extensionId: extension.identifier.id,
161 attempts
162 });
163 }
165 > return location;
166 > } catch (e) {
167 throw toExtensionManagementError(e, ExtensionManagementErrorCode.DownloadSignature);
168 }
171 > private async downloadFile(extension: IGalleryExtension, location: URI, downloadFn: (location: URI) => Promise<void>): Promise<void> {
172 > // Do not download if exists
173 > if (await this.fileService.exists(location)) {
174 return;
175 }
177 > // Download directly if locaiton is not file scheme
178 > if (location.scheme !== Schemas.file) {
179 > await downloadFn(location);
180 > return;
181 > }
182
183 // Download to temporary location first only if file does not exist
184 const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`);
185 try {
186 await downloadFn(tempLocation);
187 } catch (error) {
188 try {
189 await this.fileService.del(tempLocation);
190 } catch (e) { /* ignore */ }
191 throw error;
192 }
193
194 try {
195 // Rename temp location to original
196 await FSPromises.rename(tempLocation.fsPath, location.fsPath, 2 * 60 * 1000 /* Retry for 2 minutes */);
197 } catch (error) {
198 try { await this.fileService.del(tempLocation); } catch (e) { /* ignore */ }
199 let exists = false;
200 try { exists = await this.fileService.exists(location); } catch (e) { /* ignore */ }
201 if (exists) {
202 this.logService.info(`Rename failed because the file was downloaded by another source. So ignoring renaming.`, extension.identifier.id, location.path);
203 } else {
204 this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the file from downloaded location`, tempLocation.path);
205 throw error;
206 }
207 }
209 >
210 > private async doDownload(extension: IGalleryExtension, name: string, downloadFn: () => Promise<void>, retries: number): Promise<number> {
211 > let attempts = 1;
212 > while (true) {
213 > try {
214 > await downloadFn();
215 > return attempts;
216 > } catch (e) {
217 if (attempts++ > retries) {
218 throw e;
219 }
220 this.logService.warn(`Failed downloading ${name}. ${getErrorMessage(e)}. Retry again...`, extension.identifier.id);
221 }
223 > }
224 >
225 > protected async validate(zipPath: string, filePath: string): Promise<void> {
226 try {
227 await buffer(zipPath, filePath);
228 } catch (e) {
229 throw fromExtractError(e);
230 }
231 }
233 > async delete(location: URI): Promise<void> {
234 > await this.cleanUpPromise; extensionDownloader.ts ×10
235 > const trashRelativePath = this.uriIdentityService.extUri.relativePath(this.extensionsDownloadDir, location);
236 > if (trashRelativePath) {
237 > await this.fileService.move(location, this.uriIdentityService.extUri.joinPath(this.extensionsTrashDir, trashRelativePath), true);
238 > } else {
239 await this.fileService.del(location);
240 }
243 > private async cleanUp(): Promise<void> {
244 > try {
245 > if (!(await this.fileService.exists(this.extensionsDownloadDir))) {
246 > this.logService.trace('Extension VSIX downloads cache dir does not exist');
247 > return;
248 > }
249
250 try {
251 await this.fileService.del(this.extensionsTrashDir, { recursive: true });
252 } catch (error) {
253 if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
254 this.logService.error(error);
255 }
256 }
257
258 const folderStat = await this.fileService.resolve(this.extensionsDownloadDir, { resolveMetadata: true });
259 if (folderStat.children) {
260 const toDelete: URI[] = [];
261 const vsixs: [ExtensionKey, IFileStatWithMetadata][] = [];
262 const signatureArchives: URI[] = [];
263
264 for (const stat of folderStat.children) {
265 if (stat.name.endsWith(ExtensionsDownloader.SignatureArchiveExtension)) {
266 signatureArchives.push(stat.resource);
267 } else {
268 const extension = ExtensionKey.parse(stat.name);
269 if (extension) {
270 vsixs.push([extension, stat]);
271 }
272 }
273 }
274
275 const byExtension = groupByExtension(vsixs, ([extension]) => extension);
276 const distinct: IFileStatWithMetadata[] = [];
277 for (const p of byExtension) {
278 p.sort((a, b) => semver.rcompare(a[0].version, b[0].version));
279 toDelete.push(...p.slice(1).map(e => e[1].resource)); // Delete outdated extensions
280 distinct.push(p[0][1]);
281 }
282 distinct.sort((a, b) => a.mtime - b.mtime); // sort by modified time
283 toDelete.push(...distinct.slice(0, Math.max(0, distinct.length - this.cache)).map(s => s.resource)); // Retain minimum cacheSize and delete the rest
284 toDelete.push(...signatureArchives); // Delete all signature archives
285
286 await Promises.settled(toDelete.map(resource => {
287 this.logService.trace('Deleting from cache', resource.path);
288 return this.fileService.del(resource);
289 }));
290 }
292 this.logService.error(e);
293 }
295 >
296 > private getName(extension: IGalleryExtension): string {
297 > return ExtensionKey.create(extension).toString().toLowerCase();
298 > }
299 >
300 > }