mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fix(cli): shut down preview embedded-mode server on Ctrl+C (#483)
* fix(cli): shut down preview embedded-mode server on Ctrl+C
runEmbeddedMode awaited a promise that never resolved, relying on Node
to exit on SIGINT. Two things kept that from working in practice:
1. @hono/node-server's listening handle keeps the event loop alive after
the signal fires, so the process hangs even when SIGINT does arrive.
2. On Windows, some terminals (Git Bash / MSYS) don't deliver Ctrl+C to
the Node process as a SIGINT at all — the keystroke is eaten at the
TTY layer.
Register a SIGINT/SIGTERM handler that closes the server explicitly and
resolves the promise, with a 2s force-exit fallback. On Windows, run a
readline interface on stdin to catch Ctrl+C at the TTY and re-emit it
as SIGINT so the same handler fires.
Print "Shutting down studio..." as soon as the signal is received —
server.close() can take a second or two to drain keep-alive connections
and an unmarked pause reads as "stuck".
Exit code 0 because a user-initiated Ctrl+C isn't an error; non-zero
codes make pnpm print ELIFECYCLE right where the user just asked the
process to stop.
* fix(cli): close readline interface in shutdown to honour exit-code intent
After the SIGINT handler removes itself, the Windows readline interface
is still alive and listening. A second Ctrl+C during the 2s grace
period would re-emit SIGINT with no registered handler, triggering
Node's default exit-130 behaviour and contradicting the explicit
exit(0) we chose for clean teardown.
Hoist the readline handle out of the win32 branch so shutdown can
close it before invoking server.close(). Also pass the signal name
to process.emit("SIGINT", "SIGINT") to match Node's ProcessEvents
overload — runtime behaviour is unchanged.
This commit is contained in:
@@ -387,7 +387,47 @@ async function runEmbeddedMode(
|
||||
console.log();
|
||||
import("open").then((mod) => mod.default(`${url}#project/${pName}`)).catch(() => {});
|
||||
|
||||
// Block until the process is killed. Ctrl+C (SIGINT) uses Node's default
|
||||
// behavior — exit immediately. The OS reclaims the port and file handles.
|
||||
return new Promise<void>(() => {});
|
||||
// Block until Ctrl+C. Node would normally exit on SIGINT, but the listening
|
||||
// HTTP server keeps handles open, so the event loop stays alive after the
|
||||
// signal handler fires. Close the server explicitly and resolve the promise
|
||||
// so `run()` returns cleanly instead of requiring a second Ctrl+C (or,
|
||||
// worse, the user force-killing the terminal).
|
||||
//
|
||||
// Windows wrinkle: Ctrl+C in some terminals (Git Bash / MSYS) doesn't reach
|
||||
// Node as a SIGINT at all — the process just sits there. Run a readline
|
||||
// interface on stdin so the keystroke is observed at the TTY layer and
|
||||
// re-emit it as SIGINT. No-op on platforms where the signal already arrives.
|
||||
let rl: import("node:readline").Interface | undefined;
|
||||
if (process.platform === "win32") {
|
||||
const readline = await import("node:readline");
|
||||
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
rl.on("SIGINT", () => {
|
||||
process.emit("SIGINT", "SIGINT");
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise<void>((resolveRun) => {
|
||||
let shuttingDown = false;
|
||||
const shutdown = (): void => {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
process.off("SIGINT", shutdown);
|
||||
process.off("SIGTERM", shutdown);
|
||||
// Close the readline interface so a second Ctrl+C during the grace
|
||||
// period below doesn't re-emit SIGINT and trigger Node's default
|
||||
// exit-130 behaviour, contradicting our intent to exit cleanly.
|
||||
rl?.close();
|
||||
// `server.close()` can take a second or two to drain keep-alive
|
||||
// connections; surface progress so the terminal doesn't look frozen.
|
||||
console.log();
|
||||
console.log(` ${c.dim("Shutting down studio...")}`);
|
||||
result.server.close(() => resolveRun());
|
||||
// If close() hangs on an open connection, force exit after a short
|
||||
// grace period. Exit 0 because user-initiated Ctrl+C isn't an error
|
||||
// — a non-zero code makes pnpm / npm print ELIFECYCLE.
|
||||
setTimeout(() => process.exit(0), 2000).unref();
|
||||
};
|
||||
process.once("SIGINT", shutdown);
|
||||
process.once("SIGTERM", shutdown);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user