From a90eba0c17004f87f25d049f9d52d63279592d50 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Tue, 9 Jun 2026 23:18:54 +0530 Subject: [PATCH] fix(companion): Correct manager pid and reset companion signals --- gunicorn/companion/manager.py | 27 +++++++++++++++++++++++++++ tests/test_companion_manager.py | 16 +++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/gunicorn/companion/manager.py b/gunicorn/companion/manager.py index 98c436ee..2a032758 100644 --- a/gunicorn/companion/manager.py +++ b/gunicorn/companion/manager.py @@ -25,6 +25,28 @@ if TYPE_CHECKING: PR_SET_PDEATHSIG = 1 +# Signals the arbiter and manager install handlers for; a forked companion +# resets them to the default so its stop signal works and the target starts +# from a clean slate, the same way a gunicorn worker does. +INHERITED_SIGNALS = [ + "SIGCHLD", "SIGTERM", "SIGINT", "SIGHUP", "SIGQUIT", + "SIGUSR1", "SIGUSR2", "SIGWINCH", "SIGTTIN", "SIGTTOU", +] + + +def reset_child_signals() -> None: + """Restore default signal handling in a freshly forked companion. + + The companion inherits the manager's (and arbiter's) handlers across the + fork. Without this, a stop signal like SIGTERM would hit the manager's + handler -- which just flips a flag -- instead of terminating the companion. + """ + for name in INHERITED_SIGNALS: + number = getattr(signal, name, None) + if number is not None: + signal.signal(number, signal.SIG_DFL) + + def set_parent_death_signal(stop_signal) -> bool: """Ask the kernel to send ``stop_signal`` when this process's parent dies. @@ -79,6 +101,10 @@ class CompanionManager: signal on Linux and, as a portable fallback, watches ``getppid`` each tick so it never keeps companions running under a dead arbiter. """ + # __init__ ran in the arbiter, so refresh pid/parent now that this is + # the manager process: the companion parent-death guard compares its + # getppid() against self.pid. + self.pid = os.getpid() self.parent_pid = os.getppid() self._install_signals() set_parent_death_signal(signal.SIGTERM) @@ -325,6 +351,7 @@ class CompanionManager: try: self._close_manager_fds() + reset_child_signals() set_parent_death_signal(signal.SIGTERM) if os.getppid() != self.pid: # Manager already died between fork and arming: do not run. diff --git a/tests/test_companion_manager.py b/tests/test_companion_manager.py index 3fe6e48b..356882da 100644 --- a/tests/test_companion_manager.py +++ b/tests/test_companion_manager.py @@ -9,7 +9,11 @@ from unittest import mock import pytest from gunicorn.companion.control import CommandError -from gunicorn.companion.manager import CompanionManager, set_parent_death_signal +from gunicorn.companion.manager import ( + CompanionManager, + reset_child_signals, + set_parent_death_signal, +) from gunicorn.companion.config import CompanionConfig from gunicorn.companion.process import State @@ -52,6 +56,16 @@ def test_log_exit_reports_signal_or_status(): assert any("status" in message for message in messages) +def test_reset_child_signals_restores_defaults(): + with mock.patch("signal.signal") as signal_signal: + reset_child_signals() + restored = {call.args[0]: call.args[1] for call in signal_signal.call_args_list} + # The stop signal must reach the default disposition, not the manager's + # inherited handler, so a forked companion actually terminates on SIGTERM. + assert restored[signal.SIGTERM] is signal.SIG_DFL + assert restored[signal.SIGINT] is signal.SIG_DFL + + def test_set_parent_death_signal_noop_off_linux(): with mock.patch("sys.platform", "darwin"): assert set_parent_death_signal(signal.SIGTERM) is False