From 4a2ba8e2abd4c7ffccbb9d8bc35730ee10b6e466 Mon Sep 17 00:00:00 2001 From: Matt Meshulam Date: Tue, 3 Nov 2026 09:41:09 -0600 Subject: [PATCH 1/3] Defer registering SIGCONT handler until UnixConsole is actually used Related: gh-161254 --- Lib/_pyrepl/unix_console.py | 11 +++++++++-- Lib/test/test_pyrepl/test_unix_console.py | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 3e15a340135669..3f0d057cfb8a3f 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -267,8 +267,6 @@ def _my_getstr(cap: str, optional: bool = False) -> bytes | None: ) self.cursor_visible = True - signal.signal(signal.SIGCONT, self._sigcont_handler) - def _sigcont_handler(self, signum, frame): self.restore() self.prepare() @@ -505,6 +503,7 @@ def prepare(self) -> None: try: self.old_sigwinch = signal.signal(signal.SIGWINCH, self.__sigwinch) + signal.signal(signal.SIGCONT, self._sigcont_handler) except ValueError: pass @@ -515,6 +514,9 @@ def restore(self) -> None: Restore the console to the default state """ trace("unix.restore") + if not hasattr(self, "_UnixConsole__svtermstate"): + # prepare() never ran, so there is nothing to restore + return self.__write(ANSIColors.RESET) self.__disable_bracketed_paste() self.__maybe_write_code(self._rmkx) @@ -533,6 +535,11 @@ def restore(self) -> None: raise e del self.old_sigwinch + try: + signal.signal(signal.SIGCONT, signal.SIG_DFL) + except ValueError: + pass + def push_char(self, char: int | bytes) -> None: """ Push a character to the console event queue. diff --git a/Lib/test/test_pyrepl/test_unix_console.py b/Lib/test/test_pyrepl/test_unix_console.py index dbf7f91696b559..912d49c8411e97 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -401,6 +401,26 @@ def test_restore_in_thread(self, _os_write): thread.start() thread.join() # this should not raise + @unittest.skipIf(not hasattr(signal, "SIGCONT"), "needs signal.SIGCONT") + def test_sigcont_is_armed_only_by_prepare(self, _os_write): + # gh-161254: A console must install its SIGCONT handler only while it + # owns the terminal, i.e. between prepare() and restore(). + console = UnixConsole(term="xterm") + self.assertEqual(signal.getsignal(signal.SIGCONT), signal.SIG_DFL) + console.prepare() + self.assertEqual( + signal.getsignal(signal.SIGCONT), console._sigcont_handler + ) + console.restore() + self.assertEqual(signal.getsignal(signal.SIGCONT), signal.SIG_DFL) + + @unittest.skipIf(not hasattr(signal, "SIGCONT"), "needs signal.SIGCONT") + def test_restore_without_prepare(self, _os_write): + # gh-161254 + console = UnixConsole(term="xterm") + self.assertFalse(hasattr(console, "_UnixConsole__svtermstate")) + console.restore() # this should not raise + @unittest.skipIf(sys.platform == "win32", "No Unix console on Windows") class TestUnixConsoleEIOHandling(TestCase): From e8b697e9ca34dee7fbe6be53c46793a5e46cfb0c Mon Sep 17 00:00:00 2001 From: Matt Meshulam Date: Tue, 3 Nov 2026 10:59:23 -0600 Subject: [PATCH 2/3] Add news blurb --- .../next/Library/2026-11-03-10-59-00.gh-issue-161254.6G4s79.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-11-03-10-59-00.gh-issue-161254.6G4s79.rst diff --git a/Misc/NEWS.d/next/Library/2026-11-03-10-59-00.gh-issue-161254.6G4s79.rst b/Misc/NEWS.d/next/Library/2026-11-03-10-59-00.gh-issue-161254.6G4s79.rst new file mode 100644 index 00000000000000..1ab08b4586806a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-03-10-59-00.gh-issue-161254.6G4s79.rst @@ -0,0 +1,2 @@ +Fix a crash when suspending (:kbd:`Ctrl-Z`) and resuming (:kbd:`fg`) an +embedded :func:`code.interact` session after enabling interactive mode. From facba2de51fe3e7db58c29c40b1c0842f530d5a2 Mon Sep 17 00:00:00 2001 From: Matt Meshulam Date: Tue, 3 Nov 2026 15:06:17 -0600 Subject: [PATCH 3/3] Reset SIGCONT handler to isolate test behavior --- Lib/test/test_pyrepl/test_unix_console.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_pyrepl/test_unix_console.py b/Lib/test/test_pyrepl/test_unix_console.py index 912d49c8411e97..c0a72d113454fa 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -401,10 +401,11 @@ def test_restore_in_thread(self, _os_write): thread.start() thread.join() # this should not raise - @unittest.skipIf(not hasattr(signal, "SIGCONT"), "needs signal.SIGCONT") + @unittest.skipUnless(hasattr(signal, "SIGCONT"), "needs signal.SIGCONT") def test_sigcont_is_armed_only_by_prepare(self, _os_write): # gh-161254: A console must install its SIGCONT handler only while it # owns the terminal, i.e. between prepare() and restore(). + signal.signal(signal.SIGCONT, signal.SIG_DFL) console = UnixConsole(term="xterm") self.assertEqual(signal.getsignal(signal.SIGCONT), signal.SIG_DFL) console.prepare() @@ -414,7 +415,7 @@ def test_sigcont_is_armed_only_by_prepare(self, _os_write): console.restore() self.assertEqual(signal.getsignal(signal.SIGCONT), signal.SIG_DFL) - @unittest.skipIf(not hasattr(signal, "SIGCONT"), "needs signal.SIGCONT") + @unittest.skipUnless(hasattr(signal, "SIGCONT"), "needs signal.SIGCONT") def test_restore_without_prepare(self, _os_write): # gh-161254 console = UnixConsole(term="xterm")