diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 3e15a3401356697..3f0d057cfb8a3f9 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 dbf7f91696b5591..c0a72d113454fab 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -401,6 +401,27 @@ def test_restore_in_thread(self, _os_write): thread.start() thread.join() # this should not raise + @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() + self.assertEqual( + signal.getsignal(signal.SIGCONT), console._sigcont_handler + ) + console.restore() + self.assertEqual(signal.getsignal(signal.SIGCONT), signal.SIG_DFL) + + @unittest.skipUnless(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): 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 000000000000000..1ab08b4586806a7 --- /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.