]> granicus.if.org Git - python/commitdiff
M NEWS.txt
authorKurt B. Kaiser <kbk@shore.net>
Mon, 17 Feb 2003 18:57:16 +0000 (18:57 +0000)
committerKurt B. Kaiser <kbk@shore.net>
Mon, 17 Feb 2003 18:57:16 +0000 (18:57 +0000)
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
Lib/idlelib/PyShell.py
Lib/idlelib/ScriptBinding.py
Lib/idlelib/rpc.py
Lib/idlelib/run.py

index e1c6fc7028a43aa65517293fd86e2d34f5338ff4..a0f186946a33f84feec9c8bc8fbe0b6a294e6c82 100644 (file)
@@ -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?
 ===================================
index 98e091821bc7f59b1e52835a15286f0dfa0e05f7..f3291628bdec5d4c787462a2c643acbee5a6531c 100644 (file)
@@ -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:
 
index 0f4483278e30b490759b492321c6e24add3cb521..9604cb86184a4b6358831a7168b4443dcf5b95d0 100644 (file)
@@ -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:
index b50643ada8c8509b315a21bc5b62487181939745..c79f4fea14b6ef25c64d2eae9d36dd7adfb03c9c 100644 (file)
@@ -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("<i", len(s)) + s
         while len(s) > 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 --------------------
index f394bacd73e9d78ef875a10df11ba04ca0d238bb..06fc049b09b1f640ad26c3ff07c49e90c09efaea 100644 (file)
@@ -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)