[Idle-dev] CVS: idle NEWS.txt,1.10,1.11 PyShell.py,1.55,1.56 ScriptBinding.py,1.17,1.18 rpc.py,1.15,1.16 run.py,1.9,1.10

Kurt B. Kaiser kbk@users.sourceforge.net
Mon, 17 Feb 2003 10:57:19 -0800


Update of /cvsroot/idlefork/idle
In directory sc8-pr-cvs1:/tmp/cvs-serv20344

Modified Files:
	NEWS.txt PyShell.py ScriptBinding.py rpc.py run.py 
Log Message:
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().



Index: NEWS.txt
===================================================================
RCS file: /cvsroot/idlefork/idle/NEWS.txt,v
retrieving revision 1.10
retrieving revision 1.11
diff -C2 -r1.10 -r1.11
*** NEWS.txt	27 Jan 2003 02:40:20 -0000	1.10
--- NEWS.txt	17 Feb 2003 18:57:16 -0000	1.11
***************
*** 3,6 ****
--- 3,32 ----
  +++++++++++++
  
+ 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,117 ****
  
  - 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.
  
  
--- 131,134 ----

Index: PyShell.py
===================================================================
RCS file: /cvsroot/idlefork/idle/PyShell.py,v
retrieving revision 1.55
retrieving revision 1.56
diff -C2 -r1.55 -r1.56
*** PyShell.py	31 Jan 2003 05:06:43 -0000	1.55
--- PyShell.py	17 Feb 2003 18:57:16 -0000	1.56
***************
*** 9,12 ****
--- 9,13 ----
  import socket
  import time
+ import threading
  import traceback
  import types
***************
*** 362,368 ****
          debug = self.getdebugger()
          if debug:
!             RemoteDebugger.close_subprocess_debugger(self.rpcclt)
!         # kill subprocess, spawn a new one, accept connection
!         self.rpcclt.close()
          self.spawn_subprocess()
          self.rpcclt.accept()
--- 363,383 ----
          debug = self.getdebugger()
          if debug:
!             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()
***************
*** 375,379 ****
          console.text.mark_set("restart", "end-1c")
          console.text.mark_gravity("restart", "left")
!         # restart remote debugger
          if debug:
              gui = RemoteDebugger.restart_subprocess_debugger(self.rpcclt)
--- 390,394 ----
          console.text.mark_set("restart", "end-1c")
          console.text.mark_gravity("restart", "left")
!         # restart subprocess debugger
          if debug:
              gui = RemoteDebugger.restart_subprocess_debugger(self.rpcclt)
***************
*** 381,384 ****
--- 396,417 ----
              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:
***************
*** 394,398 ****
          if clt is None:
              return
!         response = clt.pollresponse(self.active_seq)
          # Reschedule myself in 50 ms
          self.tkconsole.text.after(50, self.poll_subprocess)
--- 427,444 ----
          if clt is None:
              return
!         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)
***************
*** 572,577 ****
          "Override base class method"
          if self.tkconsole.executing:
!             self.display_executing_dialog()
!             return
          self.checklinecache()
          if self.save_warnings_filters is not None:
--- 618,622 ----
          "Override base class method"
          if self.tkconsole.executing:
!             self.interp.restart_subprocess()
          self.checklinecache()
          if self.save_warnings_filters is not None:
***************
*** 671,678 ****
              self.interp.start_subprocess()
  
!     reading = 0
!     executing = 0
!     canceled = 0
!     endoffile = 0
  
      def toggle_debugger(self, event=None):
--- 716,724 ----
              self.interp.start_subprocess()
  
!     reading = False
!     executing = False
!     canceled = False
!     endoffile = False
!     closing = False
  
      def toggle_debugger(self, event=None):
***************
*** 749,763 ****
          "Extend EditorWindow.close()"
          if self.executing:
!             # XXX Need to ask a question here
!             if not tkMessageBox.askokcancel(
                  "Kill?",
!                 "The program is still running; do you want to kill it?",
                  default="ok",
!                 master=self.text):
                  return "cancel"
!             self.canceled = 1
!             if self.reading:
!                 self.top.quit()
!             return "cancel"
          return EditorWindow.close(self)
  
--- 795,809 ----
          "Extend EditorWindow.close()"
          if self.executing:
!             response = tkMessageBox.askokcancel(
                  "Kill?",
!                 "The program is still running!\n Do you want to kill it?",
                  default="ok",
!                 master=self.text)
!             if response == False:
                  return "cancel"
!             # interrupt the subprocess
!             self.closing = True
!             self.cancel_callback()
!             self.endexecuting()
          return EditorWindow.close(self)
  
***************
*** 820,824 ****
          return True
  
!     def cancel_callback(self, event):
          try:
              if self.text.compare("sel.first", "!=", "sel.last"):
--- 866,870 ----
          return True
  
!     def cancel_callback(self, event=None):
          try:
              if self.text.compare("sel.first", "!=", "sel.last"):
***************
*** 832,847 ****
              return "break"
          self.endoffile = 0
          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
          return "break"
  
--- 878,886 ----
              return "break"
          self.endoffile = 0
+         self.canceled = 1
          if self.reading:
              self.top.quit()
!         elif (self.executing and self.interp.rpcclt):
!             self.interp.interrupt_subprocess()
          return "break"
  
***************
*** 1021,1030 ****
  
      def write(self, s, tags=()):
!         self.text.mark_gravity("iomark", "right")
!         OutputWindow.write(self, s, tags, "iomark")
!         self.text.mark_gravity("iomark", "left")
          if self.canceled:
              self.canceled = 0
-             raise KeyboardInterrupt
  
  class PseudoFile:
