download.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. "use strict";
  2. /*---------------------------------------------------------------------------------------------
  3. * Copyright (c) Microsoft Corporation. All rights reserved.
  4. * Licensed under the MIT License. See License.txt in the project root for license information.
  5. *--------------------------------------------------------------------------------------------*/
  6. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
  7. if (k2 === undefined) k2 = k;
  8. var desc = Object.getOwnPropertyDescriptor(m, k);
  9. if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
  10. desc = { enumerable: true, get: function() { return m[k]; } };
  11. }
  12. Object.defineProperty(o, k2, desc);
  13. }) : (function(o, m, k, k2) {
  14. if (k2 === undefined) k2 = k;
  15. o[k2] = m[k];
  16. }));
  17. var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
  18. Object.defineProperty(o, "default", { enumerable: true, value: v });
  19. }) : function(o, v) {
  20. o["default"] = v;
  21. });
  22. var __importStar = (this && this.__importStar) || function (mod) {
  23. if (mod && mod.__esModule) return mod;
  24. var result = {};
  25. if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
  26. __setModuleDefault(result, mod);
  27. return result;
  28. };
  29. Object.defineProperty(exports, "__esModule", { value: true });
  30. exports.defaultCachePath = exports.fetchInsiderVersions = exports.fetchStableVersions = void 0;
  31. exports.fetchTargetInferredVersion = fetchTargetInferredVersion;
  32. exports.download = download;
  33. exports.downloadAndUnzipVSCode = downloadAndUnzipVSCode;
  34. const cp = __importStar(require("child_process"));
  35. const fs = __importStar(require("fs"));
  36. const os_1 = require("os");
  37. const path = __importStar(require("path"));
  38. const semver = __importStar(require("semver"));
  39. const stream_1 = require("stream");
  40. const util_1 = require("util");
  41. const progress_js_1 = require("./progress.js");
  42. const request = __importStar(require("./request"));
  43. const util_2 = require("./util");
  44. const extensionRoot = process.cwd();
  45. const pipelineAsync = (0, util_1.promisify)(stream_1.pipeline);
  46. const vscodeStableReleasesAPI = `https://update.code.visualstudio.com/api/releases/stable`;
  47. const vscodeInsiderReleasesAPI = `https://update.code.visualstudio.com/api/releases/insider`;
  48. const downloadDirNameFormat = /^vscode-(?<platform>[a-z0-9-]+)-(?<version>[0-9.]+)$/;
  49. const makeDownloadDirName = (platform, version) => `vscode-${platform}-${version.id}`;
  50. const DOWNLOAD_ATTEMPTS = 3;
  51. // Turn off Electron's special handling of .asar files, otherwise
  52. // extraction will fail when we try to extract node_modules.asar
  53. // under Electron's Node (i.e. in the test CLI invoked by an extension)
  54. // https://github.com/electron/packager/issues/875
  55. //
  56. // Also, trying to delete a directory with an asar in it will fail.
  57. //
  58. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  59. process.noAsar = true;
  60. exports.fetchStableVersions = (0, util_2.onceWithoutRejections)((released, timeout) => request.getJSON(`${vscodeStableReleasesAPI}?released=${released}`, timeout));
  61. exports.fetchInsiderVersions = (0, util_2.onceWithoutRejections)((released, timeout) => request.getJSON(`${vscodeInsiderReleasesAPI}?released=${released}`, timeout));
  62. /**
  63. * Returns the stable version to run tests against. Attempts to get the latest
  64. * version from the update sverice, but falls back to local installs if
  65. * not available (e.g. if the machine is offline).
  66. */
  67. async function fetchTargetStableVersion({ timeout, cachePath, platform }) {
  68. try {
  69. const versions = await (0, exports.fetchStableVersions)(true, timeout);
  70. return new util_2.Version(versions[0]);
  71. }
  72. catch (e) {
  73. return fallbackToLocalEntries(cachePath, platform, e);
  74. }
  75. }
  76. async function fetchTargetInferredVersion(options) {
  77. if (!options.extensionsDevelopmentPath) {
  78. return fetchTargetStableVersion(options);
  79. }
  80. // load all engines versions from all development paths. Then, get the latest
  81. // stable version (or, latest Insiders version) that satisfies all
  82. // `engines.vscode` constraints.
  83. const extPaths = Array.isArray(options.extensionsDevelopmentPath)
  84. ? options.extensionsDevelopmentPath
  85. : [options.extensionsDevelopmentPath];
  86. const maybeExtVersions = await Promise.all(extPaths.map(getEngineVersionFromExtension));
  87. const extVersions = maybeExtVersions.filter(util_2.isDefined);
  88. const matches = (v) => !extVersions.some((range) => !semver.satisfies(v, range, { includePrerelease: true }));
  89. try {
  90. const stable = await (0, exports.fetchStableVersions)(true, options.timeout);
  91. const found1 = stable.find(matches);
  92. if (found1) {
  93. return new util_2.Version(found1);
  94. }
  95. const insiders = await (0, exports.fetchInsiderVersions)(true, options.timeout);
  96. const found2 = insiders.find(matches);
  97. if (found2) {
  98. return new util_2.Version(found2);
  99. }
  100. const v = extVersions.join(', ');
  101. console.warn(`No version of VS Code satisfies all extension engine constraints (${v}). Falling back to stable.`);
  102. return new util_2.Version(stable[0]); // 🤷
  103. }
  104. catch (e) {
  105. return fallbackToLocalEntries(options.cachePath, options.platform, e);
  106. }
  107. }
  108. async function getEngineVersionFromExtension(extensionPath) {
  109. try {
  110. const packageContents = await fs.promises.readFile(path.join(extensionPath, 'package.json'), 'utf8');
  111. const packageJson = JSON.parse(packageContents);
  112. return packageJson?.engines?.vscode;
  113. }
  114. catch {
  115. return undefined;
  116. }
  117. }
  118. async function fallbackToLocalEntries(cachePath, platform, fromError) {
  119. const entries = await fs.promises.readdir(cachePath).catch(() => []);
  120. const [fallbackTo] = entries
  121. .map((e) => downloadDirNameFormat.exec(e))
  122. .filter(util_2.isDefined)
  123. .filter((e) => e.groups.platform === platform)
  124. .map((e) => e.groups.version)
  125. .sort((a, b) => semver.compare(b, a));
  126. if (fallbackTo) {
  127. console.warn(`Error retrieving VS Code versions, using already-installed version ${fallbackTo}`, fromError);
  128. return new util_2.Version(fallbackTo);
  129. }
  130. throw fromError;
  131. }
  132. async function isValidVersion(version, timeout) {
  133. if (version.id === 'insiders' || version.id === 'stable' || version.isCommit) {
  134. return true;
  135. }
  136. if (version.isStable) {
  137. const stableVersionNumbers = await (0, exports.fetchStableVersions)(version.isReleased, timeout);
  138. if (stableVersionNumbers.includes(version.id)) {
  139. return true;
  140. }
  141. }
  142. if (version.isInsiders) {
  143. const insiderVersionNumbers = await (0, exports.fetchInsiderVersions)(version.isReleased, timeout);
  144. if (insiderVersionNumbers.includes(version.id)) {
  145. return true;
  146. }
  147. }
  148. return false;
  149. }
  150. function getFilename(contentDisposition) {
  151. const parts = contentDisposition.split(';').map((s) => s.trim());
  152. for (const part of parts) {
  153. const match = /^filename="?([^"]*)"?$/i.exec(part);
  154. if (match) {
  155. return match[1];
  156. }
  157. }
  158. return undefined;
  159. }
  160. /**
  161. * Download a copy of VS Code archive to `.vscode-test`.
  162. *
  163. * @param version The version of VS Code to download such as '1.32.0'. You can also use
  164. * `'stable'` for downloading latest stable release.
  165. * `'insiders'` for downloading latest Insiders.
  166. */
  167. async function downloadVSCodeArchive(options) {
  168. if (!fs.existsSync(options.cachePath)) {
  169. fs.mkdirSync(options.cachePath);
  170. }
  171. const timeout = options.timeout;
  172. const version = util_2.Version.parse(options.version);
  173. const downloadUrl = (0, util_2.getVSCodeDownloadUrl)(version, options.platform);
  174. options.reporter?.report({ stage: progress_js_1.ProgressReportStage.ResolvingCDNLocation, url: downloadUrl });
  175. const res = await request.getStream(downloadUrl, timeout);
  176. if (res.statusCode !== 302) {
  177. throw 'Failed to get VS Code archive location';
  178. }
  179. const url = res.headers.location;
  180. if (!url) {
  181. throw 'Failed to get VS Code archive location';
  182. }
  183. const contentSHA256 = res.headers['x-sha256'];
  184. res.destroy();
  185. const download = await request.getStream(url, timeout);
  186. const totalBytes = Number(download.headers['content-length']);
  187. const contentDisposition = download.headers['content-disposition'];
  188. const fileName = contentDisposition ? getFilename(contentDisposition) : undefined;
  189. const isZip = fileName?.endsWith('zip') ?? url.endsWith('.zip');
  190. const timeoutCtrl = new request.TimeoutController(timeout);
  191. options.reporter?.report({
  192. stage: progress_js_1.ProgressReportStage.Downloading,
  193. url,
  194. bytesSoFar: 0,
  195. totalBytes,
  196. });
  197. let bytesSoFar = 0;
  198. download.on('data', (chunk) => {
  199. bytesSoFar += chunk.length;
  200. timeoutCtrl.touch();
  201. options.reporter?.report({
  202. stage: progress_js_1.ProgressReportStage.Downloading,
  203. url,
  204. bytesSoFar,
  205. totalBytes,
  206. });
  207. });
  208. download.on('end', () => {
  209. timeoutCtrl.dispose();
  210. options.reporter?.report({
  211. stage: progress_js_1.ProgressReportStage.Downloading,
  212. url,
  213. bytesSoFar: totalBytes,
  214. totalBytes,
  215. });
  216. });
  217. timeoutCtrl.signal.addEventListener('abort', () => {
  218. download.emit('error', new request.TimeoutError(timeout));
  219. download.destroy();
  220. });
  221. return {
  222. stream: download,
  223. format: isZip ? 'zip' : 'tgz',
  224. sha256: contentSHA256,
  225. length: totalBytes,
  226. };
  227. }
  228. /**
  229. * Unzip a .zip or .tar.gz VS Code archive stream.
  230. */
  231. async function unzipVSCode(reporter, extractDir, platform, { format, stream, length, sha256 }) {
  232. const stagingFile = path.join((0, os_1.tmpdir)(), `vscode-test-${Date.now()}.zip`);
  233. const checksum = (0, util_2.validateStream)(stream, length, sha256);
  234. if (format === 'zip') {
  235. const stripComponents = (0, util_2.isPlatformServer)(platform) ? 1 : 0;
  236. try {
  237. reporter.report({ stage: progress_js_1.ProgressReportStage.ExtractingSynchonrously });
  238. // note: this used to use Expand-Archive, but this caused a failure
  239. // on longer file paths on windows. And we used to use the streaming
  240. // "unzipper", but the module was very outdated and a bit buggy.
  241. // Instead, use jszip. It's well-used and actually 8x faster than
  242. // Expand-Archive on my machine.
  243. if (process.platform === 'win32') {
  244. const [buffer, JSZip] = await Promise.all([(0, util_2.streamToBuffer)(stream), import('jszip')]);
  245. await checksum;
  246. const content = await JSZip.default.loadAsync(buffer);
  247. // extract file with jszip
  248. for (const filename of Object.keys(content.files)) {
  249. const file = content.files[filename];
  250. if (file.dir) {
  251. continue;
  252. }
  253. const filepath = stripComponents
  254. ? path.join(extractDir, filename.split(/[/\\]/g).slice(stripComponents).join(path.sep))
  255. : path.join(extractDir, filename);
  256. // vscode update zips are trusted, but check for zip slip anyway.
  257. if (!(0, util_2.isSubdirectory)(extractDir, filepath)) {
  258. throw new Error(`Invalid zip file: ${filename}`);
  259. }
  260. await fs.promises.mkdir(path.dirname(filepath), { recursive: true });
  261. await pipelineAsync(file.nodeStream(), fs.createWriteStream(filepath));
  262. }
  263. }
  264. else {
  265. // darwin or *nix sync
  266. await pipelineAsync(stream, fs.createWriteStream(stagingFile));
  267. await checksum;
  268. // unzip does not create intermediate directories when using -d
  269. await fs.promises.mkdir(extractDir, { recursive: true });
  270. await spawnDecompressorChild('unzip', ['-q', stagingFile, '-d', extractDir]);
  271. // unzip has no --strip-components equivalent
  272. if (stripComponents) {
  273. const files = await fs.promises.readdir(extractDir);
  274. for (const file of files) {
  275. const dirPath = path.join(extractDir, file);
  276. const children = await fs.promises.readdir(dirPath);
  277. await Promise.all(children.map((c) => fs.promises.rename(path.join(dirPath, c), path.join(extractDir, c))));
  278. await fs.promises.rmdir(dirPath);
  279. }
  280. }
  281. }
  282. }
  283. finally {
  284. fs.unlink(stagingFile, () => undefined);
  285. }
  286. }
  287. else {
  288. const stripComponents = (0, util_2.isPlatformCLI)(platform) ? 0 : 1;
  289. // tar does not create extractDir by default
  290. if (!fs.existsSync(extractDir)) {
  291. fs.mkdirSync(extractDir);
  292. }
  293. // The CLI is a singular binary that doesn't have a wrapper component to remove
  294. await spawnDecompressorChild('tar', ['-xzf', '-', `--strip-components=${stripComponents}`, '-C', extractDir], stream);
  295. await checksum;
  296. }
  297. }
  298. function spawnDecompressorChild(command, args, input) {
  299. return new Promise((resolve, reject) => {
  300. const child = cp.spawn(command, args, { stdio: 'pipe' });
  301. if (input) {
  302. input.on('error', reject);
  303. input.pipe(child.stdin);
  304. }
  305. child.stderr.pipe(process.stderr);
  306. child.stdout.pipe(process.stdout);
  307. child.on('error', reject);
  308. child.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`Failed to unzip archive, exited with ${code}`)));
  309. });
  310. }
  311. exports.defaultCachePath = path.resolve(extensionRoot, '.vscode-test');
  312. const COMPLETE_FILE_NAME = 'is-complete';
  313. /**
  314. * Download and unzip a copy of VS Code.
  315. * @returns Promise of `vscodeExecutablePath`.
  316. */
  317. async function download(options = {}) {
  318. const inputVersion = options?.version ? util_2.Version.parse(options.version) : undefined;
  319. const { platform = util_2.systemDefaultPlatform, cachePath = exports.defaultCachePath, reporter = await (0, progress_js_1.makeConsoleReporter)(), timeout = 15_000, } = options;
  320. let version;
  321. if (inputVersion?.id === 'stable') {
  322. version = await fetchTargetStableVersion({ timeout, cachePath, platform });
  323. }
  324. else if (inputVersion) {
  325. /**
  326. * Only validate version against server when no local download that matches version exists
  327. */
  328. if (!fs.existsSync(path.resolve(cachePath, makeDownloadDirName(platform, inputVersion)))) {
  329. if (!(await isValidVersion(inputVersion, timeout))) {
  330. throw Error(`Invalid version ${inputVersion.id}`);
  331. }
  332. }
  333. version = inputVersion;
  334. }
  335. else {
  336. version = await fetchTargetInferredVersion({
  337. timeout,
  338. cachePath,
  339. platform,
  340. extensionsDevelopmentPath: options.extensionDevelopmentPath,
  341. });
  342. }
  343. if (platform === 'win32-archive' && semver.satisfies(version.id, '>= 1.85.0', { includePrerelease: true })) {
  344. throw new Error('Windows 32-bit is no longer supported from v1.85 onwards');
  345. }
  346. reporter.report({ stage: progress_js_1.ProgressReportStage.ResolvedVersion, version: version.toString() });
  347. const downloadedPath = path.resolve(cachePath, makeDownloadDirName(platform, version));
  348. if (fs.existsSync(path.join(downloadedPath, COMPLETE_FILE_NAME))) {
  349. if (version.isInsiders) {
  350. reporter.report({ stage: progress_js_1.ProgressReportStage.FetchingInsidersMetadata });
  351. const { version: currentHash, date: currentDate } = (0, util_2.insidersDownloadDirMetadata)(downloadedPath, platform, reporter);
  352. const { version: latestHash, timestamp: latestTimestamp } = version.id === 'insiders' // not qualified with a date
  353. ? await (0, util_2.getLatestInsidersMetadata)(util_2.systemDefaultPlatform, version.isReleased)
  354. : await (0, util_2.getInsidersVersionMetadata)(util_2.systemDefaultPlatform, version.id, version.isReleased);
  355. if (currentHash === latestHash) {
  356. reporter.report({ stage: progress_js_1.ProgressReportStage.FoundMatchingInstall, downloadedPath });
  357. return Promise.resolve((0, util_2.insidersDownloadDirToExecutablePath)(downloadedPath, platform));
  358. }
  359. else {
  360. try {
  361. reporter.report({
  362. stage: progress_js_1.ProgressReportStage.ReplacingOldInsiders,
  363. downloadedPath,
  364. oldDate: currentDate,
  365. oldHash: currentHash,
  366. newDate: new Date(latestTimestamp),
  367. newHash: latestHash,
  368. });
  369. await fs.promises.rm(downloadedPath, { force: true, recursive: true });
  370. }
  371. catch (err) {
  372. reporter.error(err);
  373. throw Error(`Failed to remove outdated Insiders at ${downloadedPath}.`);
  374. }
  375. }
  376. }
  377. else if (version.isStable) {
  378. reporter.report({ stage: progress_js_1.ProgressReportStage.FoundMatchingInstall, downloadedPath });
  379. return Promise.resolve((0, util_2.downloadDirToExecutablePath)(downloadedPath, platform));
  380. }
  381. else {
  382. reporter.report({ stage: progress_js_1.ProgressReportStage.FoundMatchingInstall, downloadedPath });
  383. return Promise.resolve((0, util_2.insidersDownloadDirToExecutablePath)(downloadedPath, platform));
  384. }
  385. }
  386. for (let i = 0;; i++) {
  387. try {
  388. await fs.promises.rm(downloadedPath, { recursive: true, force: true });
  389. const download = await downloadVSCodeArchive({
  390. version: version.toString(),
  391. platform,
  392. cachePath,
  393. reporter,
  394. timeout,
  395. });
  396. // important! do not put anything async here, since unzipVSCode will need
  397. // to start consuming the stream immediately.
  398. await unzipVSCode(reporter, downloadedPath, platform, download);
  399. await fs.promises.writeFile(path.join(downloadedPath, COMPLETE_FILE_NAME), '');
  400. reporter.report({ stage: progress_js_1.ProgressReportStage.NewInstallComplete, downloadedPath });
  401. break;
  402. }
  403. catch (error) {
  404. if (i++ < DOWNLOAD_ATTEMPTS) {
  405. reporter.report({
  406. stage: progress_js_1.ProgressReportStage.Retrying,
  407. attempt: i,
  408. error: error,
  409. totalAttempts: DOWNLOAD_ATTEMPTS,
  410. });
  411. }
  412. else {
  413. reporter.error(error);
  414. throw Error(`Failed to download and unzip VS Code ${version}`);
  415. }
  416. }
  417. }
  418. reporter.report({ stage: progress_js_1.ProgressReportStage.NewInstallComplete, downloadedPath });
  419. if (version.isStable) {
  420. return (0, util_2.downloadDirToExecutablePath)(downloadedPath, platform);
  421. }
  422. else {
  423. return (0, util_2.insidersDownloadDirToExecutablePath)(downloadedPath, platform);
  424. }
  425. }
  426. async function downloadAndUnzipVSCode(versionOrOptions, platform, reporter, extractSync) {
  427. return await download(typeof versionOrOptions === 'object'
  428. ? versionOrOptions
  429. : { version: versionOrOptions, platform, reporter, extractSync });
  430. }