import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import * as repo from '../../db/repos/speedtest.js'; import { log } from '../../log.js'; const pexec = promisify(execFile); export const NAME = 'speedtest'; // Ookla CLI gives the full metric set (jitter, packet loss, server, ISP, // shareable result URL). Override the binary via SPEEDTEST_BIN if needed. const OOKLA_BIN = process.env.SPEEDTEST_BIN || 'ookla-speedtest'; async function ooklaRunner() { const { stdout } = await pexec(OOKLA_BIN, ['-f', 'json', '--accept-license', '--accept-gdpr'], { timeout: 120000 }); const j = JSON.parse(stdout); const mbps = bw => (Number(bw) || 0) * 8 / 1e6; // Ookla bandwidth is bytes/s return { down_mbps: mbps(j.download?.bandwidth), up_mbps: mbps(j.upload?.bandwidth), ping_ms: j.ping?.latency ?? null, jitter_ms: j.ping?.jitter ?? null, packet_loss: j.packetLoss ?? null, server_name: j.server ? [j.server.name, j.server.location].filter(Boolean).join(' ยท ') : null, server_id: j.server?.id != null ? String(j.server.id) : null, isp: j.isp ?? null, result_url: j.result?.url ?? null, down_bytes: j.download?.bytes ?? null, up_bytes: j.upload?.bytes ?? null, ok: true }; } let runner = ooklaRunner; export function _setRunner(fn) { runner = fn; } export async function handler(_job) { try { const r = await runner(); const saved = await repo.record(r); log.info({ down: r.down_mbps, up: r.up_mbps, ping: r.ping_ms }, 'speedtest recorded'); return saved; } catch (e) { await repo.record({ ok: false, error: String(e?.message || e).slice(0, 300) }); log.error({ err: e }, 'speedtest failed'); throw e; } }