@@ -4,7 +4,7 @@ const EventEmitter = require('node:events')
44const os = require ( 'node:os' )
55const t = require ( 'tap' )
66const fsMiniPass = require ( 'fs-minipass' )
7- const { output, time } = require ( 'proc-log' )
7+ const { output, time, log } = require ( 'proc-log' )
88const errorMessage = require ( '../../../lib/utils/error-message.js' )
99const ExecCommand = require ( '../../../lib/commands/exec.js' )
1010const { load : loadMockNpm } = require ( '../../fixtures/mock-npm' )
@@ -707,3 +707,136 @@ t.test('do no fancy handling for shellouts', async t => {
707707 } )
708708 } )
709709} )
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