summaryrefslogtreecommitdiff
path: root/tools/util.js
diff options
context:
space:
mode:
authorMatt Mastracci <matthew@mastracci.com>2023-09-07 11:13:11 -0600
committerGitHub <noreply@github.com>2023-09-07 17:13:11 +0000
commit8eda8c0dbd05afa7313216a1c91d64a41442c9e9 (patch)
treed6674c8dafcf3c83769bb3df3e3901538d47c66f /tools/util.js
parent9226207c017666ef10712fa3fda35f773025ae03 (diff)
chore: make downloadPrebuilt a little more reliable (#20401)
Makes the prebuilt installation a bit more reliable: - Check for 200 - Check for an executable header (MZ, ELF, etc) - Download to a .temp file until we're certain the file is valid - If multiple requests for a tool are made, only run one task
Diffstat (limited to 'tools/util.js')
-rw-r--r--tools/util.js59
1 files changed, 57 insertions, 2 deletions
diff --git a/tools/util.js b/tools/util.js
index de28b6679..1a4ee89be 100644
--- a/tools/util.js
+++ b/tools/util.js
@@ -1,4 +1,5 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+import { deferred } from "../test_util/std/async/deferred.ts";
import {
dirname,
fromFileUrl,
@@ -115,10 +116,45 @@ const platformDirName = {
const executableSuffix = Deno.build.os === "windows" ? ".exe" : "";
+async function sanityCheckPrebuiltFile(toolPath) {
+ const stat = await Deno.stat(toolPath);
+ if (stat.size < PREBUILT_MINIMUM_SIZE) {
+ throw new Error(
+ `File size ${stat.size} is less than expected minimum file size ${PREBUILT_MINIMUM_SIZE}`,
+ );
+ }
+ const file = await Deno.open(toolPath, { read: true });
+ const buffer = new Uint8Array(1024);
+ let n = 0;
+ while (n < 1024) {
+ n += await file.read(buffer.subarray(n));
+ }
+
+ // Mac: OK
+ if (buffer[0] == 0xcf && buffer[1] == 0xfa) {
+ return;
+ }
+
+ // Windows OK
+ if (buffer[0] == "M".charCodeAt(0) && buffer[1] == "Z".charCodeAt(0)) {
+ return;
+ }
+
+ // Linux OK
+ if (
+ buffer[0] == 0x7f && buffer[1] == "E".charCodeAt(0) &&
+ buffer[2] == "L".charCodeAt(0) && buffer[3] == "F".charCodeAt(0)
+ ) {
+ return;
+ }
+
+ throw new Error(`Invalid executable (header was ${buffer.subarray(0, 16)}`);
+}
+
export async function getPrebuilt(toolName) {
const toolPath = getPrebuiltToolPath(toolName);
try {
- await Deno.stat(toolPath);
+ await sanityCheckPrebuiltFile(toolPath);
const versionOk = await verifyVersion(toolName);
if (!versionOk) {
throw new Error("Version mismatch");
@@ -132,6 +168,8 @@ export async function getPrebuilt(toolName) {
const PREBUILT_PATH = join(ROOT_PATH, "third_party", "prebuilt");
const PREBUILT_TOOL_DIR = join(PREBUILT_PATH, platformDirName);
+const PREBUILT_MINIMUM_SIZE = 16 * 1024;
+const DOWNLOAD_TASKS = {};
export function getPrebuiltToolPath(toolName) {
return join(PREBUILT_TOOL_DIR, toolName + executableSuffix);
@@ -141,8 +179,15 @@ const downloadUrl =
`https://raw.githubusercontent.com/denoland/deno_third_party/69ffd968c0c435f5f9dbba713a92b4fb6a3e2301/prebuilt/${platformDirName}`;
export async function downloadPrebuilt(toolName) {
+ // Ensure only one download per tool happens at a time
+ if (DOWNLOAD_TASKS[toolName]) {
+ return await DOWNLOAD_TASKS[toolName];
+ }
+
+ const downloadPromise = DOWNLOAD_TASKS[toolName] = deferred();
const spinner = wait("Downloading prebuilt tool: " + toolName).start();
const toolPath = getPrebuiltToolPath(toolName);
+ const tempFile = `${toolPath}.temp`;
try {
await Deno.mkdir(PREBUILT_TOOL_DIR, { recursive: true });
@@ -150,19 +195,29 @@ export async function downloadPrebuilt(toolName) {
const url = `${downloadUrl}/${toolName}${executableSuffix}`;
const resp = await fetch(url);
- const file = await Deno.open(toolPath, {
+ if (!resp.ok) {
+ throw new Error(`Non-successful response from ${url}: ${resp.status}`);
+ }
+
+ const file = await Deno.open(tempFile, {
create: true,
write: true,
mode: 0o755,
});
await resp.body.pipeTo(file.writable);
+ spinner.text = `Checking prebuilt tool: ${toolName}`;
+ await sanityCheckPrebuiltFile(tempFile);
+ spinner.text = `Successfully downloaded: ${toolName}`;
+ await Deno.rename(tempFile, toolPath);
} catch (e) {
spinner.fail();
+ downloadPromise.reject(e);
throw e;
}
spinner.succeed();
+ downloadPromise.resolve(null);
}
export async function verifyVersion(toolName) {