From e570f91611bf5991f7a2e5684ab39825a871dcd6 Mon Sep 17 00:00:00 2001 From: leej3 Date: Sun, 11 Oct 2026 15:47:45 -0400 Subject: [PATCH] fix: make SIGINT attachment atomic and enable macOS setsid tests Block SIGINT during PID attachment and pending-signal forwarding. Add a regression test and install util-linux for macOS CI session coverage. Co-Authored-By: Codex Desktop 26.1007.21159 (runtime codex-cli 0.162.0-alpha.17.2) / gpt-6-astra Codex-Reasoning-Effort: low --- .github/workflows/test.yaml | 6 ++++++ src/con_duct/_signals.py | 12 +++++++++--- test/duct_main/test_signals.py | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3bd538d9..a00c31ee 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -72,6 +72,12 @@ jobs: name: Install libjpeg on Ubuntu run: sudo apt-get install -y libjpeg-dev + - name: Install setsid on macOS + if: runner.os == 'macOS' + run: | + brew install util-linux + echo "$(brew --prefix util-linux)/bin" >> "$GITHUB_PATH" + - name: Install uv if: matrix.toxenv == 'oldestdeps' run: python -m pip install uv diff --git a/src/con_duct/_signals.py b/src/con_duct/_signals.py index fa198509..82cc907b 100644 --- a/src/con_duct/_signals.py +++ b/src/con_duct/_signals.py @@ -53,9 +53,15 @@ def attach(self, pid: int) -> None: The PID of the process monitored by duct. A SIGINT that arrived while it was being started is acted on now. """ - self.pid = pid - if self.sigcount: - self._act() + # A SIGINT after publishing the PID must not be forwarded both by + # __call__ and by the pending-signal check below. + old_mask = signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGINT}) + try: + self.pid = pid + if self.sigcount: + self._act() + finally: + signal.pthread_sigmask(signal.SIG_SETMASK, old_mask) def __call__(self, _sig: int, _frame: Optional[FrameType]) -> None: self.sigcount += 1 diff --git a/test/duct_main/test_signals.py b/test/duct_main/test_signals.py index 1fe67690..bdc84a11 100644 --- a/test/duct_main/test_signals.py +++ b/test/duct_main/test_signals.py @@ -82,6 +82,23 @@ def test_attach_acts_on_earlier_sigints( assert sent == [(1234, expected)] +@pytest.mark.usefixtures("restore_sigint") +def test_sigint_during_attach_is_forwarded_once(sent: list[tuple[int, int]]) -> None: + class InterruptOnAttach(SigIntHandler): + def __setattr__(self, name: str, value: Any) -> None: + super().__setattr__(name, value) + if name == "pid" and value is not None: + # Deliver a real SIGINT immediately after publishing the PID. + signal.raise_signal(signal.SIGINT) + + handler = InterruptOnAttach() + signal.signal(signal.SIGINT, handler) + handler.attach(1234) + + assert handler.sigcount == 1 + assert sent == [(1234, signal.SIGINT)] + + @pytest.mark.usefixtures("stderr_locked") def test_forwards_even_if_logging_fails(sent: list[tuple[int, int]]) -> None: handler = SigIntHandler()