@@ -7,14 +7,85 @@ import { rescript_exe } from "./common/bins.js";
7
7
8
8
const args = process . argv . slice ( 2 ) ;
9
9
10
- try {
11
- child_process . execFileSync ( rescript_exe , args , {
12
- stdio : "inherit" ,
13
- } ) ;
14
- } catch ( err ) {
15
- if ( err . status !== undefined ) {
16
- process . exit ( err . status ) ; // Pass through the exit code
10
+ // We intentionally use spawn (async) instead of execFileSync (sync) here.
11
+ // Rationale:
12
+ // - execFileSync blocks Node's event loop, so Ctrl+C (SIGINT) causes Node to
13
+ // exit immediately without giving us a chance to forward the signal to the
14
+ // child and wait for its cleanup. In watch mode, the Rust watcher prints
15
+ // "Exiting..." on SIGINT and performs cleanup; with execFileSync that output
16
+ // may appear after the shell prompt and sometimes requires an extra keypress.
17
+ // - spawn lets us install signal handlers, forward them to the child, and then
18
+ // exit the parent with the correct status only after the child has exited.
19
+ const child = child_process . spawn ( rescript_exe , args , {
20
+ stdio : "inherit" ,
21
+ } ) ;
22
+
23
+ // Map POSIX signal names to conventional exit status numbers so we can
24
+ // reproduce the usual 128 + signal behavior when exiting due to a signal.
25
+ /** @type {Record<string, number> } */
26
+ const signalToNumber = { SIGINT : 2 , SIGTERM : 15 , SIGHUP : 1 , SIGQUIT : 3 } ;
27
+
28
+ let forwardedSignal = false ;
29
+ /**
30
+ * @param {NodeJS.Signals } signal
31
+ */
32
+ const handleSignal = ( signal ) => {
33
+ // Intercept the signal in the parent, forward it to the child, and let the
34
+ // child perform its own cleanup. This ensures ordered shutdown in watch mode.
35
+ // Guard against double-forwarding since terminals or OSes can deliver
36
+ // multiple signals (e.g., repeated Ctrl+C).
37
+ // Prevent Node from exiting immediately; forward to child first
38
+ if ( forwardedSignal ) return ;
39
+ forwardedSignal = true ;
40
+ try {
41
+ if ( child . exitCode === null && child . signalCode == null ) {
42
+ child . kill ( signal ) ;
43
+ }
44
+ } catch {
45
+ // best effort
46
+ }
47
+ } ;
48
+
49
+ process . on ( "SIGINT" , handleSignal ) ;
50
+ process . on ( "SIGTERM" , handleSignal ) ;
51
+ process . on ( "SIGHUP" , handleSignal ) ;
52
+ process . on ( "SIGQUIT" , handleSignal ) ;
53
+
54
+ // Cross-platform note:
55
+ // - On Unix, Ctrl+C sends SIGINT to the process group; we also explicitly
56
+ // forward it to the child to be robust.
57
+ // - On Windows, Node maps kill('SIGINT'/'SIGTERM') to console control events;
58
+ // the Rust watcher (via the ctrlc crate) handles these and exits cleanly.
59
+
60
+ // Ensure no orphaned process if parent exits unexpectedly
61
+ process . on ( "exit" , ( ) => {
62
+ if ( child . exitCode === null && child . signalCode == null ) {
63
+ try {
64
+ child . kill ( "SIGTERM" ) ;
65
+ } catch {
66
+ // ignore
67
+ }
68
+ }
69
+ } ) ;
70
+
71
+ child . on ( "exit" , ( code , signal ) => {
72
+ process . removeListener ( "SIGINT" , handleSignal ) ;
73
+ process . removeListener ( "SIGTERM" , handleSignal ) ;
74
+ process . removeListener ( "SIGHUP" , handleSignal ) ;
75
+ process . removeListener ( "SIGQUIT" , handleSignal ) ;
76
+
77
+ // If the child exited due to a signal, emulate the conventional exit status
78
+ // (128 + signalNumber). Otherwise, pass through the child's numeric exit code.
79
+ if ( signal ) {
80
+ const n = signalToNumber [ signal ] ;
81
+ process . exit ( typeof n === "number" ? 128 + n : 1 ) ;
17
82
} else {
18
- process . exit ( 1 ) ; // Generic error
83
+ process . exit ( typeof code === "number" ? code : 0 ) ;
19
84
}
20
- }
85
+ } ) ;
86
+
87
+ // Surface spawn errors (e.g., executable not found) and exit with failure.
88
+ child . on ( "error" , ( err ) => {
89
+ console . error ( err ?. message ?? String ( err ) ) ;
90
+ process . exit ( 1 ) ;
91
+ } ) ;
0 commit comments