A gate's tree server dies with its gate: serve-strict --exit-with-parent, passed by every spawning gate
On 2026-09-29, 41 serve-strict processes had outlived the gates that started them: 279 MB resident, the oldest 5.3 days, spread across eight worktrees. A gate that crashes, is killed, or is refused by the browser-slot guard's process.exit cannot run its own srv.kill(), and _serve-reaped's handlers cover signals and normal exit but not a SIGKILLed parent. serve-strict takes --exit-with-parent: it records the pid that started it and exits within 2 s once that process is gone. Opt-in, so a server someone deliberately left running is never taken down. Every internal/ gate and tool that spawns it now passes the flag (14 files, plus _serve-reaped, the shared helper). Proven: a parent that spawns the server with the flag and exits abruptly (as the guard does) leaves it alive for a moment and gone within 3 s. The orphans already running belong to other worktrees' runs and are left to their owners; plugin-local scripts that spawn serve-strict should pass the flag too.
This commit is contained in:
parent
b9854dd072
commit
7c99d1a06c
16 changed files with 24 additions and 16 deletions
|
|
@ -34,7 +34,7 @@ const HERE = dirname(fileURLToPath(import.meta.url))
|
|||
export function serveReaped(directory, port) {
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[resolve(HERE, 'serve-strict.mjs'), '--port', String(port), '--directory', directory],
|
||||
[resolve(HERE, 'serve-strict.mjs'), '--port', String(port), '--directory', directory, '--exit-with-parent'],
|
||||
{ stdio: 'ignore' },
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const FAKE_TOKEN = ['pk', b64({ u: 'auto-engine-check', a: 'not-a-real-token' })
|
|||
if (!existsSync(join(DIR, TREE_NAME, LEAF, 'index.html'))) { console.log(`AUTO-ENGINE: NOT MEASURED — no ${LEAF} under ${DIR}/${TREE_NAME}/`); process.exit(75) }
|
||||
|
||||
const port = await new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const n = s.address().port; s.close(() => ok(n)) }) })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR, '--exit-with-parent'], { stdio: 'ignore' })
|
||||
const stop = () => { try { srv.kill() } catch { /* gone */ } }
|
||||
let up = false
|
||||
for (let i = 0; i < 40 && !up; i++) { await new Promise((r) => setTimeout(r, 150)); up = await fetch(`http://127.0.0.1:${port}/${TREE_NAME}/manifest.json`).then((r) => r.ok).catch(() => false) }
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const LEAF = arg('--leaf', 'doxa-simple-map/doxa-simple-map')
|
|||
const PAIRS = { Germany: 'Германия', Ukraine: 'Украина', Turkey: 'Турция', Egypt: 'Египет', Kazakhstan: 'Казахстан', Poland: 'Польша' }
|
||||
|
||||
const port = await new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR, '--exit-with-parent'], { stdio: 'ignore' })
|
||||
await new Promise((r) => setTimeout(r, 900))
|
||||
const up = await fetch(`http://127.0.0.1:${port}/${TREE_NAME}/${LEAF}/index.html`).then((r) => r.ok).catch(() => false)
|
||||
if (!up) { srv.kill(); console.log(`BASEMAP-LABELS: NOT MEASURED — could not serve ${DIR}`); process.exit(2) }
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ const HOST_PORT = await freePort()
|
|||
const TREE = `http://127.0.0.1:${TREE_PORT}/${TREE_NAME}`
|
||||
|
||||
let pageHtml = '<!doctype html><html><body>nothing yet</body></html>'
|
||||
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore' })
|
||||
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT), '--exit-with-parent'], { cwd: ROOT, stdio: 'ignore' })
|
||||
const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html' }); r.end(pageHtml) }).listen(HOST_PORT, 'localhost')
|
||||
const cleanup = () => { try { tree.kill() } catch { /* already gone */ } try { host.close() } catch { /* already closed */ } }
|
||||
await new Promise((r) => setTimeout(r, 900))
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ for (const [, p] of presets) for (const pg of p.pages) {
|
|||
|
||||
// ── our side: this checkout's tree on an OS-assigned port ─────────────────────────────────────────
|
||||
const port = await new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const n = s.address().port; s.close(() => ok(n)) }) })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR, '--exit-with-parent'], { stdio: 'ignore' })
|
||||
const stopSrv = () => { try { srv.kill() } catch { /* already gone */ } }
|
||||
let up = false
|
||||
for (let i = 0; i < 40 && !up; i++) { await new Promise((r) => setTimeout(r, 150)); up = await fetch(`http://127.0.0.1:${port}/${TREE_NAME}/manifest.json`).then((r) => r.ok).catch(() => false) }
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ const SEED = {
|
|||
|
||||
// ── our own server, on a port nobody else can be holding ──────────────────────
|
||||
const port = await new Promise(ok => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', 'public'], { cwd: ROOT, stdio: 'ignore' })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', 'public', '--exit-with-parent'], { cwd: ROOT, stdio: 'ignore' })
|
||||
const reap = () => { try { srv.kill() } catch (_) {} }
|
||||
process.on('exit', reap); for (const s of ['SIGINT', 'SIGTERM']) process.on(s, () => { reap(); process.exit(130) })
|
||||
const BASE = `http://127.0.0.1:${port}/${TREE_NAME}`
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ for (const b of mf.bundles || []) {
|
|||
}
|
||||
|
||||
// ── 2. SERVE THE BUILT TREE the way production serves it (directories 404) ─────────────────────
|
||||
const server = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(PORT), '--directory', 'public'], {
|
||||
const server = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(PORT), '--directory', 'public', '--exit-with-parent'], {
|
||||
cwd: ROOT, stdio: 'pipe',
|
||||
})
|
||||
let serverErr = ''
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ for (const b of manifest.bundles || []) for (const p of b.profiles || []) {
|
|||
if (!leaves.includes(CONTROL)) { console.log(`HOST-LOCALE: NOT MEASURED — the control ${CONTROL} is not in this tree`); process.exit(75) }
|
||||
|
||||
const port = await new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR, '--exit-with-parent'], { stdio: 'ignore' })
|
||||
const stop = () => { try { srv.kill() } catch { /* already gone */ } }
|
||||
let up = false
|
||||
for (let i = 0; i < 40 && !up; i++) { await new Promise((r) => setTimeout(r, 150)); up = await fetch(`http://127.0.0.1:${port}/${TREE_NAME}/manifest.json`).then((r) => r.ok).catch(() => false) }
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ if (!leaves.length) { console.log('LEAF-ENGINE: NOT MEASURED — no leaves match
|
|||
|
||||
async function serve(dir) {
|
||||
const port = await new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const n = s.address().port; s.close(() => ok(n)) }) })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', dir], { stdio: 'ignore' })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', dir, '--exit-with-parent'], { stdio: 'ignore' })
|
||||
let up = false
|
||||
for (let i = 0; i < 40 && !up; i++) { await new Promise((r) => setTimeout(r, 150)); up = await fetch(`http://127.0.0.1:${port}/${TREE_NAME}/manifest.json`).then((r) => r.ok).catch(() => false) }
|
||||
if (!up) { try { srv.kill() } catch { /* gone */ } return null }
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const TREE_PORT = await freePort()
|
|||
const HOST_PORT = await freePort()
|
||||
const TREE = `http://127.0.0.1:${TREE_PORT}/${TREE_NAME}`
|
||||
let pageHtml = '<!doctype html><title>host</title><body>nothing</body>'
|
||||
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore' })
|
||||
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT), '--exit-with-parent'], { cwd: ROOT, stdio: 'ignore' })
|
||||
const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); r.end(pageHtml) }).listen(HOST_PORT, 'localhost')
|
||||
const cleanup = () => { try { tree.kill() } catch (_) {} try { host.close() } catch (_) {} }
|
||||
await new Promise((r) => setTimeout(r, 900))
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ if (!leaves.length) { console.log('MAPBOX-DREW: NOT MEASURED — no composed map
|
|||
|
||||
async function serve(dir) {
|
||||
const port = await new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const n = s.address().port; s.close(() => ok(n)) }) })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', dir], { stdio: 'ignore' })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', dir, '--exit-with-parent'], { stdio: 'ignore' })
|
||||
await new Promise((r) => setTimeout(r, 600))
|
||||
return { port, stop: () => { try { srv.kill() } catch (_) {} } }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ try { ({ chromium } = await import('./lib/playwright-guarded.mjs')) } catch { co
|
|||
const OUT = process.env.OUT || '/tmp'
|
||||
const free = () => new Promise(ok => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
|
||||
const TP = await free(), HP = await free(), TREE = `http://127.0.0.1:${TP}/${TREE_NAME}`
|
||||
const srv = spawn('node', ['internal/serve-strict.mjs', '--port', String(TP)], { cwd: ROOT, stdio: 'ignore' })
|
||||
const srv = spawn('node', ['internal/serve-strict.mjs', '--port', String(TP), '--exit-with-parent'], { cwd: ROOT, stdio: 'ignore' })
|
||||
const host = createServer((q, r) => { const l = new URL(q.url, 'http://x').searchParams.get('l') || 'en'; r.writeHead(200, { 'content-type': 'text/html' }); r.end(`<!doctype html><html lang="${l}"><body style="margin:0"><script src="${TREE}/doxa-maps-spa/doxa-maps-spa.js"></script><doxa-maps-spa tab="prayer-walk-planner" style="height:100vh"></doxa-maps-spa></body></html>`) }).listen(HP, 'localhost')
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ const hostPage = () => {
|
|||
<style>body{margin:0} header,footer{height:60px;background:#eee}</style></head>
|
||||
<body><header id=hdr>HOST HEADER</header><main style="max-width:1200px;margin:0 auto">${embed}</main><footer>HOST FOOTER</footer></body></html>`
|
||||
}
|
||||
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT)], { cwd: ROOT, stdio: 'ignore', env: { ...process.env, LIVE_CORS: LIVE_CORS ? '1' : '' } })
|
||||
const tree = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(TREE_PORT), '--exit-with-parent'], { cwd: ROOT, stdio: 'ignore', env: { ...process.env, LIVE_CORS: LIVE_CORS ? '1' : '' } })
|
||||
const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html' }); r.end(hostPage()) }).listen(HOST_PORT, 'localhost')
|
||||
const cleanup = () => { try { tree.kill() } catch {} try { host.close() } catch {} }
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const ONLY = arg('--only', '') ? arg('--only', '').split(',') : DEFAULT
|
|||
const manifest = JSON.parse(readFileSync(`public/${TREE_NAME}/manifest.json`, 'utf8'))
|
||||
|
||||
const port = await new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
|
||||
const srv = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(port)], { stdio: 'ignore' })
|
||||
const srv = spawn(process.execPath, ['internal/serve-strict.mjs', '--port', String(port), '--exit-with-parent'], { stdio: 'ignore' })
|
||||
const hostPort = await new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
|
||||
let hostHtml = ''
|
||||
const host = createServer((_, r) => { r.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); r.end(hostHtml) }).listen(hostPort, 'localhost')
|
||||
|
|
|
|||
|
|
@ -33,7 +33,11 @@
|
|||
* HTTP 200 on the box's PUBLIC IP while a browser gate was running. Override with HOST only if you
|
||||
* deliberately want another host to reach it.
|
||||
*
|
||||
* Usage: node internal/serve-strict.mjs [--port 4173] [--directory public]
|
||||
* Usage: node internal/serve-strict.mjs [--port 4173] [--directory public] [--exit-with-parent]
|
||||
* --exit-with-parent for a gate that spawns this server: exit once the process that started it is gone.
|
||||
* A gate that crashes or is refused by the browser-slot guard cannot kill its child, and
|
||||
* on 2026-09-29 41 such servers had outlived their gates (279 MB, the oldest 5 days).
|
||||
* Opt-in, so a server someone deliberately left running is never taken down.
|
||||
*/
|
||||
import { createServer } from 'node:http'
|
||||
import { createReadStream, statSync } from 'node:fs'
|
||||
|
|
@ -49,6 +53,10 @@ const argOf = (name, dflt) => { const i = argv.indexOf(name); return i >= 0 && a
|
|||
const PORT = Number(process.env.PORT || argOf('--port', '4173'))
|
||||
const HOST = process.env.HOST || '127.0.0.1'
|
||||
const DIR = resolve(ROOT, argOf('--directory', 'public'))
|
||||
if (argv.includes('--exit-with-parent')) {
|
||||
const parent = process.ppid
|
||||
setInterval(() => { if (process.ppid !== parent) process.exit(0) }, 2000).unref()
|
||||
}
|
||||
|
||||
const TYPES = {
|
||||
'.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8',
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ if (adopted !== true) {
|
|||
// Ephemeral port so two gates running at once cannot collide on 4173.
|
||||
const port = 5100 + Math.floor(Math.random() * 800)
|
||||
base = `http://127.0.0.1:${port}`
|
||||
server = spawn(process.execPath, [resolve(HERE, 'serve-strict.mjs'), '--port', String(port), '--directory', resolve(REPO, 'public')], { stdio: 'ignore', detached: false })
|
||||
server = spawn(process.execPath, [resolve(HERE, 'serve-strict.mjs'), '--port', String(port), '--directory', resolve(REPO, 'public'), '--exit-with-parent'], { stdio: 'ignore', detached: false })
|
||||
const until = Date.now() + 15000
|
||||
while (Date.now() < until && !(await alive(base))) await new Promise((r) => setTimeout(r, 250))
|
||||
if (!(await alive(base))) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue