← back to Dw Fleet Registry
probe.mjs
80 lines
#!/usr/bin/env node
// Live-status probe for the DW fleet registry.
// Reads dw-fleet-registry.json, fetches every domain (https first, http fallback),
// classifies the result, writes a `live` block into each site row + a probeSummary,
// and saves the registry back. Read-only against the public web (GET requests).
//
// node probe.mjs # probe all domains, default 8s timeout, 16 concurrent
// node probe.mjs --timeout 12000 --concurrency 24
import fs from 'node:fs';
import path from 'node:path';
const REG = path.join(path.dirname(new URL(import.meta.url).pathname), 'dw-fleet-registry.json');
const args = process.argv.slice(2);
const argVal = (f, d) => { const i = args.indexOf(f); return i >= 0 && args[i+1] ? args[i+1] : d; };
const TIMEOUT = parseInt(argVal('--timeout', '8000'), 10);
const CONCURRENCY = parseInt(argVal('--concurrency', '16'), 10);
const PARKING_HOSTS = /(afternic|sedo|dan\.com|bodis|parkingcrew|godaddy\.com\/forsale|undeveloped|hugedomains|namecheap\.com\/parking|cashparking)/i;
const registry = JSON.parse(fs.readFileSync(REG, 'utf8'));
async function fetchOnce(url) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), TIMEOUT);
try {
const res = await fetch(url, { redirect: 'follow', signal: ctrl.signal, headers: { 'User-Agent': 'dw-fleet-registry-probe/1.0' } });
return { ok: true, status: res.status, finalUrl: res.url, server: res.headers.get('server') || null };
} catch (e) {
return { ok: false, error: e.name === 'AbortError' ? 'timeout' : (e.cause?.code || e.message) };
} finally { clearTimeout(t); }
}
function classify(r, domain) {
if (!r.ok) return { state: r.error === 'timeout' ? 'timeout' : 'unreachable', detail: r.error };
const finalHost = (() => { try { return new URL(r.finalUrl).host.replace(/^www\./, ''); } catch { return ''; } })();
if (PARKING_HOSTS.test(r.finalUrl)) return { state: 'parked', detail: 'redirects to ' + finalHost, status: r.status };
const offDomain = finalHost && !finalHost.endsWith(domain.replace(/^www\./, ''));
if (r.status >= 200 && r.status < 400) {
return { state: 'live', status: r.status, finalUrl: r.finalUrl, server: r.server, redirectedOffDomain: offDomain ? finalHost : null };
}
return { state: 'error', status: r.status, finalUrl: r.finalUrl };
}
async function probe(domain) {
let r = await fetchOnce('https://' + domain + '/');
let scheme = 'https';
if (!r.ok) { const h = await fetchOnce('http://' + domain + '/'); if (h.ok) { r = h; scheme = 'http'; } }
return { ...classify(r, domain), scheme, checkedAt: new Date().toISOString() };
}
// concurrency-limited pool
async function pool(items, n, worker) {
const out = new Array(items.length);
let i = 0;
await Promise.all(Array.from({ length: Math.min(n, items.length) }, async () => {
while (i < items.length) { const idx = i++; out[idx] = await worker(items[idx], idx); }
}));
return out;
}
const sites = registry.sites;
console.error(`Probing ${sites.length} domains (timeout ${TIMEOUT}ms, ${CONCURRENCY} concurrent)…`);
let done = 0;
const results = await pool(sites, CONCURRENCY, async (s) => {
const live = await probe(s.domain);
s.live = live;
done++;
if (done % 20 === 0) console.error(` …${done}/${sites.length}`);
return live.state;
});
const tally = results.reduce((m, s) => (m[s] = (m[s] || 0) + 1, m), {});
registry.probeSummary = { probedAt: new Date().toISOString(), timeoutMs: TIMEOUT, tally };
fs.writeFileSync(REG, JSON.stringify(registry, null, 2) + '\n');
console.log('\nLive-status tally:');
for (const [state, n] of Object.entries(tally).sort((a, b) => b[1] - a[1])) console.log(` ${state.padEnd(12)} ${n}`);
console.log(`\nWrote live status into ${REG}`);