Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions src/con_duct/_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions test/duct_main/test_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down