From 6d0489a649e1b8dba3a9ee73d917b1459efa914d Mon Sep 17 00:00:00 2001 From: Michiel Jan Laurens de Hoon Date: Sat, 20 Sep 2025 19:49:55 +0900 Subject: [PATCH 1/9] bpo-139145: fix spinning event loop in tkinter --- Modules/_tkinter.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index f0882191d3c3e8..2ff767651234ea 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3356,7 +3356,9 @@ static int stdin_ready = 0; static void MyFileProc(void *clientData, int mask) { + int *tfile = clientData; stdin_ready = 1; + Tcl_DeleteFileHandler(*tfile); } #endif @@ -3373,7 +3375,7 @@ EventHook(void) errorInCmd = 0; #ifndef MS_WINDOWS tfile = fileno(stdin); - Tcl_CreateFileHandler(tfile, TCL_READABLE, MyFileProc, NULL); + Tcl_CreateFileHandler(tfile, TCL_READABLE, MyFileProc, &tfile); #endif while (!errorInCmd && !stdin_ready) { int result; @@ -3398,9 +3400,6 @@ EventHook(void) if (result < 0) break; } -#ifndef MS_WINDOWS - Tcl_DeleteFileHandler(tfile); -#endif if (errorInCmd) { errorInCmd = 0; PyErr_SetRaisedException(excInCmd); From 5dcb99fa4f5eaa8a05df09309d7bbfeff1738a3e Mon Sep 17 00:00:00 2001 From: Michiel Jan Laurens de Hoon Date: Sun, 21 Sep 2025 00:39:56 +0900 Subject: [PATCH 2/9] Adding test exposing the bug. The test mimics a user running Python interactively. --- .../test_tkinter/test_vwait_busyloop_stdin.py | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 Lib/test/test_tkinter/test_vwait_busyloop_stdin.py diff --git a/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py b/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py new file mode 100644 index 00000000000000..bf72d917dbcb5f --- /dev/null +++ b/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py @@ -0,0 +1,87 @@ +# test_tkinter_vwait_mainloop_stdin.py +import unittest +import subprocess +import sys +import time +from test import support +from test.support import import_helper + +tkinter = import_helper.import_module("tkinter") + + +@unittest.skipUnless(support.has_subprocess_support, "test requires subprocess") +class TkVwaitMainloopStdinTest(unittest.TestCase): + + def run_child(self): + # Full Python script to execute in the child + code = r""" +import tkinter as tk +import time + +interp = tk.Tcl() + +def do_vwait(): + start_wall = time.time() + start_cpu = time.process_time() + interp.eval("vwait myvar") + end_cpu = time.process_time() + end_wall = time.time() + cpu_frac = (end_cpu - start_cpu) / (end_wall - start_wall) + print(f"CPU fraction during vwait: {cpu_frac:.2f}", flush=True) + +# Schedule vwait and release +interp.after(100, do_vwait) +interp.after(500, lambda: interp.setvar("myvar", "done")) +# Schedule quit to stop mainloop +interp.after(1000, interp.quit) + +interp.mainloop() +""" + + # Start child in interactive mode, but use -c to execute code immediately + proc = subprocess.Popen( + [sys.executable, "-i", "-c", code], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return proc + + def test_vwait_stdin_busy_loop(self): + proc = self.run_child() + + # Wait until vwait likely started + time.sleep(0.15) + + # Send input to stdin to trigger the bug + proc.stdin.write(b"x\n") + proc.stdin.flush() + + # Ensure child exits cleanly + proc.stdin.write(b"exit()\n") + proc.stdin.flush() + + stdout, stderr = proc.communicate() + out = stdout.decode("utf-8", errors="replace") + err = stderr.decode("utf-8", errors="replace") + + if proc.returncode != 0: + self.fail(f"Child exited with {proc.returncode}\nSTDOUT:\n{out}\nSTDERR:\n{err}") + + # Extract CPU fraction printed by child + cpu_frac = None + for line in out.splitlines(): + if line.startswith("CPU fraction during vwait:"): + cpu_frac = float(line.split(":")[1].strip()) + break + + self.assertIsNotNone(cpu_frac, "CPU fraction not printed by child") + + # Fail if CPU fraction is too high (indicative of busy-loop) + self.assertLess(cpu_frac, 0.5, + f"CPU usage too high during vwait with stdin input (fraction={cpu_frac:.2f})") + + +if __name__ == "__main__": + unittest.main() + From 49258d1a5948a9376de278718a84b6d1b6cb6d21 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 15:52:14 +0000 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-09-20-15-52-12.gh-issue-139145.1bMDI-.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-09-20-15-52-12.gh-issue-139145.1bMDI-.rst diff --git a/Misc/NEWS.d/next/Library/2025-09-20-15-52-12.gh-issue-139145.1bMDI-.rst b/Misc/NEWS.d/next/Library/2025-09-20-15-52-12.gh-issue-139145.1bMDI-.rst new file mode 100644 index 00000000000000..c0da6f30d2a0cb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-20-15-52-12.gh-issue-139145.1bMDI-.rst @@ -0,0 +1 @@ +Avoids tkinter ending up in a busy loop when ``vwait`` (and other Tcl commands that run their own tight event loop) are used with Python in interactive mode, and the user types any into on stdin while ``vwait`` is still running. Patch by Michiel de Hoon. From 4c4e4b5a1afec7f121737334071e8c77002a23e6 Mon Sep 17 00:00:00 2001 From: Michiel Jan Laurens de Hoon Date: Sun, 21 Sep 2025 01:14:51 +0900 Subject: [PATCH 4/9] fixing lint --- Lib/test/test_tkinter/test_vwait_busyloop_stdin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py b/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py index bf72d917dbcb5f..53ca9038cdc892 100644 --- a/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py +++ b/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py @@ -84,4 +84,3 @@ def test_vwait_stdin_busy_loop(self): if __name__ == "__main__": unittest.main() - From 99086de95a5e753084c9663b0773f155273d5d5f Mon Sep 17 00:00:00 2001 From: Michiel Jan Laurens de Hoon Date: Sun, 21 Sep 2025 01:18:58 +0900 Subject: [PATCH 5/9] fix blurb --- .../Library/2025-09-20-15-52-12.gh-issue-139145.1bMDI-.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-09-20-15-52-12.gh-issue-139145.1bMDI-.rst b/Misc/NEWS.d/next/Library/2025-09-20-15-52-12.gh-issue-139145.1bMDI-.rst index c0da6f30d2a0cb..de29cf08a8d235 100644 --- a/Misc/NEWS.d/next/Library/2025-09-20-15-52-12.gh-issue-139145.1bMDI-.rst +++ b/Misc/NEWS.d/next/Library/2025-09-20-15-52-12.gh-issue-139145.1bMDI-.rst @@ -1 +1,4 @@ -Avoids tkinter ending up in a busy loop when ``vwait`` (and other Tcl commands that run their own tight event loop) are used with Python in interactive mode, and the user types any into on stdin while ``vwait`` is still running. Patch by Michiel de Hoon. +Prevents tkinter ending up in a busy loop when ``vwait`` (and other Tcl commands +that run their own tight event loop) are used with Python in interactive mode, +and the user types any input on stdin while ``vwait`` is still running. Patch +by Michiel de Hoon. From add49bdfec3723efcbdb081ac62b1e9d8c3223c7 Mon Sep 17 00:00:00 2001 From: Michiel de Hoon Date: Wed, 1 Oct 2025 16:07:14 +0900 Subject: [PATCH 6/9] simplify test --- .../test_tkinter/test_vwait_busyloop_stdin.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py b/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py index 53ca9038cdc892..95829ebafeb1d2 100644 --- a/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py +++ b/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py @@ -10,6 +10,7 @@ @unittest.skipUnless(support.has_subprocess_support, "test requires subprocess") +@unittest.skipIf(sys.platform == "win32", "test not supported on Windows") class TkVwaitMainloopStdinTest(unittest.TestCase): def run_child(self): @@ -27,15 +28,11 @@ def do_vwait(): end_cpu = time.process_time() end_wall = time.time() cpu_frac = (end_cpu - start_cpu) / (end_wall - start_wall) - print(f"CPU fraction during vwait: {cpu_frac:.2f}", flush=True) + print(cpu_frac) # Schedule vwait and release interp.after(100, do_vwait) interp.after(500, lambda: interp.setvar("myvar", "done")) -# Schedule quit to stop mainloop -interp.after(1000, interp.quit) - -interp.mainloop() """ # Start child in interactive mode, but use -c to execute code immediately @@ -54,12 +51,10 @@ def test_vwait_stdin_busy_loop(self): time.sleep(0.15) # Send input to stdin to trigger the bug - proc.stdin.write(b"x\n") - proc.stdin.flush() + proc.stdin.write(b"\n") # Ensure child exits cleanly proc.stdin.write(b"exit()\n") - proc.stdin.flush() stdout, stderr = proc.communicate() out = stdout.decode("utf-8", errors="replace") @@ -69,13 +64,7 @@ def test_vwait_stdin_busy_loop(self): self.fail(f"Child exited with {proc.returncode}\nSTDOUT:\n{out}\nSTDERR:\n{err}") # Extract CPU fraction printed by child - cpu_frac = None - for line in out.splitlines(): - if line.startswith("CPU fraction during vwait:"): - cpu_frac = float(line.split(":")[1].strip()) - break - - self.assertIsNotNone(cpu_frac, "CPU fraction not printed by child") + cpu_frac = float(out) # Fail if CPU fraction is too high (indicative of busy-loop) self.assertLess(cpu_frac, 0.5, From 35758f10d9b7e094963f132d558b754a95043ee6 Mon Sep 17 00:00:00 2001 From: Michiel de Hoon Date: Wed, 1 Oct 2025 16:10:07 +0900 Subject: [PATCH 7/9] be sure to delete the file handler also in case of errorInCmd --- Modules/_tkinter.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 2ff767651234ea..0b66468fc89dcd 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3405,6 +3405,7 @@ EventHook(void) PyErr_SetRaisedException(excInCmd); excInCmd = NULL; PyErr_Print(); + if (!stdin_ready) Tcl_DeleteFileHandler(tfile); } PyEval_SaveThread(); return 0; From a3d07d43b41f71effa4f6522f5972af5fa816ddc Mon Sep 17 00:00:00 2001 From: Michiel de Hoon Date: Wed, 1 Oct 2025 16:21:58 +0900 Subject: [PATCH 8/9] only relevant on Windows --- Modules/_tkinter.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 0b66468fc89dcd..c1d4efd745802f 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3405,7 +3405,9 @@ EventHook(void) PyErr_SetRaisedException(excInCmd); excInCmd = NULL; PyErr_Print(); +#ifndef MS_WINDOWS if (!stdin_ready) Tcl_DeleteFileHandler(tfile); +#endif } PyEval_SaveThread(); return 0; From e8fc83302d8a0ccd047f9f96cfcc398d8905135d Mon Sep 17 00:00:00 2001 From: Michiel Jan Laurens de Hoon Date: Wed, 1 Oct 2025 23:11:27 +0900 Subject: [PATCH 9/9] trivial change to trigger another round of testing --- Lib/test/test_tkinter/test_vwait_busyloop_stdin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py b/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py index 95829ebafeb1d2..a12aebdb2c69ac 100644 --- a/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py +++ b/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py @@ -10,7 +10,7 @@ @unittest.skipUnless(support.has_subprocess_support, "test requires subprocess") -@unittest.skipIf(sys.platform == "win32", "test not supported on Windows") +@unittest.skipIf(sys.platform == "win32", "test is not supported on Windows") class TkVwaitMainloopStdinTest(unittest.TestCase): def run_child(self):