@@ -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
+
})
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4