@@ -56,6 +56,9 @@ interface IBundlerCompilation {
5656/* for specific bundling debugging separate from logger */
5757const debugLog = false ;
5858
59+ /** Grace period a bundler child gets to honour SIGINT before it is killed. */
60+ const BUNDLER_STOP_TIMEOUT_MS = 5000 ;
61+
5962export class BundlerCompilerService
6063 extends EventEmitter
6164 implements IBundlerCompilerService
@@ -382,7 +385,10 @@ export class BundlerCompilerService
382385 this . $logger . trace (
383386 `Unable to start ${ projectData . bundler } process in watch mode. Error is: ${ err } ` ,
384387 ) ;
385- delete this . bundlerProcesses [ platformData . platformNameLowerCase ] ;
388+ this . forgetBundlerProcess (
389+ platformData . platformNameLowerCase ,
390+ childProcess ,
391+ ) ;
386392 reject ( err ) ;
387393 } ) ;
388394
@@ -399,7 +405,10 @@ export class BundlerCompilerService
399405 `Executing ${ projectData . bundler } failed with exit code ${ exitCode } .` ,
400406 ) ;
401407 error . code = exitCode ;
402- delete this . bundlerProcesses [ platformData . platformNameLowerCase ] ;
408+ this . forgetBundlerProcess (
409+ platformData . platformNameLowerCase ,
410+ childProcess ,
411+ ) ;
403412 reject ( error ) ;
404413 } ) ;
405414 } catch ( err ) {
@@ -430,7 +439,10 @@ export class BundlerCompilerService
430439 this . $logger . trace (
431440 `Unable to start ${ projectData . bundler } process in non-watch mode. Error is: ${ err } ` ,
432441 ) ;
433- delete this . bundlerProcesses [ platformData . platformNameLowerCase ] ;
442+ this . forgetBundlerProcess (
443+ platformData . platformNameLowerCase ,
444+ childProcess ,
445+ ) ;
434446 reject ( err ) ;
435447 } ) ;
436448
@@ -441,7 +453,10 @@ export class BundlerCompilerService
441453 childProcess . pid . toString ( ) ,
442454 ) ;
443455
444- delete this . bundlerProcesses [ platformData . platformNameLowerCase ] ;
456+ this . forgetBundlerProcess (
457+ platformData . platformNameLowerCase ,
458+ childProcess ,
459+ ) ;
445460 const exitCode = typeof arg === "number" ? arg : arg && arg . code ;
446461 if ( exitCode === 0 ) {
447462 // Non-watch Vite builds spawn the child with stdio:"inherit"
@@ -748,7 +763,9 @@ export class BundlerCompilerService
748763 await this . $cleanupService . addKillProcess ( childProcess . pid . toString ( ) ) ;
749764
750765 childProcess . once ( "exit" , ( code : number ) => {
751- delete this . viteServeProcesses [ key ] ;
766+ if ( this . viteServeProcesses [ key ] === childProcess ) {
767+ delete this . viteServeProcesses [ key ] ;
768+ }
752769 if ( code ) {
753770 this . $logger . warn (
754771 `Vite dev server for ${ key } exited with code ${ code } .` ,
@@ -1000,22 +1017,84 @@ export class BundlerCompilerService
10001017 this . $logger . trace (
10011018 `Stopping ${ this . getBundler ( ) } watch for platform ${ platform } .` ,
10021019 ) ;
1020+
10031021 const bundlerProcess = this . bundlerProcesses [ platform ] ;
1004- await this . $cleanupService . removeKillProcess ( bundlerProcess . pid . toString ( ) ) ;
10051022 if ( bundlerProcess ) {
1006- bundlerProcess . kill ( "SIGINT" ) ;
1007- delete this . bundlerProcesses [ platform ] ;
1023+ // A compilation already in flight can still reach us between the
1024+ // kill and the exit; nothing downstream may act on output from a
1025+ // watcher the caller has torn down.
1026+ bundlerProcess . removeAllListeners ( "message" ) ;
1027+ bundlerProcess . stdout ?. removeAllListeners ( "data" ) ;
1028+ bundlerProcess . stderr ?. removeAllListeners ( "data" ) ;
1029+
1030+ await this . terminate ( bundlerProcess ) ;
1031+ this . forgetBundlerProcess ( platform , bundlerProcess ) ;
10081032 }
10091033
10101034 // Tear down the Vite dev server we manage alongside the build watcher.
10111035 const viteServeProcess = this . viteServeProcesses [ platform ] ;
10121036 if ( viteServeProcess ) {
1013- await this . $cleanupService . removeKillProcess (
1014- viteServeProcess . pid . toString ( ) ,
1015- ) ;
1016- viteServeProcess . kill ( "SIGINT" ) ;
1017- delete this . viteServeProcesses [ platform ] ;
1037+ await this . terminate ( viteServeProcess ) ;
1038+ if ( this . viteServeProcesses [ platform ] === viteServeProcess ) {
1039+ delete this . viteServeProcesses [ platform ] ;
1040+ }
1041+ }
1042+ }
1043+
1044+ /**
1045+ * Drops a platform's entry only while it still points at `childProcess`, so
1046+ * an exit arriving after a restart cannot evict the replacement watcher.
1047+ */
1048+ private forgetBundlerProcess (
1049+ platform : string ,
1050+ childProcess : child_process . ChildProcess ,
1051+ ) : void {
1052+ if ( this . bundlerProcesses [ platform ] === childProcess ) {
1053+ delete this . bundlerProcesses [ platform ] ;
1054+ }
1055+ }
1056+
1057+ /**
1058+ * Resolves once the child is gone, so a caller that restarts the bundler
1059+ * cannot spawn a replacement while the old one still holds the watch.
1060+ */
1061+ private async terminate (
1062+ childProcess : child_process . ChildProcess ,
1063+ timeoutMs : number = BUNDLER_STOP_TIMEOUT_MS ,
1064+ ) : Promise < void > {
1065+ await this . $cleanupService . removeKillProcess ( childProcess . pid . toString ( ) ) ;
1066+
1067+ childProcess . kill ( "SIGINT" ) ;
1068+ if ( await this . waitForExit ( childProcess , timeoutMs ) ) {
1069+ return ;
10181070 }
1071+
1072+ this . $logger . trace (
1073+ `Process ${ childProcess . pid } did not exit on SIGINT within ${ timeoutMs } ms; sending SIGKILL.` ,
1074+ ) ;
1075+ childProcess . kill ( "SIGKILL" ) ;
1076+ await this . waitForExit ( childProcess , timeoutMs ) ;
1077+ }
1078+
1079+ private waitForExit (
1080+ childProcess : child_process . ChildProcess ,
1081+ timeoutMs : number ,
1082+ ) : Promise < boolean > {
1083+ return new Promise < boolean > ( ( resolve ) => {
1084+ const settle = ( exited : boolean ) => {
1085+ clearTimeout ( timer ) ;
1086+ childProcess . removeListener ( "exit" , onExit ) ;
1087+ childProcess . removeListener ( "close" , onExit ) ;
1088+ resolve ( exited ) ;
1089+ } ;
1090+ const onExit = ( ) => settle ( true ) ;
1091+ const timer = setTimeout ( ( ) => settle ( false ) , timeoutMs ) ;
1092+ // A pending timer must not be what keeps the CLI alive.
1093+ timer . unref ?.( ) ;
1094+
1095+ childProcess . once ( "exit" , onExit ) ;
1096+ childProcess . once ( "close" , onExit ) ;
1097+ } ) ;
10191098 }
10201099
10211100 private handleHMRMessage (
0 commit comments