From c0bb775ac013b8cf74b3847cd4565e56abb0b1c5 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sun, 28 Sep 2025 16:38:20 +0800 Subject: [PATCH 1/6] fix: close issue 139391 by ignore the error... Signed-off-by: yihong0618 --- Lib/_pyrepl/unix_console.py | 10 +++++++++- Lib/test/test_pyrepl/test_unix_console.py | 18 ++++++++++++++++++ ...5-09-28-16-34-11.gh-issue-139391.nRFnmx.rst | 2 ++ 3 files changed, 29 insertions(+), 1 deletion(-) 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..19065eac092053 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -390,7 +390,15 @@ def restore(self): os.write(self.output_fd, b"\033[?7h") if hasattr(self, "old_sigwinch"): - signal.signal(signal.SIGWINCH, self.old_sigwinch) + # Only restore signal handler if we're in the main thread + # signal.signal() only works in the main thread of the main interpreter + try: + signal.signal(signal.SIGWINCH, self.old_sigwinch) + except ValueError: + # This can happen when called from a non-main thread + # (e.g., asyncio REPL). In this case, we skip signal restoration + # to avoid the "signal only works in main thread" error. + pass 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 3b0d2637dab9cb..155def5188095f 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -317,6 +317,24 @@ def test_restore_with_invalid_environ_on_macos(self, _os_write): console.prepare() # needed to call restore() console.restore() # this should succeed + def test_restore_in_thread(self, _os_write): + # for gh-139391 + import threading + console = unix_console([]) + console.old_sigwinch = signal.SIG_DFL + exception_caught = [] + def thread_target(): + try: + console.restore() + except ValueError as e: + if "signal only works in main thread" in str(e): + exception_caught.append(e) + thread = threading.Thread(target=thread_target) + thread.start() + thread.join() + self.assertEqual(len(exception_caught), 0, + "restore() should not raise ValueError in non-main thread") + @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..f30fcec80e16cc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-28-16-34-11.gh-issue-139391.nRFnmx.rst @@ -0,0 +1,2 @@ +Fix crash in PyREPL asyncio mode when using Ctrl+Z (suspend) followed by +``fg`` (resume). From 85b571db5b16354871b80ebe8b1e96288e16ad4d Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sun, 28 Sep 2025 17:34:58 +0800 Subject: [PATCH 2/6] fix: address comments better way fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: yihong0618 Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/_pyrepl/unix_console.py | 19 ++++++++++--------- Lib/test/test_pyrepl/test_unix_console.py | 8 ++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 19065eac092053..1753b4b5ab1d1e 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -28,6 +28,7 @@ import signal import struct import termios +import threading import time import types import platform @@ -390,15 +391,15 @@ def restore(self): os.write(self.output_fd, b"\033[?7h") if hasattr(self, "old_sigwinch"): - # Only restore signal handler if we're in the main thread - # signal.signal() only works in the main thread of the main interpreter - try: - signal.signal(signal.SIGWINCH, self.old_sigwinch) - except ValueError: - # This can happen when called from a non-main thread - # (e.g., asyncio REPL). In this case, we skip signal restoration - # to avoid the "signal only works in main thread" error. - pass + if os.name != 'nt': + try: + signal.signal(signal.SIGWINCH, self.old_sigwinch) + except ValueError as e: + # We can silence the ValueError if signal.signal() raised it + # from a non-main thread on a non-Windows platform. Otherwise, + # we need to re-raise it as its cause could be different. + 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 155def5188095f..198c2d7d1b13fa 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -326,14 +326,14 @@ def test_restore_in_thread(self, _os_write): def thread_target(): try: console.restore() - except ValueError as e: - if "signal only works in main thread" in str(e): - exception_caught.append(e) + except Exception as e: + exception_caught.append(e) thread = threading.Thread(target=thread_target) thread.start() thread.join() + # gh-139391: should not raise any exception when called from non-main thread self.assertEqual(len(exception_caught), 0, - "restore() should not raise ValueError in non-main thread") + "restore() should not raise any exception in non-main thread") @unittest.skipIf(sys.platform == "win32", "No Unix console on Windows") From c454f6307e6f036379d082ce87864c2801baec97 Mon Sep 17 00:00:00 2001 From: yihong Date: Sun, 28 Sep 2025 17:39:26 +0800 Subject: [PATCH 3/6] Update Lib/_pyrepl/unix_console.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/_pyrepl/unix_console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 1753b4b5ab1d1e..ab228f6f405496 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -399,7 +399,7 @@ def restore(self): # from a non-main thread on a non-Windows platform. Otherwise, # we need to re-raise it as its cause could be different. if threading.current_thread() is threading.main_thread(): - raise e + raise e del self.old_sigwinch def push_char(self, char: int | bytes) -> None: From bcd67d3c6d275299e55403cc9c4928fcd67f7bb6 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sun, 28 Sep 2025 19:12:58 +0800 Subject: [PATCH 4/6] fix: apply the suggestion Signed-off-by: yihong0618 --- Lib/_pyrepl/unix_console.py | 17 ++++++++--------- Lib/test/test_pyrepl/test_unix_console.py | 22 ++++++++-------------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index ab228f6f405496..9eef8c3b5dafa3 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -391,15 +391,14 @@ def restore(self): os.write(self.output_fd, b"\033[?7h") if hasattr(self, "old_sigwinch"): - if os.name != 'nt': - try: - signal.signal(signal.SIGWINCH, self.old_sigwinch) - except ValueError as e: - # We can silence the ValueError if signal.signal() raised it - # from a non-main thread on a non-Windows platform. Otherwise, - # we need to re-raise it as its cause could be different. - if threading.current_thread() is threading.main_thread(): - raise e + try: + signal.signal(signal.SIGWINCH, self.old_sigwinch) + except ValueError as e: + # We can silence the ValueError if signal.signal() raised it + # from a non-main thread on a non-Windows platform. Otherwise, + # we need to re-raise it as its cause could be different. + 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 198c2d7d1b13fa..5522ac4d83d545 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -4,10 +4,11 @@ import signal import subprocess import sys +import threading import unittest from functools import partial 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 @@ -317,23 +318,16 @@ 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): - # for gh-139391 - import threading + # 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 - exception_caught = [] - def thread_target(): - try: - console.restore() - except Exception as e: - exception_caught.append(e) - thread = threading.Thread(target=thread_target) + thread = threading.Thread(target=console.restore) thread.start() - thread.join() - # gh-139391: should not raise any exception when called from non-main thread - self.assertEqual(len(exception_caught), 0, - "restore() should not raise any exception in non-main thread") + thread.join() # this should not raise @unittest.skipIf(sys.platform == "win32", "No Unix console on Windows") From 933a9d7d614fd17e93f3f6279cafe50c2f8d4f2e Mon Sep 17 00:00:00 2001 From: yihong Date: Mon, 29 Sep 2025 17:05:08 +0800 Subject: [PATCH 5/6] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/_pyrepl/unix_console.py | 1 + .../Library/2025-09-28-16-34-11.gh-issue-139391.nRFnmx.rst | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 9eef8c3b5dafa3..2a1f2e67a2dab5 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -397,6 +397,7 @@ def restore(self): # We can silence the ValueError if signal.signal() raised it # from a non-main thread on a non-Windows platform. Otherwise, # we need to re-raise it as its cause could be different. + import threading if threading.current_thread() is threading.main_thread(): raise e del self.old_sigwinch 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 index f30fcec80e16cc..93d1ce613bc2d6 100644 --- 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 @@ -1,2 +1,3 @@ -Fix crash in PyREPL asyncio mode when using Ctrl+Z (suspend) followed by -``fg`` (resume). +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`. From 56e0f760d3d6c4e3e2156376a3ad0714f131a804 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Mon, 29 Sep 2025 17:06:05 +0800 Subject: [PATCH 6/6] fix: drop useless import as review Signed-off-by: yihong0618 --- Lib/_pyrepl/unix_console.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 2a1f2e67a2dab5..c6b4cd221dfa8e 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -28,7 +28,6 @@ import signal import struct import termios -import threading import time import types import platform