@@ -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