1// The jevstrudel Worker under `wrangler dev`, supervised: shared by dev.mjs 2// (`--env dev`, with the MCP hub) and preview.mjs (production, with the 3// static build as its assets). 4// 5// It runs worker.mjs inside `op-env-run`, which supplies the Jev key, and 6// restarts it when it exits or stops answering HTTP. Both have happened 7// (2026-09-24): wrangler exited with an empty error, and after wrangler 8// restarted its own crashed runtime the Worker listened but answered 9// nothing. Either way the Jev relay was silently down. The empty-error exit 10// was wrangler 4.129.0 dying when a client dropped a request; the flake's 11// wrangler no longer does (worker/CLAUDE.md), and this stays for the rest. 12import { execFileSync, spawn } from 'node:child_process'; 13 14export const has = (tool) => { 15 try { 16 execFileSync('sh', ['-c', `command -v ${tool}`], { stdio: 'ignore' }); 17 return true; 18 } catch { 19 return false; 20 } 21}; 22 23const GRACE_MS = 60_000; // wrangler builds and boots before the first check 24const CHECK_MS = 5_000; 25const MISSES_ALLOWED = 3; 26const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); 27 28async function answers(port) { 29 try { 30 await fetch(`http://localhost:${port}/`, { signal: AbortSignal.timeout(3000) }); 31 return true; // any HTTP answer, 404 included 32 } catch { 33 return false; 34 } 35} 36 37// `args` go to `wrangler dev` after `--config worker/wrangler.json`; `port` 38// is where the Worker answers. Returns { stop }. 39export function superviseWorker({ port, args, name = 'worker' }) { 40 const keyed = has('op-env-run'); 41 if (!keyed) console.error(`${name}: no op-env-run; the Worker runs without a Jev key and the relay will refuse`); 42 let worker; 43 let stopping = false; 44 45 // worker.mjs inside op-env-run, in its own process group so a hung Worker 46 // is stopped whole (op, wrangler, and the workerd runtime under it). 47 // --pdeathsig stops it if this process is killed outright. 48 function stopWorker() { 49 if (!worker || worker.exitCode !== null) return; 50 try { 51 process.kill(-worker.pid, 'SIGTERM'); 52 } catch {} 53 const group = worker.pid; 54 setTimeout(() => { 55 try { 56 process.kill(-group, 'SIGKILL'); 57 } catch {} 58 }, 3000).unref(); 59 } 60 61 (async () => { 62 while (!stopping) { 63 const cmd = ['setpriv', '--pdeathsig', 'TERM', '--']; 64 if (keyed) cmd.push('op-env-run', '--'); 65 cmd.push(process.execPath, 'tools/dev/worker.mjs', ...args); 66 worker = spawn(cmd[0], cmd.slice(1), { stdio: 'inherit', detached: true }); 67 const exited = new Promise((r) => worker.once('exit', r)); 68 69 const start = Date.now(); 70 while (worker.exitCode === null && Date.now() - start < GRACE_MS && !(await answers(port))) await sleep(2000); 71 let misses = 0; 72 while (worker.exitCode === null && !stopping) { 73 misses = (await answers(port)) ? 0 : misses + 1; 74 if (misses >= MISSES_ALLOWED) { 75 console.error(`${name}: the Worker stopped answering on :${port}; restarting it`); 76 stopWorker(); 77 break; 78 } 79 await sleep(CHECK_MS); 80 } 81 await exited; 82 if (!stopping) { 83 console.error(`${name}: the Worker stopped; starting it again in 2 s`); 84 await sleep(2000); 85 } 86 } 87 })(); 88 89 return { 90 stop() { 91 stopping = true; 92 stopWorker(); 93 }, 94 }; 95}