← 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}`);