fix(release): make verifier RPC smoke resilient

Use OS-selected localhost ports with bounded bind retries and handle aria2c spawn failures without unhandled child-process errors.

Refs #3
This commit is contained in:
NimBold
2026-07-15 08:50:34 +03:30
parent 4a3fece22b
commit 6ff0047d6c
+111 -22
View File
@@ -2,6 +2,7 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import os from 'node:os'; import os from 'node:os';
import net from 'node:net';
import { execFileSync, spawn } from 'node:child_process'; import { execFileSync, spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@@ -365,6 +366,34 @@ async function terminateProcess(proc, label) {
return true; return true;
} }
function findAvailablePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.once('error', reject);
server.listen({ host: '127.0.0.1', port: 0 }, () => {
const address = server.address();
if (!address || typeof address === 'string') {
server.close();
reject(new Error('Could not determine the verifier RPC port.'));
return;
}
const port = address.port;
server.close(error => {
if (error) {
reject(error);
} else {
resolve(port);
}
});
});
});
}
function isPortBindingFailure(message) {
return /address already in use|failed to bind|could not bind|listen failed/i.test(message);
}
const coldStartTimeout = isMacOS ? 120000 : 30000; const coldStartTimeout = isMacOS ? 120000 : 30000;
if (canExecuteTarget) { if (canExecuteTarget) {
runEngine('yt-dlp cold start', 'yt-dlp', ['--version'], coldStartTimeout); runEngine('yt-dlp cold start', 'yt-dlp', ['--version'], coldStartTimeout);
@@ -393,7 +422,25 @@ if (canExecuteTarget) {
return; return;
} }
const port = 16801 + (process.pid % 1000); const body = JSON.stringify({
jsonrpc: '2.0',
id: 'firelink-verify',
method: 'aria2.getVersion',
params: [],
});
let result = { ok: false, error: 'RPC test did not run.' };
let rpcStderr = '';
const maxPortAttempts = 3;
for (let portAttempt = 0; portAttempt < maxPortAttempts; portAttempt += 1) {
let port;
try {
port = await findAvailablePort();
} catch (error) {
result = { ok: false, error: `Could not reserve an RPC port: ${error.message}` };
break;
}
const proc = spawn(p, [ const proc = spawn(p, [
'--enable-rpc', '--enable-rpc',
`--rpc-listen-port=${port}`, `--rpc-listen-port=${port}`,
@@ -403,51 +450,93 @@ if (canExecuteTarget) {
'--rpc-listen-all=false', '--rpc-listen-all=false',
], { ], {
env: engineEnv('aria2c'), env: engineEnv('aria2c'),
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'ignore', 'pipe'],
timeout: 15000, timeout: 15000,
}); });
let rpcStderr = ''; let spawnError = null;
proc.stderr.on('data', (d) => { let childExit = null;
rpcStderr += d.toString(); let attemptStderr = '';
proc.once('error', error => {
spawnError = error;
});
proc.once('exit', (code, signal) => {
childExit = { code, signal };
});
proc.stderr.on('data', data => {
attemptStderr += data.toString();
}); });
const body = JSON.stringify({ const attemptResult = await new Promise(resolve => {
jsonrpc: '2.0',
id: 'firelink-verify',
method: 'aria2.getVersion',
params: [],
});
const result = await new Promise((resolve) => {
const maxAttempts = 20; const maxAttempts = 20;
let attempts = 0; let attempts = 0;
function resolveProcessFailure() {
if (spawnError) {
resolve({ ok: false, error: `aria2c failed to spawn: ${spawnError.message}` });
} else if (childExit) {
resolve({
ok: false,
error: `aria2c exited before RPC became ready with code ${childExit.code} signal ${childExit.signal}.`,
});
}
}
function tryFetch() { function tryFetch() {
attempts++; if (spawnError || childExit) {
resolveProcessFailure();
return;
}
attempts += 1;
fetch(`http://127.0.0.1:${port}/jsonrpc`, { fetch(`http://127.0.0.1:${port}/jsonrpc`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body, body,
}) })
.then(async (res) => { .then(async response => {
resolve({ ok: true, data: await res.text() }); resolve({ ok: true, data: await response.text() });
}) })
.catch(() => { .catch(() => {
if (attempts >= maxAttempts) { if (spawnError || childExit) {
resolveProcessFailure();
} else if (attempts >= maxAttempts) {
resolve({ ok: false, error: `RPC not ready after ${maxAttempts} attempts` }); resolve({ ok: false, error: `RPC not ready after ${maxAttempts} attempts` });
return; } else {
}
setTimeout(tryFetch, 300); setTimeout(tryFetch, 300);
}
}); });
} }
tryFetch(); tryFetch();
}); });
// Clean up and wait before the verifier exits so a stuck daemon cannot be const exitedBeforeCleanup = childExit;
// left behind on a runner and contaminate later package or smoke checks. const terminated = proc.pid
await terminateProcess(proc, 'aria2 RPC verifier process'); ? await terminateProcess(proc, 'aria2 RPC verifier process')
: true;
rpcStderr = attemptStderr;
result = attemptResult;
if (
result.ok
&& exitedBeforeCleanup
&& (exitedBeforeCleanup.code !== 0 || exitedBeforeCleanup.signal !== null)
) {
result = {
ok: false,
error: `aria2c exited after responding with code ${exitedBeforeCleanup.code} signal ${exitedBeforeCleanup.signal}.`,
};
}
if (!terminated || result.ok || !isPortBindingFailure(`${result.error || ''}\n${rpcStderr}`)) {
break;
}
if (portAttempt + 1 < maxPortAttempts) {
console.log(`[INFO] RPC port ${port} became unavailable; retrying with a new port.`);
}
}
if (result.ok) { if (result.ok) {
try { try {