Skip to content

Fix main-thread detection when SIGINT handler is C-level - #3524

Open
00200200 wants to merge 4 commits into
python-trio:mainfrom
00200200:fix-2564-c-level-sigint-main-thread
Open

00200200 wants to merge 4 commits into
python-trio:mainfrom
00200200:fix-2564-c-level-sigint-main-thread

Conversation

@00200200

Copy link
Copy Markdown

Summary

Fixes #2564.

is_main_thread() currently treats any failure from signal.signal(SIGINT, getsignal(SIGINT)) as "not the main thread". That is correct for ValueError (the CPython error when you are not on the main thread of the main interpreter). It is wrong for TypeError.

When an embedding host such as Qt/PySide installs a C-level SIGINT handler, signal.getsignal(SIGINT) returns None. Round-tripping that through signal.signal() raises:

TypeError: signal handler must be signal.SIG_IGN, signal.SIG_DFL, or a callable object

That TypeError still means we reached signal.signal(), so we are on the main thread. Returning False made Trio skip signal setup (open_signal_receiver, the wakeup fd, etc.) even though it was running on the real main thread.

KIManager.install is unchanged for this case: it still refuses to replace a non-default SIGINT handler.

Test plan

  • New test test_is_main_thread_with_non_python_sigint_handler mocks signal.getsignal(SIGINT) -> None
    • fails on main (is_main_thread() is False)
    • passes after this change (True on the main thread, still False from a worker thread)
  • Existing test_is_main_thread and test_signals.py still pass

Embedding hosts such as Qt install a non-Python SIGINT handler, so
round-tripping it through signal.signal() raises TypeError. That is not
the same as running off the main thread (ValueError), and treating it
as such skipped Trio's signal setup. Fixes python-trio#2564.
@codecov

codecov Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00000%. Comparing base (0480602) to head (8afd6ed).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@               Coverage Diff               @@
##                 main        #3524   +/-   ##
===============================================
  Coverage   100.00000%   100.00000%           
===============================================
  Files             128          128           
  Lines           19471        19530   +59     
  Branches         1323         1326    +3     
===============================================
+ Hits            19471        19530   +59     
Files with missing lines Coverage Δ
src/trio/_signals.py 100.00000% <100.00000%> (ø)
src/trio/_tests/test_signals.py 100.00000% <100.00000%> (ø)
src/trio/_tests/test_util.py 100.00000% <100.00000%> (ø)
src/trio/_util.py 100.00000% <100.00000%> (ø)

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@A5rocks

A5rocks commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Thanks but I think this is the wrong solution to this. In particular, it is the main thread and we should be adding our SIGINT handler.

Edit: hmm, just realized I got my logic inverted. 🤔

I'm not convinced this is right and we don't have tests for it. I guess I'd be interested in some test cases!

@A5rocks A5rocks closed this Sep 30, 2026
@A5rocks A5rocks reopened this Sep 30, 2026
@00200200

Copy link
Copy Markdown
Author

Agreed the current approach needs a rethink — I flipped the “is this the main thread?” check the wrong way for the C-level SIGINT case. I’ll come back with tests that cover the real scenario before pushing another attempt.

@00200200

00200200 commented Oct 2, 2026

Copy link
Copy Markdown
Author

Added regression tests in ddfcc2c, including an embedded CPython host with a real C SIGINT handler. Main-thread detection succeeds while worker-thread detection remains false; KIManager preserves the host handler. Testing exposed a cleanup bug: open_signal_receiver(SIGINT) could replace the native handler and then fail to restore None. It now checks all requested handlers before changing any and rejects native handlers that Python cannot restore; unrelated signal receivers remain usable. The native host confirms four SIGINT deliveries reach the original handler. Targeted signal/util suites: Python 3.12, 36 passed and 1 xfailed; Python 3.11, 35 passed, 1 skipped and 1 xfailed. Black and Ruff pass. This does not claim the existing upstream PyPy/OpenSSL CI failures are fixed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Main thread recognition broken when embedding

2 participants