From 003091cd51c5278e3ef76b6db01bd719b8b1c416 Mon Sep 17 00:00:00 2001 From: "Kurt B. Kaiser" Date: Mon, 17 Feb 2003 18:57:16 +0000 Subject: M NEWS.txt M PyShell.py M ScriptBinding.py M rpc.py M run.py Clean up the way IDLEfork handles termination of the subprocess, restore ability to interrupt user code in Windows (so long as it's doing terminal I/O). 1. Handle subprocess interrupts in Windows with an RPC message. 2. Run/F5 will restart the subprocess even if user code is running. 3. Restart the subprocess if the link is dropped. 4. Exit IDLE cleanly even during I/O. 4. In rpc.py, remove explicit calls to statelock, let the condition variable handle acquire() and release(). --- Lib/idlelib/NEWS.txt | 35 ++++++++++---- Lib/idlelib/PyShell.py | 109 +++++++++++++++++++++++++++++-------------- Lib/idlelib/ScriptBinding.py | 3 -- Lib/idlelib/rpc.py | 60 +++++++++++++++++------- Lib/idlelib/run.py | 5 ++ 5 files changed, 149 insertions(+), 63 deletions(-) diff --git a/Lib/idlelib/NEWS.txt b/Lib/idlelib/NEWS.txt index e1c6fc7..a0f1869 100644 --- a/Lib/idlelib/NEWS.txt +++ b/Lib/idlelib/NEWS.txt @@ -2,6 +2,32 @@ IDLEfork NEWS +++++++++++++ +What's New in IDLEfork 0.9 Alpha 3? +=================================== + +*Release date: xx-xxx-2003* + +- Exit IDLE cleanly even when doing subprocess I/O + +- Handle subprocess interrupt in Windows with an RPC message. + +- Calling Run will restart the subprocess even if user code is running. + +- Restart the subprocess if it terminates itself. (VPython programs do that) + +- Support subclassing of exceptions, including in the shell, by moving the + exception formatting to the subprocess. + +- Known issues: + + + Can't kill/restart a tight loop in the Windows version: add + I/O to the loop or use the Task Manager to kill the subprocess. + + Typing two Control-C in close succession when the subprocess is busy can + cause IDLE to lose communication with the subprocess. Please type one + only and wait for the exception to complete. + + Printing under some versions of Linux may be problematic. + + What's New in IDLEfork 0.9 Alpha 2? =================================== @@ -105,15 +131,6 @@ What's New in IDLEfork 0.9 Alpha 2? - Modified idle, idle.py, idle.pyw to improve exception handling. -- Known issues: - - + Can't kill a tight loop in the Windows version: Insert a - ``print "*",`` in an outer loop or use the Task Manager to kill. - + Typing two Control-C in close succession when the subprocess is busy can - cause IDLE to lose communication with the subprocess. Please type one - only and wait for the exception to complete. - + Printing under some versions of Linux may be problematic. - What's New in IDLEfork 0.9 Alpha 1? =================================== diff --git a/Lib/idlelib/PyShell.py b/Lib/idlelib/PyShell.py index 98e0918..f329162 100644 --- a/Lib/idlelib/PyShell.py +++ b/Lib/idlelib/PyShell.py @@ -8,6 +8,7 @@ import getopt import re import socket import time +import threading import traceback import types import exceptions @@ -361,9 +362,23 @@ class ModifiedInterpreter(InteractiveInterpreter): # close only the subprocess debugger debug = self.getdebugger() if debug: - RemoteDebugger.close_subprocess_debugger(self.rpcclt) - # kill subprocess, spawn a new one, accept connection - self.rpcclt.close() + try: + RemoteDebugger.close_subprocess_debugger(self.rpcclt) + except: + pass + # Kill subprocess, spawn a new one, accept connection. + if hasattr(os, 'kill'): + # We can interrupt any loop if we can use SIGINT. This doesn't + # work in Windows, currently we can only interrupt loops doing I/O. + self.__signal_interrupt() + # XXX KBK 13Feb03 Don't close the socket until the interrupt thread + # finishes. + self.tkconsole.executing = False + try: + self.rpcclt.close() + os.wait() + except: + pass self.spawn_subprocess() self.rpcclt.accept() self.transfer_path() @@ -374,12 +389,30 @@ class ModifiedInterpreter(InteractiveInterpreter): console.write(halfbar + ' RESTART ' + halfbar) console.text.mark_set("restart", "end-1c") console.text.mark_gravity("restart", "left") - # restart remote debugger + # restart subprocess debugger if debug: gui = RemoteDebugger.restart_subprocess_debugger(self.rpcclt) # reload remote debugger breakpoints for all PyShellEditWindows debug.load_breakpoints() + def __signal_interrupt(self): + try: + from signal import SIGINT + except ImportError: + SIGINT = 2 + os.kill(self.rpcpid, SIGINT) + + def __request_interrupt(self): + self.rpcclt.asynccall("exec", "interrupt_the_server", (), {}) + + def interrupt_subprocess(self): + if hasattr(os, "kill"): + self.__signal_interrupt() + else: + # Windows has no os.kill(), use an RPC message. + # This is async, must be done in a thread. + threading.Thread(target=self.__request_interrupt).start() + def transfer_path(self): self.runcommand("""if 1: import sys as _sys @@ -393,7 +426,20 @@ class ModifiedInterpreter(InteractiveInterpreter): clt = self.rpcclt if clt is None: return - response = clt.pollresponse(self.active_seq) + try: + response = clt.pollresponse(self.active_seq) + except (EOFError, IOError): + # lost connection: subprocess terminated itself, restart + if self.tkconsole.closing: + return + response = None + try: + # stake any zombie before restarting + os.wait() + except (AttributeError, OSError): + pass + self.restart_subprocess() + self.tkconsole.endexecuting() # Reschedule myself in 50 ms self.tkconsole.text.after(50, self.poll_subprocess) if response: @@ -571,8 +617,7 @@ class ModifiedInterpreter(InteractiveInterpreter): def runcode(self, code): "Override base class method" if self.tkconsole.executing: - self.display_executing_dialog() - return + self.interp.restart_subprocess() self.checklinecache() if self.save_warnings_filters is not None: warnings.filters[:] = self.save_warnings_filters @@ -670,10 +715,11 @@ class PyShell(OutputWindow): if use_subprocess: self.interp.start_subprocess() - reading = 0 - executing = 0 - canceled = 0 - endoffile = 0 + reading = False + executing = False + canceled = False + endoffile = False + closing = False def toggle_debugger(self, event=None): if self.executing: @@ -748,17 +794,17 @@ class PyShell(OutputWindow): def close(self): "Extend EditorWindow.close()" if self.executing: - # XXX Need to ask a question here - if not tkMessageBox.askokcancel( + response = tkMessageBox.askokcancel( "Kill?", - "The program is still running; do you want to kill it?", + "The program is still running!\n Do you want to kill it?", default="ok", - master=self.text): + master=self.text) + if response == False: return "cancel" - self.canceled = 1 - if self.reading: - self.top.quit() - return "cancel" + # interrupt the subprocess + self.closing = True + self.cancel_callback() + self.endexecuting() return EditorWindow.close(self) def _close(self): @@ -819,7 +865,7 @@ class PyShell(OutputWindow): def isatty(self): return True - def cancel_callback(self, event): + def cancel_callback(self, event=None): try: if self.text.compare("sel.first", "!=", "sel.last"): return # Active selection -- always use default binding @@ -831,18 +877,11 @@ class PyShell(OutputWindow): self.showprompt() return "break" self.endoffile = 0 + self.canceled = 1 if self.reading: - self.canceled = 1 self.top.quit() - elif (self.executing and self.interp.rpcclt and - self.interp.rpcpid and hasattr(os, "kill")): - try: - from signal import SIGINT - except ImportError: - SIGINT = 2 - os.kill(self.interp.rpcpid, SIGINT) - else: - self.canceled = 1 + elif (self.executing and self.interp.rpcclt): + self.interp.interrupt_subprocess() return "break" def eof_callback(self, event): @@ -1020,12 +1059,14 @@ class PyShell(OutputWindow): sys.stdout.softspace = 0 def write(self, s, tags=()): - self.text.mark_gravity("iomark", "right") - OutputWindow.write(self, s, tags, "iomark") - self.text.mark_gravity("iomark", "left") + try: + self.text.mark_gravity("iomark", "right") + OutputWindow.write(self, s, tags, "iomark") + self.text.mark_gravity("iomark", "left") + except: + pass if self.canceled: self.canceled = 0 - raise KeyboardInterrupt class PseudoFile: diff --git a/Lib/idlelib/ScriptBinding.py b/Lib/idlelib/ScriptBinding.py index 0f44832..9604cb8 100644 --- a/Lib/idlelib/ScriptBinding.py +++ b/Lib/idlelib/ScriptBinding.py @@ -124,9 +124,6 @@ class ScriptBinding: flist = self.editwin.flist shell = flist.open_shell() interp = shell.interp - if interp.tkconsole.executing: - interp.display_executing_dialog() - return interp.restart_subprocess() # XXX Too often this discards arguments the user just set... interp.runcommand("""if 1: diff --git a/Lib/idlelib/rpc.py b/Lib/idlelib/rpc.py index b50643a..c79f4fe 100644 --- a/Lib/idlelib/rpc.py +++ b/Lib/idlelib/rpc.py @@ -86,6 +86,16 @@ class RPCServer(SocketServer.TCPServer): "Override TCPServer method, return already connected socket" return self.socket, self.server_address + def handle_error(self, request, client_address): + """Override TCPServer method, no error message if exiting""" + try: + raise + except SystemExit: + raise + else: + TCPServer.handle_error(request, client_address) + + objecttable = {} class SocketIO: @@ -100,9 +110,10 @@ class SocketIO: if objtable is None: objtable = objecttable self.objtable = objtable - self.statelock = threading.Lock() + self.cvar = threading.Condition() self.responses = {} self.cvars = {} + self.interrupted = False def close(self): sock = self.sock @@ -153,13 +164,16 @@ class SocketIO: if isinstance(ret, RemoteObject): ret = remoteref(ret) return ("OK", ret) + except SystemExit: + raise except: self.debug("localcall:EXCEPTION") + if self.debugging: traceback.print_exc(file=sys.__stderr__) efile = sys.stderr typ, val, tb = info = sys.exc_info() sys.last_type, sys.last_value, sys.last_traceback = info tbe = traceback.extract_tb(tb) - print >>efile, 'Traceback (most recent call last):' + print >>efile, '\nTraceback (most recent call last):' exclude = ("run.py", "rpc.py", "RemoteDebugger.py", "bdb.py") self.cleanup_traceback(tbe, exclude) traceback.print_list(tbe, file=efile) @@ -186,9 +200,9 @@ class SocketIO: break del tb[-1] if len(tb) == 0: - # error was in RPC internals, don't prune! + # exception was in RPC internals, don't prune! tb[:] = orig_tb[:] - print>>sys.stderr, "** RPC Internal Error: ", tb + print>>sys.stderr, "** IDLE RPC Internal Exception: " for i in range(len(tb)): fn, ln, nm, line = tb[i] if nm == '?': @@ -199,7 +213,12 @@ class SocketIO: tb[i] = fn, ln, nm, line def remotecall(self, oid, methodname, args, kwargs): - self.debug("calling asynccall via remotecall") + self.debug("remotecall:asynccall: ", oid, methodname) + # XXX KBK 06Feb03 self.interrupted logic may not be necessary if + # subprocess is threaded. + if self.interrupted: + self.interrupted = False + raise KeyboardInterrupt seq = self.asynccall(oid, methodname, args, kwargs) return self.asyncreturn(seq) @@ -221,7 +240,8 @@ class SocketIO: if how == "OK": return what if how == "EXCEPTION": - raise Exception, "RPC SocketIO.decoderesponse exception" + self.debug("decoderesponse: EXCEPTION") + return None if how == "ERROR": self.debug("decoderesponse: Internal ERROR:", what) raise RuntimeError, what @@ -266,16 +286,15 @@ class SocketIO: return response else: # Auxiliary thread: wait for notification from main thread - cvar = threading.Condition(self.statelock) - self.statelock.acquire() - self.cvars[myseq] = cvar + self.cvar.acquire() + self.cvars[myseq] = self.cvar while not self.responses.has_key(myseq): - cvar.wait() + self.cvar.wait() response = self.responses[myseq] del self.responses[myseq] del self.cvars[myseq] - self.statelock.release() - return response # might be None + self.cvar.release() + return response def newseq(self): self.nextseq = seq = self.nextseq + 2 @@ -290,8 +309,13 @@ class SocketIO: raise s = struct.pack(" 0: - n = self.sock.send(s) - s = s[n:] + try: + n = self.sock.send(s) + except AttributeError: + # socket was closed + raise IOError + else: + s = s[n:] def ioready(self, wait=0.0): r, w, x = select.select([self.sock.fileno()], [], [], wait) @@ -374,12 +398,14 @@ class SocketIO: elif seq == myseq: return resq else: - self.statelock.acquire() - self.responses[seq] = resq + self.cvar.acquire() cv = self.cvars.get(seq) + # response involving unknown sequence number is discarded, + # probably intended for prior incarnation if cv is not None: + self.responses[seq] = resq cv.notify() - self.statelock.release() + self.cvar.release() continue #----------------- end class SocketIO -------------------- diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py index f394bac..06fc049 100644 --- a/Lib/idlelib/run.py +++ b/Lib/idlelib/run.py @@ -71,6 +71,11 @@ class Executive: def runcode(self, code): exec code in self.locals + def interrupt_the_server(self): + # XXX KBK 05Feb03 Windows requires this be done with messages and + # threads.... + self.rpchandler.interrupted = True + def start_the_debugger(self, gui_adap_oid): return RemoteDebugger.start_debugger(self.rpchandler, gui_adap_oid) -- cgit v0.12