Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/extensionManagement/node/extensionDownloader.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
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 };
71
}
72
73
if (!extension.isSigned) {
74
return { location, verificationStatus: ExtensionSignatureVerificationCode.NotSigned };
75
}
76
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) {
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 };
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 {
100
if (signatureArchiveLocation) {
101
try {
102
// Delete signature archive always
103
await this.delete(signatureArchiveLocation);
104
} catch (error) {
105
this.logService.error(error);
106
}
107
}
108
}
109
}
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
}
126
}, 2);
127
128
if (attempts > 1) {
129
this.telemetryService.publicLog2<RetryDownloadEvent, RetryDownloadClassification>('extensiongallery:downloadvsix:retry', {
130
extensionId: extension.identifier.id,
131
attempts
132
});
133
}
134
135
return location;
136
} catch (e) {
137
throw toExtensionManagementError(e, ExtensionManagementErrorCode.Download);
138
}
139
}
140
141
private async downloadSignatureArchive(extension: IGalleryExtension): Promise<URI> {
142
try {
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
}
156
}, 2);
157
158
if (attempts > 1) {
159
this.telemetryService.publicLog2<RetryDownloadEvent, RetryDownloadClassification>('extensiongallery:downloadsigzip:retry', {
160
extensionId: extension.identifier.id,
161
attempts
162
});
163
}
164
165
return location;
166
} catch (e) {
167
throw toExtensionManagementError(e, ExtensionManagementErrorCode.DownloadSignature);
168
}
169
}
170
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
}
176
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
}
208
}
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
}
222
}
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
}
232
233
async delete(location: URI): Promise<void> {
234
await this.cleanUpPromise;
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
}
241
}
242
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
}
291
} catch (e) {
292
this.logService.error(e);
293
}
294
}
295
296
private getName(extension: IGalleryExtension): string {
297
return ExtensionKey.create(extension).toString().toLowerCase();
298
}
299
300
}
301
302