|
|
|
@@ -1,8 +1,13 @@
|
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
import { execFile as execFileCallback, spawnSync } from "node:child_process";
|
|
|
|
|
import { readFileSync } from "node:fs";
|
|
|
|
|
import { promisify } from "node:util";
|
|
|
|
|
|
|
|
|
|
const catalog = JSON.parse(readFileSync(new URL("../distribution/node-runtime.json", import.meta.url), "utf8"));
|
|
|
|
|
const expected = new Map(Object.values(catalog.platforms).map((value) => [value.file, value.sha256]));
|
|
|
|
|
const execFile = promisify(execFileCallback);
|
|
|
|
|
const curlAvailable = spawnSync("curl", ["--version"], { stdio: "ignore" }).status === 0;
|
|
|
|
|
const nullDevice = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
|
|
|
const results = await Promise.all(catalog.sourceBases.map(checkSource));
|
|
|
|
|
for (const result of results) {
|
|
|
|
|
console.log(`${result.ok ? "OK" : "FAIL"} ${result.host}: ${result.message}`);
|
|
|
|
@@ -18,18 +23,11 @@ async function checkSource(sourceBase) {
|
|
|
|
|
const timeout = setTimeout(() => controller.abort(), 20_000);
|
|
|
|
|
timeout.unref();
|
|
|
|
|
try {
|
|
|
|
|
const checksumResponse = await fetch(new URL("SHASUMS256.txt", base), { signal: controller.signal });
|
|
|
|
|
if (!checksumResponse.ok) return failure(base, `SHASUMS256.txt HTTP ${checksumResponse.status}`);
|
|
|
|
|
const checksums = parseChecksums(await checksumResponse.text());
|
|
|
|
|
const checksums = parseChecksums(await requestText(new URL("SHASUMS256.txt", base), controller.signal));
|
|
|
|
|
for (const [file, sha256] of expected) {
|
|
|
|
|
if (checksums.get(file) !== sha256) return failure(base, `${file} 的官方 SHA-256 不匹配或缺失`);
|
|
|
|
|
}
|
|
|
|
|
const heads = await Promise.all([...expected.keys()].map(async (file) => {
|
|
|
|
|
const response = await fetch(new URL(file, base), { method: "HEAD", signal: controller.signal });
|
|
|
|
|
return { file, status: response.status, ok: response.ok };
|
|
|
|
|
}));
|
|
|
|
|
const unavailable = heads.filter((item) => !item.ok);
|
|
|
|
|
if (unavailable.length) return failure(base, `${unavailable[0].file} HTTP ${unavailable[0].status}`);
|
|
|
|
|
await Promise.all([...expected.keys()].map((file) => assertAvailable(new URL(file, base), controller.signal)));
|
|
|
|
|
return { host: base.host, ok: true, message: `${expected.size} 个三平台制品可用且哈希一致` };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return failure(base, error instanceof Error ? error.message : String(error));
|
|
|
|
@@ -38,6 +36,31 @@ async function checkSource(sourceBase) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function requestText(url, signal) {
|
|
|
|
|
if (!curlAvailable) {
|
|
|
|
|
const response = await fetch(url, { signal });
|
|
|
|
|
if (!response.ok) throw new Error(`${url.pathname} HTTP ${response.status}`);
|
|
|
|
|
return response.text();
|
|
|
|
|
}
|
|
|
|
|
const { stdout } = await execFile("curl", [
|
|
|
|
|
"--fail", "--silent", "--show-error", "--location",
|
|
|
|
|
"--connect-timeout", "10", "--max-time", "20", url.toString(),
|
|
|
|
|
], { encoding: "utf8", maxBuffer: 2 * 1024 * 1024, signal });
|
|
|
|
|
return stdout;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function assertAvailable(url, signal) {
|
|
|
|
|
if (!curlAvailable) {
|
|
|
|
|
const response = await fetch(url, { method: "HEAD", signal });
|
|
|
|
|
if (!response.ok) throw new Error(`${url.pathname} HTTP ${response.status}`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await execFile("curl", [
|
|
|
|
|
"--fail", "--silent", "--show-error", "--location", "--head",
|
|
|
|
|
"--connect-timeout", "10", "--max-time", "20", "--output", nullDevice, url.toString(),
|
|
|
|
|
], { encoding: "utf8", maxBuffer: 256 * 1024, signal });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseChecksums(body) {
|
|
|
|
|
const values = new Map();
|
|
|
|
|
for (const line of body.split(/\r?\n/)) {
|
|
|
|
|