jevstrudel.git / tools / dev / supervise.mjs

The jevstrudel Worker under wrangler dev, supervised: shared by dev.mjs (--env dev, with the MCP hub) and preview.mjs (production, with the static build as its assets).

It runs worker.mjs inside op-env-run, which supplies the Jev key, and restarts it when it exits or stops answering HTTP. Both have happened (2026-09-24): wrangler exited with an empty error, and after wrangler restarted its own crashed runtime the Worker listened but answered nothing. Either way the Jev relay was silently down. The empty-error exit was wrangler 4.129.0 dying when a client dropped a request; the flake's wrangler no longer does (worker/CLAUDE.md), and this stays for the rest.

12import { execFileSync, spawn } from 'node:child_process';
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}

args go to wrangler dev after --config worker/wrangler.json; port 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;

worker.mjs inside op-env-run, in its own process group so a hung Worker is stopped whole (op, wrangler, and the workerd runtime under it). --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  }
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}