@@ -4,7 +4,7 @@ const EventEmitter = require('node:events')
4
4
const os = require ( 'node:os' )
5
5
const t = require ( 'tap' )
6
6
const fsMiniPass = require ( 'fs-minipass' )
7
- const { output, time } = require ( 'proc-log' )
7
+ const { output, time, log } = require ( 'proc-log' )
8
8
const errorMessage = require ( '../../../lib/utils/error-message.js' )
9
9
const ExecCommand = require ( '../../../lib/commands/exec.js' )
10
10
const { load : loadMockNpm } = require ( '../../fixtures/mock-npm' )
@@ -707,3 +707,136 @@ t.test('do no fancy handling for shellouts', async t => {
707
707
} )
708
708
} )
709
709
} )
710
+
711
+ t . test ( 'container scenarios that trigger exit handler bug' , async t => {
712
+ t . test ( 'process.exit() called before exit handler cleanup' , async ( t ) => {
713
+ // Simulates when npm process exits directly without going through proper cleanup
714
+
715
+ let exitHandlerNeverCalledLogged = false
716
+ let npmBugReportLogged = false
717
+
718
+ await mockExitHandler ( t , {
719
+ config : { loglevel : 'notice' } ,
720
+ } )
721
+
722
+ // Override log.error to capture the specific error messages
723
+ const originalLogError = log . error
724
+ log . error = ( prefix , msg ) => {
725
+ if ( msg === 'Exit handler never called!' ) {
726
+ exitHandlerNeverCalledLogged = true
727
+ }
728
+ if ( msg === 'This is an error with npm itself. Please report this error at:' ) {
729
+ npmBugReportLogged = true
730
+ }
731
+ return originalLogError ( prefix , msg )
732
+ }
733
+
734
+ t . teardown ( ( ) => {
735
+ log . error = originalLogError
736
+ } )
737
+
738
+ // This happens when containers are stopped/killed before npm can clean up properly
739
+ process . emit ( 'exit' , 1 )
740
+
741
+ // Verify the bug is detected and logged correctly
742
+ t . equal ( exitHandlerNeverCalledLogged , true , 'should log "Exit handler never called!" error' )
743
+ t . equal ( npmBugReportLogged , true , 'should log npm bug report message' )
744
+ } )
745
+
746
+ t . test ( 'SIGTERM signal is handled properly' , ( t ) => {
747
+ // This test verifies that our fix handles SIGTERM signals
748
+
749
+ const ExitHandler = tmock ( t , '{LIB}/cli/exit-handler.js' )
750
+ const exitHandler = new ExitHandler ( { process } )
751
+
752
+ const initialSigtermCount = process . listeners ( 'SIGTERM' ) . length
753
+ const initialSigintCount = process . listeners ( 'SIGINT' ) . length
754
+ const initialSighupCount = process . listeners ( 'SIGHUP' ) . length
755
+
756
+ // Register signal handlers
757
+ exitHandler . registerUncaughtHandlers ( )
758
+
759
+ const finalSigtermCount = process . listeners ( 'SIGTERM' ) . length
760
+ const finalSigintCount = process . listeners ( 'SIGINT' ) . length
761
+ const finalSighupCount = process . listeners ( 'SIGHUP' ) . length
762
+
763
+ // Verify the fix: signal handlers should be registered
764
+ t . ok ( finalSigtermCount > initialSigtermCount , 'SIGTERM handler should be registered' )
765
+ t . ok ( finalSigintCount > initialSigintCount , 'SIGINT handler should be registered' )
766
+ t . ok ( finalSighupCount > initialSighupCount , 'SIGHUP handler should be registered' )
767
+
768
+ // Clean up listeners to avoid affecting other tests
769
+ const sigtermListeners = process . listeners ( 'SIGTERM' )
770
+ const sigintListeners = process . listeners ( 'SIGINT' )
771
+ const sighupListeners = process . listeners ( 'SIGHUP' )
772
+
773
+ for ( const listener of sigtermListeners ) {
774
+ process . removeListener ( 'SIGTERM' , listener )
775
+ }
776
+ for ( const listener of sigintListeners ) {
777
+ process . removeListener ( 'SIGINT' , listener )
778
+ }
779
+ for ( const listener of sighupListeners ) {
780
+ process . removeListener ( 'SIGHUP' , listener )
781
+ }
782
+
783
+ t . end ( )
784
+ } )
785
+
786
+ t . test ( 'signal handler execution' , async ( t ) => {
787
+ const ExitHandler = tmock ( t , '{LIB}/cli/exit-handler.js' )
788
+ const exitHandler = new ExitHandler ( { process } )
789
+
790
+ // Register signal handlers
791
+ exitHandler . registerUncaughtHandlers ( )
792
+
793
+ process . emit ( 'SIGTERM' )
794
+ process . emit ( 'SIGINT' )
795
+ process . emit ( 'SIGHUP' )
796
+
797
+ // Clean up listeners
798
+ process . removeAllListeners ( 'SIGTERM' )
799
+ process . removeAllListeners ( 'SIGINT' )
800
+ process . removeAllListeners ( 'SIGHUP' )
801
+
802
+ t . pass ( 'signal handlers executed successfully' )
803
+ t . end ( )
804
+ } )
805
+
806
+ t . test ( 'hanging async operation interrupted by signal' , async ( t ) => {
807
+ // This test simulates the scenario where npm hangs on a long operation and receives SIGTERM/SIGKILL before it can complete
808
+
809
+ let exitHandlerNeverCalledLogged = false
810
+
811
+ const { exitHandler } = await mockExitHandler ( t , {
812
+ config : { loglevel : 'notice' } ,
813
+ } )
814
+
815
+ // Override log.error to detect the bug message
816
+ const originalLogError = log . error
817
+ log . error = ( prefix , msg ) => {
818
+ if ( msg === 'Exit handler never called!' ) {
819
+ exitHandlerNeverCalledLogged = true
820
+ }
821
+ return originalLogError ( prefix , msg )
822
+ }
823
+
824
+ t . teardown ( ( ) => {
825
+ log . error = originalLogError
826
+ } )
827
+
828
+ // Track if exit handler was called properly
829
+ let exitHandlerCalled = false
830
+ exitHandler . exit = ( ) => {
831
+ exitHandlerCalled = true
832
+ }
833
+
834
+ // Simulate sending signal to the process without proper cleanup
835
+ // This mimics what happens when a container is terminated
836
+ process . emit ( 'exit' , 1 )
837
+
838
+ // Verify the bug conditions
839
+ t . equal ( exitHandlerCalled , false , 'exit handler should not be called in this scenario' )
840
+ t . equal ( exitHandlerNeverCalledLogged , true , 'should detect and log the exit handler bug' )
841
+ } )
842
+ } )
0 commit comments