1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

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
This commit is contained in:
Matt Mastracci 2023-09-07 11:13:11 -06:00 committed by GitHub
parent 9226207c01
commit 8eda8c0dbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -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) {