Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#### :nail_care: Polish

- Improve error message for trying to define a type inside a function. https://github.com/rescript-lang/rescript/pull/7843
- Refactor CLI to use spawn for better signal handling in watch mode. https://github.com/rescript-lang/rescript/pull/7844

#### :house: Internal

Expand Down
89 changes: 80 additions & 9 deletions cli/rescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,85 @@ import { rescript_exe } from "./common/bins.js";

const args = process.argv.slice(2);

try {
child_process.execFileSync(rescript_exe, args, {
stdio: "inherit",
});
} catch (err) {
if (err.status !== undefined) {
process.exit(err.status); // Pass through the exit code
// We intentionally use spawn (async) instead of execFileSync (sync) here.
// Rationale:
// - execFileSync blocks Node's event loop, so Ctrl+C (SIGINT) causes Node to
// exit immediately without giving us a chance to forward the signal to the
// child and wait for its cleanup. In watch mode, the Rust watcher prints
// "Exiting..." on SIGINT and performs cleanup; with execFileSync that output
// may appear after the shell prompt and sometimes requires an extra keypress.
// - spawn lets us install signal handlers, forward them to the child, and then
// exit the parent with the correct status only after the child has exited.
const child = child_process.spawn(rescript_exe, args, {
stdio: "inherit",
});

// Map POSIX signal names to conventional exit status numbers so we can
// reproduce the usual 128 + signal behavior when exiting due to a signal.
/** @type {Record<string, number>} */
const signalToNumber = { SIGINT: 2, SIGTERM: 15, SIGHUP: 1, SIGQUIT: 3 };

let forwardedSignal = false;
/**
* @param {NodeJS.Signals} signal
*/
const handleSignal = (signal) => {
// Intercept the signal in the parent, forward it to the child, and let the
// child perform its own cleanup. This ensures ordered shutdown in watch mode.
// Guard against double-forwarding since terminals or OSes can deliver
// multiple signals (e.g., repeated Ctrl+C).
// Prevent Node from exiting immediately; forward to child first
if (forwardedSignal) return;
forwardedSignal = true;
try {
if (child.exitCode === null && child.signalCode == null) {
child.kill(signal);
}
} catch {
// best effort
}
};

process.on("SIGINT", handleSignal);
process.on("SIGTERM", handleSignal);
process.on("SIGHUP", handleSignal);
process.on("SIGQUIT", handleSignal);

// Cross-platform note:
// - On Unix, Ctrl+C sends SIGINT to the process group; we also explicitly
// forward it to the child to be robust.
// - On Windows, Node maps kill('SIGINT'/'SIGTERM') to console control events;
// the Rust watcher (via the ctrlc crate) handles these and exits cleanly.

// Ensure no orphaned process if parent exits unexpectedly
process.on("exit", () => {
if (child.exitCode === null && child.signalCode == null) {
try {
child.kill("SIGTERM");
} catch {
// ignore
}
}
});

child.on("exit", (code, signal) => {
process.removeListener("SIGINT", handleSignal);
process.removeListener("SIGTERM", handleSignal);
process.removeListener("SIGHUP", handleSignal);
process.removeListener("SIGQUIT", handleSignal);

// If the child exited due to a signal, emulate the conventional exit status
// (128 + signalNumber). Otherwise, pass through the child's numeric exit code.
if (signal) {
const n = signalToNumber[signal];
process.exit(typeof n === "number" ? 128 + n : 1);
} else {
process.exit(1); // Generic error
process.exit(typeof code === "number" ? code : 0);
}
}
});

// Surface spawn errors (e.g., executable not found) and exit with failure.
child.on("error", (err) => {
console.error(err?.message ?? String(err));
process.exit(1);
});
Loading