--- 1060,1071 ----
  
      def write(self, s, tags=()):
!         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
  
  class PseudoFile:

Index: ScriptBinding.py
===================================================================
RCS file: /cvsroot/idlefork/idle/ScriptBinding.py,v
retrieving revision 1.17
retrieving revision 1.18
diff -C2 -r1.17 -r1.18
*** ScriptBinding.py	26 Jan 2003 04:17:16 -0000	1.17
--- ScriptBinding.py	17 Feb 2003 18:57:16 -0000	1.18
***************
*** 125,131 ****
          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...
--- 125,128 ----

Index: rpc.py
===================================================================
RCS file: /cvsroot/idlefork/idle/rpc.py,v
retrieving revision 1.15
retrieving revision 1.16
diff -C2 -r1.15 -r1.16
*** rpc.py	31 Jan 2003 05:06:43 -0000	1.15
--- rpc.py	17 Feb 2003 18:57:16 -0000	1.16
***************
*** 87,90 ****
--- 87,100 ----
          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 = {}
  
***************
*** 101,107 ****
              objtable = objecttable
          self.objtable = objtable
!         self.statelock = threading.Lock()
          self.responses = {}
          self.cvars = {}
  
      def close(self):
--- 111,118 ----
              objtable = objecttable
          self.objtable = objtable
!         self.cvar = threading.Condition()
          self.responses = {}
          self.cvars = {}
+         self.interrupted = False
  
      def close(self):
***************
*** 154,164 ****
                  ret = remoteref(ret)
              return ("OK", ret)
          except:
              self.debug("localcall:EXCEPTION")
              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):'
              exclude = ("run.py", "rpc.py", "RemoteDebugger.py", "bdb.py")
              self.cleanup_traceback(tbe, exclude)
--- 165,178 ----
                  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, '\nTraceback (most recent call last):'
              exclude = ("run.py", "rpc.py", "RemoteDebugger.py", "bdb.py")
              self.cleanup_traceback(tbe, exclude)
***************
*** 187,193 ****
              del tb[-1]
          if len(tb) == 0:
!             # error was in RPC internals, don't prune!
              tb[:] = orig_tb[:]
!             print>>sys.stderr, "** RPC Internal Error: ", tb
          for i in range(len(tb)):
              fn, ln, nm, line = tb[i]
--- 201,207 ----
              del tb[-1]
          if len(tb) == 0:
!             # exception was in RPC internals, don't prune!
              tb[:] = orig_tb[:]
!             print>>sys.stderr, "** IDLE RPC Internal Exception: "
          for i in range(len(tb)):
              fn, ln, nm, line = tb[i]
***************
*** 200,204 ****
  
      def remotecall(self, oid, methodname, args, kwargs):
!         self.debug("calling asynccall via remotecall")
          seq = self.asynccall(oid, methodname, args, kwargs)
          return self.asyncreturn(seq)
--- 214,223 ----
  
      def remotecall(self, oid, methodname, args, kwargs):
!         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)
***************
*** 222,226 ****
              return what
          if how == "EXCEPTION":
!             raise Exception, "RPC SocketIO.decoderesponse exception"
          if how == "ERROR":
              self.debug("decoderesponse: Internal ERROR:", what)
--- 241,246 ----
              return what
          if how == "EXCEPTION":
!             self.debug("decoderesponse: EXCEPTION")
!             return None
          if how == "ERROR":
              self.debug("decoderesponse: Internal ERROR:", what)
***************
*** 267,280 ****
          else:
              # Auxiliary thread: wait for notification from main thread
!             cvar = threading.Condition(self.statelock)
!             self.statelock.acquire()
!             self.cvars[myseq] = cvar
              while not self.responses.has_key(myseq):
!                 cvar.wait()
              response = self.responses[myseq]
              del self.responses[myseq]
              del self.cvars[myseq]
!             self.statelock.release()
!             return response  # might be None
  
      def newseq(self):
--- 287,299 ----
          else:
              # Auxiliary thread: wait for notification from main thread
!             self.cvar.acquire()
!             self.cvars[myseq] = self.cvar
              while not self.responses.has_key(myseq):
!                 self.cvar.wait()
              response = self.responses[myseq]
              del self.responses[myseq]
              del self.cvars[myseq]
!             self.cvar.release()
!             return response
  
      def newseq(self):
***************
*** 291,296 ****
          s = struct.pack("<i", len(s)) + s
          while len(s) > 0:
!             n = self.sock.send(s)
!             s = s[n:]
  
      def ioready(self, wait=0.0):
--- 310,320 ----
          s = struct.pack("<i", len(s)) + s
          while len(s) > 0:
!             try:
!                 n = self.sock.send(s)
!             except AttributeError:
!                 # socket was closed
!                 raise IOError
!             else:
!                 s = s[n:]
  
      def ioready(self, wait=0.0):
***************
*** 375,384 ****
                  return resq
              else:
!                 self.statelock.acquire()
!                 self.responses[seq] = resq
                  cv = self.cvars.get(seq)
                  if cv is not None:
                      cv.notify()
!                 self.statelock.release()
                  continue
  
--- 399,410 ----
                  return resq
              else:
!                 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.cvar.release()
                  continue
  

Index: run.py
===================================================================
RCS file: /cvsroot/idlefork/idle/run.py,v
retrieving revision 1.9
retrieving revision 1.10
diff -C2 -r1.9 -r1.10
*** run.py	20 Dec 2002 04:24:43 -0000	1.9
--- run.py	17 Feb 2003 18:57:16 -0000	1.10
***************
*** 72,75 ****
--- 72,80 ----
          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)