summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTerry Jan Reedy <tjreedy@udel.edu>2015-11-21 00:36:43 (GMT)
committerTerry Jan Reedy <tjreedy@udel.edu>2015-11-21 00:36:43 (GMT)
commit6a904c16f7eac3d6da1add1851a8de978db09e60 (patch)
tree80f9d024d70a077e14cc79048283b4631378fc0d
parent0f3c9765d4dd5e0b4df608baf8d93cc0e5c4b227 (diff)
downloadcpython-6a904c16f7eac3d6da1add1851a8de978db09e60.zip
cpython-6a904c16f7eac3d6da1add1851a8de978db09e60.tar.gz
cpython-6a904c16f7eac3d6da1add1851a8de978db09e60.tar.bz2
Issue #24455: Prevent IDLE from hanging when a) closing the shell while the
debugger is active (15347); b) closing the debugger with the [X] button (15348); and c) activating the debugger when already active (24455). The patch by Mark Roseman does this by making two changes. 1. To suspend and resume the gui.interaction method, use the tcl vwait mechanism interded for this purpose instead of root.mainloop & .quit. 2. In gui.run, allow any existing interaction to terminate first.
-rw-r--r--Lib/idlelib/Debugger.py60
1 files changed, 53 insertions, 7 deletions
diff --git a/Lib/idlelib/Debugger.py b/Lib/idlelib/Debugger.py
index 6875197..d86c79c 100644
--- a/Lib/idlelib/Debugger.py
+++ b/Lib/idlelib/Debugger.py
@@ -17,7 +17,10 @@ class Idb(bdb.Bdb):
self.set_step()
return
message = self.__frame2message(frame)
- self.gui.interaction(message, frame)
+ try:
+ self.gui.interaction(message, frame)
+ except (TclError, RuntimeError):
+ pass
def user_exception(self, frame, info):
if self.in_rpc_code(frame):
@@ -59,8 +62,42 @@ class Debugger:
self.frame = None
self.make_gui()
self.interacting = 0
+ self.nesting_level = 0
def run(self, *args):
+ # Deal with the scenario where we've already got a program running
+ # in the debugger and we want to start another. If that is the case,
+ # our second 'run' was invoked from an event dispatched not from
+ # the main event loop, but from the nested event loop in 'interaction'
+ # below. So our stack looks something like this:
+ # outer main event loop
+ # run()
+ # <running program with traces>
+ # callback to debugger's interaction()
+ # nested event loop
+ # run() for second command
+ #
+ # This kind of nesting of event loops causes all kinds of problems
+ # (see e.g. issue #24455) especially when dealing with running as a
+ # subprocess, where there's all kinds of extra stuff happening in
+ # there - insert a traceback.print_stack() to check it out.
+ #
+ # By this point, we've already called restart_subprocess() in
+ # ScriptBinding. However, we also need to unwind the stack back to
+ # that outer event loop. To accomplish this, we:
+ # - return immediately from the nested run()
+ # - abort_loop ensures the nested event loop will terminate
+ # - the debugger's interaction routine completes normally
+ # - the restart_subprocess() will have taken care of stopping
+ # the running program, which will also let the outer run complete
+ #
+ # That leaves us back at the outer main event loop, at which point our
+ # after event can fire, and we'll come back to this routine with a
+ # clean stack.
+ if self.nesting_level > 0:
+ self.abort_loop()
+ self.root.after(100, lambda: self.run(*args))
+ return
try:
self.interacting = 1
return self.idb.run(*args)
@@ -71,6 +108,7 @@ class Debugger:
if self.interacting:
self.top.bell()
return
+ self.abort_loop()
if self.stackviewer:
self.stackviewer.close(); self.stackviewer = None
# Clean up pyshell if user clicked debugger control close widget.
@@ -191,7 +229,12 @@ class Debugger:
b.configure(state="normal")
#
self.top.wakeup()
- self.root.mainloop()
+ # Nested main loop: Tkinter's main loop is not reentrant, so use
+ # Tcl's vwait facility, which reenters the event loop until an
+ # event handler sets the variable we're waiting on
+ self.nesting_level += 1
+ self.root.tk.call('vwait', '::idledebugwait')
+ self.nesting_level -= 1
#
for b in self.buttons:
b.configure(state="disabled")
@@ -215,23 +258,26 @@ class Debugger:
def cont(self):
self.idb.set_continue()
- self.root.quit()
+ self.abort_loop()
def step(self):
self.idb.set_step()
- self.root.quit()
+ self.abort_loop()
def next(self):
self.idb.set_next(self.frame)
- self.root.quit()
+ self.abort_loop()
def ret(self):
self.idb.set_return(self.frame)
- self.root.quit()
+ self.abort_loop()
def quit(self):
self.idb.set_quit()
- self.root.quit()
+ self.abort_loop()
+
+ def abort_loop(self):
+ self.root.tk.call('set', '::idledebugwait', '1')
stackviewer = None