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..a12aebdb2c69ac --- /dev/null +++ b/Lib/test/test_tkinter/test_vwait_busyloop_stdin.py @@ -0,0 +1,75 @@ +# 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") +@unittest.skipIf(sys.platform == "win32", "test is not supported on Windows") +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(cpu_frac) + +# Schedule vwait and release +interp.after(100, do_vwait) +interp.after(500, lambda: interp.setvar("myvar", "done")) +""" + + # 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"\n") + + # Ensure child exits cleanly + proc.stdin.write(b"exit()\n") + + 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 = float(out) + + # 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() 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..de29cf08a8d235 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-20-15-52-12.gh-issue-139145.1bMDI-.rst @@ -0,0 +1,4 @@ +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. diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index f0882191d3c3e8..c1d4efd745802f 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,14 +3400,14 @@ EventHook(void) if (result < 0) break; } -#ifndef MS_WINDOWS - Tcl_DeleteFileHandler(tfile); -#endif if (errorInCmd) { errorInCmd = 0; PyErr_SetRaisedException(excInCmd); excInCmd = NULL; PyErr_Print(); +#ifndef MS_WINDOWS + if (!stdin_ready) Tcl_DeleteFileHandler(tfile); +#endif } PyEval_SaveThread(); return 0;