From fd3773d6fa19c1b4c216b5618f471e2f52a8765c Mon Sep 17 00:00:00 2001 From: yihong Date: Thu, 9 Oct 2025 22:58:01 +0800 Subject: [PATCH] gh-139391: properly handle `signal.signal()` in `UnixConsole` when called from a non-main thread (GH-139392) (cherry picked from commit b8c8b8f1d30767e5358ffa93d41e6e27ca60570d) Co-authored-by: yihong --- Lib/_pyrepl/unix_console.py | 7 ++++++- Lib/test/test_pyrepl/test_unix_console.py | 14 +++++++++++++- .../2025-09-28-16-34-11.gh-issue-139391.nRFnmx.rst | 3 +++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-09-28-16-34-11.gh-issue-139391.nRFnmx.rst diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index fe45b4eb384067..09247de748ee3b 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -390,7 +390,12 @@ def restore(self): os.write(self.output_fd, b"\033[?7h") if hasattr(self, "old_sigwinch"): - signal.signal(signal.SIGWINCH, self.old_sigwinch) + try: + signal.signal(signal.SIGWINCH, self.old_sigwinch) + except ValueError as e: + import threading + if threading.current_thread() is threading.main_thread(): + raise e del self.old_sigwinch def push_char(self, char: int | bytes) -> None: diff --git a/Lib/test/test_pyrepl/test_unix_console.py b/Lib/test/test_pyrepl/test_unix_console.py index 9ac2e9961df2dc..f4fb9237ffdfd0 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -4,11 +4,12 @@ import signal import subprocess import sys +import threading import unittest from functools import partial from test import support from test.support import os_helper, force_not_colorized_test_class -from test.support import script_helper +from test.support import script_helper, threading_helper from unittest import TestCase from unittest.mock import MagicMock, call, patch, ANY, Mock @@ -318,6 +319,17 @@ def test_restore_with_invalid_environ_on_macos(self, _os_write): console.prepare() # needed to call restore() console.restore() # this should succeed + @threading_helper.reap_threads + @threading_helper.requires_working_threading() + def test_restore_in_thread(self, _os_write): + # gh-139391: ensure that console.restore() silently suppresses + # exceptions when calling signal.signal() from a non-main thread. + console = unix_console([]) + console.old_sigwinch = signal.SIG_DFL + thread = threading.Thread(target=console.restore) + thread.start() + thread.join() # this should not raise + @unittest.skipIf(sys.platform == "win32", "No Unix console on Windows") class TestUnixConsoleEIOHandling(TestCase): diff --git a/Misc/NEWS.d/next/Library/2025-09-28-16-34-11.gh-issue-139391.nRFnmx.rst b/Misc/NEWS.d/next/Library/2025-09-28-16-34-11.gh-issue-139391.nRFnmx.rst new file mode 100644 index 00000000000000..93d1ce613bc2d6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-28-16-34-11.gh-issue-139391.nRFnmx.rst @@ -0,0 +1,3 @@ +Fix an issue when, on non-Windows platforms, it was not possible to +gracefully exit a ``python -m asyncio`` process suspended by Ctrl+Z +and later resumed by :manpage:`fg` other than with :manpage:`kill`.