142 lines
6 KiB
JavaScript
142 lines
6 KiB
JavaScript
/**
|
|
* Readiness detection: port probing, pattern matching, wait-for-ready.
|
|
*/
|
|
import { createConnection } from "node:net";
|
|
import { addEvent, pushAlert } from "./process-manager.js";
|
|
import { DEFAULT_READY_TIMEOUT, PORT_PROBE_TIMEOUT, READY_POLL_INTERVAL, } from "./types.js";
|
|
// ── Readiness Transition ───────────────────────────────────────────────────
|
|
export function transitionToReady(bg, detail) {
|
|
bg.status = "ready";
|
|
bg.wasReady = true;
|
|
addEvent(bg, { type: "ready", detail });
|
|
}
|
|
// ── Port Probing ───────────────────────────────────────────────────────────
|
|
export function probePort(port, host = "127.0.0.1") {
|
|
return new Promise((resolve) => {
|
|
const socket = createConnection({ port, host, timeout: PORT_PROBE_TIMEOUT }, () => {
|
|
socket.destroy();
|
|
resolve(true);
|
|
});
|
|
socket.on("error", () => {
|
|
socket.destroy();
|
|
resolve(false);
|
|
});
|
|
socket.on("timeout", () => {
|
|
socket.destroy();
|
|
resolve(false);
|
|
});
|
|
});
|
|
}
|
|
// ── Port Probing Loop ──────────────────────────────────────────────────────
|
|
export function startPortProbing(bg, port, customTimeout) {
|
|
const timeout = customTimeout || DEFAULT_READY_TIMEOUT;
|
|
const interval = setInterval(async () => {
|
|
if (!bg.alive) {
|
|
clearInterval(interval);
|
|
const stderrLines = bg.output
|
|
.filter((l) => l.stream === "stderr")
|
|
.slice(-10)
|
|
.map((l) => l.line);
|
|
const detail = `Process exited (code ${bg.exitCode}) before port ${port} opened${stderrLines.length > 0 ? ` — ${stderrLines.join("; ").slice(0, 200)}` : ""}`;
|
|
addEvent(bg, {
|
|
type: "port_timeout",
|
|
detail,
|
|
data: { port, exitCode: bg.exitCode },
|
|
});
|
|
return;
|
|
}
|
|
if (bg.status !== "starting") {
|
|
clearInterval(interval);
|
|
return;
|
|
}
|
|
const open = await probePort(port);
|
|
if (open) {
|
|
clearInterval(interval);
|
|
if (!bg.ports.includes(port))
|
|
bg.ports.push(port);
|
|
transitionToReady(bg, `Port ${port} is open`);
|
|
addEvent(bg, {
|
|
type: "port_open",
|
|
detail: `Port ${port} is open`,
|
|
data: { port },
|
|
});
|
|
}
|
|
}, READY_POLL_INTERVAL);
|
|
// Stop probing after timeout — transition to error state so the process
|
|
// doesn't stay in "starting" forever (fixes #428)
|
|
setTimeout(() => {
|
|
clearInterval(interval);
|
|
if (bg.alive && bg.status === "starting") {
|
|
const stderrLines = bg.output
|
|
.filter((l) => l.stream === "stderr")
|
|
.slice(-10)
|
|
.map((l) => l.line);
|
|
const detail = `Port ${port} not open after ${timeout}ms${stderrLines.length > 0 ? ` — ${stderrLines.join("; ").slice(0, 200)}` : ""}`;
|
|
bg.status = "error";
|
|
addEvent(bg, { type: "port_timeout", detail, data: { port, timeout } });
|
|
pushAlert(bg, `Port ${port} readiness timeout after ${timeout / 1000}s`);
|
|
}
|
|
}, timeout);
|
|
}
|
|
// ── Wait for Ready ─────────────────────────────────────────────────────────
|
|
export async function waitForReady(bg, timeout, signal) {
|
|
const start = Date.now();
|
|
while (Date.now() - start < timeout) {
|
|
if (signal?.aborted) {
|
|
return { ready: false, detail: "Cancelled" };
|
|
}
|
|
if (!bg.alive) {
|
|
const stderrLines = bg.output
|
|
.filter((l) => l.stream === "stderr")
|
|
.slice(-5)
|
|
.map((l) => l.line);
|
|
const stderrContext = stderrLines.length > 0
|
|
? `\nstderr:\n${stderrLines.join("\n").slice(0, 500)}`
|
|
: "";
|
|
return {
|
|
ready: false,
|
|
detail: `Process exited before becoming ready (code ${bg.exitCode})${bg.recentErrors.length > 0 ? ` — ${bg.recentErrors.slice(-1)[0]}` : ""}${stderrContext}`,
|
|
};
|
|
}
|
|
if (bg.status === "error") {
|
|
const stderrLines = bg.output
|
|
.filter((l) => l.stream === "stderr")
|
|
.slice(-5)
|
|
.map((l) => l.line);
|
|
const stderrContext = stderrLines.length > 0
|
|
? `\nstderr:\n${stderrLines.join("\n").slice(0, 500)}`
|
|
: "";
|
|
return {
|
|
ready: false,
|
|
detail: `Process entered error state${bg.readyPort ? ` (port ${bg.readyPort} never opened)` : ""}${stderrContext}`,
|
|
};
|
|
}
|
|
if (bg.status === "ready") {
|
|
return {
|
|
ready: true,
|
|
detail: bg.events.find((e) => e.type === "ready")?.detail ||
|
|
"Process is ready",
|
|
};
|
|
}
|
|
await new Promise((r) => setTimeout(r, READY_POLL_INTERVAL));
|
|
}
|
|
// Timeout — try port probe as last resort
|
|
if (bg.readyPort) {
|
|
const open = await probePort(bg.readyPort);
|
|
if (open) {
|
|
transitionToReady(bg, `Port ${bg.readyPort} is open (detected at timeout)`);
|
|
return { ready: true, detail: `Port ${bg.readyPort} is open` };
|
|
}
|
|
}
|
|
const stderrLines = bg.output
|
|
.filter((l) => l.stream === "stderr")
|
|
.slice(-5)
|
|
.map((l) => l.line);
|
|
const stderrContext = stderrLines.length > 0
|
|
? `\nstderr:\n${stderrLines.join("\n").slice(0, 500)}`
|
|
: "";
|
|
return {
|
|
ready: false,
|
|
detail: `Timed out after ${timeout}ms waiting for ready signal${stderrContext}`,
|
|
};
|
|
}
|