Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions Lib/test/test_tkinter/test_vwait_busyloop_stdin.py
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 6 additions & 4 deletions Modules/_tkinter.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +3359 to +3361
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
int *tfile = clientData;
stdin_ready = 1;
Tcl_DeleteFileHandler(*tfile);
int tfile = (int)(Py_intptr_t)clientData;
stdin_ready = 1;
Tcl_DeleteFileHandler(tfile);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for looking into this. This is an important point.

The while-loop in EventHook can only be exited if either stdin_ready is 1 or errorInCmd is 1.
stdin_ready is set to 1 only in MyFileProc, which also calls Tcl_DeleteFileHandler. As the file handler is deleted, there won't be a dangling pointer.

But I was missing a call to Tcl_DeleteFileHandler for the case in which errorInCmd is 1, and MyFileProc was never called. Then then file handler will stay alive and will keep a dangling pointer to tfile after EventHook exits.

I fixed this by adding a call to Tcl_DeleteFileHandler to the if (errorInCmd) {... to ensure that the file handler is deleted before EventHook exits, and no pointer to tfile remains,

}
#endif

Expand All @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Tcl_CreateFileHandler(tfile, TCL_READABLE, MyFileProc, &tfile);
Tcl_CreateFileHandler(tfile, TCL_READABLE, MyFileProc, (void *)(Py_intptr_t)tfile);

#endif
while (!errorInCmd && !stdin_ready) {
int result;
Expand All @@ -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;
Expand Down
Loading