diff --git a/cloudinit/signal_handler.py b/cloudinit/signal_handler.py index 4d6d1ab0a8e..af72d7b8378 100644 --- a/cloudinit/signal_handler.py +++ b/cloudinit/signal_handler.py @@ -7,6 +7,7 @@ # This file is part of cloud-init. See LICENSE file for license information. import contextlib import inspect +import threading import logging import signal import sys @@ -36,6 +37,7 @@ class ExitBehavior(NamedTuple): SIGNAL_EXIT_BEHAVIOR_CRASH: Final = ExitBehavior(1, logging.ERROR) SIGNAL_EXIT_BEHAVIOR_QUIET: Final = ExitBehavior(0, logging.INFO) _SIGNAL_EXIT_BEHAVIOR = SIGNAL_EXIT_BEHAVIOR_CRASH +_SUSPEND_WRITE_LOCK = threading.RLock() def inspect_handler(sig: Union[int, Callable, None]) -> None: @@ -99,8 +101,14 @@ def suspend_crash(): This allow signal handling without a crash where it is expected. The call stack is still printed if signal is received during this context, but the return code is 0 and no traceback is printed. + + Threadsafe. """ global _SIGNAL_EXIT_BEHAVIOR - _SIGNAL_EXIT_BEHAVIOR = SIGNAL_EXIT_BEHAVIOR_QUIET - yield - _SIGNAL_EXIT_BEHAVIOR = SIGNAL_EXIT_BEHAVIOR_CRASH + + # If multiple threads simultaneously were to modify this + # global state, this function would not behave as expected. + with _SUSPEND_WRITE_LOCK: + _SIGNAL_EXIT_BEHAVIOR = SIGNAL_EXIT_BEHAVIOR_QUIET + yield + _SIGNAL_EXIT_BEHAVIOR = SIGNAL_EXIT_BEHAVIOR_CRASH diff --git a/tests/unittests/test_signal_handler.py b/tests/unittests/test_signal_handler.py index e2808cb6b54..8f3b08fee17 100644 --- a/tests/unittests/test_signal_handler.py +++ b/tests/unittests/test_signal_handler.py @@ -8,8 +8,8 @@ from cloudinit import signal_handler +REENTRANT = "reentrant" -@patch.object(signal_handler.sys, "exit", Mock()) class TestSignalHandler: @pytest.mark.parametrize( @@ -21,7 +21,30 @@ class TestSignalHandler: (1, inspect.currentframe()), ], ) - def test_suspend_signal(self, m_args): + @pytest.mark.parametrize( + "m_suspended", + [ + (REENTRANT, 0), + (True, 0), + (False, 1), + ], + ) + def test_suspend_signal(self, m_args, m_suspended): + """suspend_crash should prevent crashing (exit 1) on signal + + otherwise cloud-init should exit 1 + """ sig, frame = m_args - with signal_handler.suspend_crash(): - signal_handler._handle_exit(sig, frame) + suspended, rc = m_suspended + + with patch.object(signal_handler.sys, "exit", Mock()) as m_exit: + if suspended is True: + with signal_handler.suspend_crash(): + signal_handler._handle_exit(sig, frame) + elif suspended == REENTRANT: + with signal_handler.suspend_crash( + ), signal_handler.suspend_crash(): + signal_handler._handle_exit(sig, frame) + else: + signal_handler._handle_exit(sig, frame) + m_exit.assert_called_with(rc)