This repository was archived by the owner on Nov 10, 2026. It is now read-only.
Repository navigation
Expand file tree
/
Copy pathstatus.mjs
More file actions
73 lines (72 loc) · 4.25 KB
/
Copy pathstatus.mjs
File metadata and controls
73 lines (72 loc) · 4.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import {mkdirSync, readFileSync, writeFileSync, renameSync, rmSync, statSync} from 'node:fs';
import {resolve} from 'node:path';
import {randomUUID} from 'node:crypto';
const alive = pid => { try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } };
export function readStatus(app, now = Date.now()) {
let rows; try { rows = JSON.parse(readFileSync(resolve(app, '.exact/status.json'), 'utf8')); } catch { return []; }
return rows.map(row => row.state === 'running' ? {...row, elapsedMs:now - row.startedAt, state:alive(row.pid) ? 'running' : 'unknown'} : row);
}
function update(app, row) {
const dir = resolve(app, '.exact'), lock = resolve(dir, 'status.lock');
mkdirSync(dir, {recursive:true});
const deadline = Date.now() + 2000;
for (;;) {
try { mkdirSync(lock); writeFileSync(resolve(lock,'owner'), String(process.pid)); break; } catch (e) {
if (e.code !== 'EEXIST') throw e;
try {
const inode = statSync(lock).ino;
let owner; try { owner = Number(readFileSync(resolve(lock,'owner'),'utf8')); } catch {}
if (owner && !alive(owner)) {
// Keep the nonempty tombstone: a second observer of this dead owner
// cannot rename a successor over it (rename refuses nonempty targets).
try { renameSync(lock, `${lock}.dead-${owner}-${inode}`); }
catch (error) { if (!['ENOENT','EEXIST','ENOTEMPTY'].includes(error.code)) throw error; }
continue;
}
} catch (error) { if (error.code === 'ENOENT') continue; throw error; }
if (Date.now() >= deadline) throw e;
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
}
}
try {
const rows = readStatus(app).filter(r => r.invocation !== row.invocation);
rows.push(row);
const temp = resolve(dir, `status.${process.pid}.tmp`);
writeFileSync(temp, JSON.stringify(rows, null, 2) + '\n');
renameSync(temp, resolve(dir, 'status.json'));
} finally {
if (readFileSync(resolve(lock,'owner'),'utf8') === String(process.pid)) rmSync(lock, {recursive:true});
}
}
export function startStatus(app, verb, extra = {}) {
const previous = readStatus(app).filter(r => r.verb === verb && r.lastDurationMs != null).at(-1);
let row = {verb, invocation:randomUUID(), pid:process.pid, phase:'starting', startedAt:Date.now(), elapsedMs:0, lastDurationMs:previous?.lastDurationMs ?? null, state:'running', exitCode:null, ...extra};
const set = patch => { row = {...row, elapsedMs:Date.now() - row.startedAt, ...patch}; update(app, row); return row; };
set({});
process.once('exit', code => set({state:code ? 'failed' : 'exited', exitCode:code, ...(verb === 'prove' ? {lastDurationMs:Date.now() - row.startedAt} : {})}));
return {set, get:() => ({...row, elapsedMs:Date.now() - row.startedAt})};
}
export function formatStatus(rows) {
return rows.map(r => `${r.verb} ${r.invocation} pid=${r.pid} ${r.state} ${r.phase} elapsed=${r.elapsedMs}ms last=${r.lastDurationMs ?? '?'}ms exit=${r.exitCode ?? '-'}${r.url ? ` ${r.url}` : ''}`).join('\n') || 'No web or prove invocations recorded.';
}
export async function waitReady(app, {timeout = 600, invocation} = {}) {
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error('--timeout needs positive seconds');
const deadline = Date.now() + timeout * 1000;
while (Date.now() < deadline) {
const row = readStatus(app).filter(r => r.verb === 'web' && (invocation ? r.invocation === invocation : r.state === 'running')).at(-1);
if (row) {
invocation ??= row.invocation;
if (row.state !== 'running') throw new Error(`web ${invocation}: ${row.state} (${row.phase}); run web to start a server`);
if (row.url) {
try {
const response = await fetch(new URL('__exact/ready', row.url), {signal:AbortSignal.timeout(1000)});
const status = await response.json();
if (response.ok && status.invocation === invocation && status.phase === 'ready') return status;
if (status.phase === 'failed') throw new Error('web build failed; inspect status and compiler output');
} catch (e) { if (e.message.startsWith('web build failed')) throw e; }
}
}
await new Promise(ok => setTimeout(ok, 50));
}
throw new Error(`web --ready timed out after ${timeout}s; run status for the current phase`);
}