[Idle-dev] CVS: idle NEWS.txt,1.14,1.15 PyShell.py,1.59,1.60 rpc.py,1.22,1.23 run.py,1.14,1.15

Kurt B. Kaiser kbk@users.sourceforge.net
Thu, 08 May 2003 13:26:58 -0700


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

Modified Files:
	NEWS.txt PyShell.py rpc.py run.py 
Log Message:
1. Implement processing of user code in subprocess MainThread.  Pass loop
   is now interruptable on Windows.
2. Tweak signal.signal() wait parameters as called by various methods
   to improve I/O response, especially on Windows.
3. Debugger is disabled at this check-in pending further development.

M NEWS.txt
M PyShell.py
M rpc.py
M run.py


Index: NEWS.txt
===================================================================
RCS file: /cvsroot/idlefork/idle/NEWS.txt,v
retrieving revision 1.14
retrieving revision 1.15
diff -C2 -r1.14 -r1.15
*** NEWS.txt	25 Apr 2003 17:48:08 -0000	1.14
--- NEWS.txt	8 May 2003 20:26:54 -0000	1.15
***************
*** 6,10 ****
  ===================================
  
! *Release date: 25-Apr-2003*
  
  - Implemented the 'interrupt' extension module, which allows a subthread
--- 6,16 ----
  ===================================
  
! *Release date: XX-XXX-2003*
! 
! - Improved I/O response by tweaking the wait parameter in various
!   calls to signal.signal().
! 
! - Implemented a threaded subprocess which allows interrupting a pass 
!   loop in user code using the 'interrupt' extension.
  
  - Implemented the 'interrupt' extension module, which allows a subthread
***************
*** 37,45 ****
  - 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.
  
--- 43,50 ----
  - Known issues:
  
    + 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.  If you do manage to 
!     interrupt the interrupt, simply restart the shell.
    + Printing under some versions of Linux may be problematic.
  

Index: PyShell.py
===================================================================
RCS file: /cvsroot/idlefork/idle/PyShell.py,v
retrieving revision 1.59
retrieving revision 1.60
diff -C2 -r1.59 -r1.60
*** PyShell.py	22 Mar 2003 19:40:18 -0000	1.59
--- PyShell.py	8 May 2003 20:26:54 -0000	1.60
***************
*** 36,39 ****
--- 36,44 ----
  IDENTCHARS = string.ascii_letters + string.digits + "_"
  
+ try:
+     from signal import SIGTERM
+ except ImportError:
+     SIGTERM = 15
+ 
  # Change warnings module to write to sys.__stderr__
  try:
***************
*** 368,378 ****
                  pass
          # Kill subprocess, spawn a new one, accept connection.
!         try:
!             self.interrupt_subprocess()
!             self.shutdown_subprocess()
!             self.rpcclt.close()
!             os.wait()
!         except:
!             pass
          self.tkconsole.executing = False
          self.spawn_subprocess()
--- 373,378 ----
                  pass
          # Kill subprocess, spawn a new one, accept connection.
!         self.rpcclt.close()
!         self.unix_terminate()
          self.tkconsole.executing = False
          self.spawn_subprocess()
***************
*** 392,431 ****
              debug.load_breakpoints()
  
-     def __signal_interrupt(self):
-         try:
-             from signal import SIGINT
-         except ImportError:
-             SIGINT = 2
-         try:
-             os.kill(self.rpcpid, SIGINT)
-         except OSError:    # subprocess may have already exited
-             pass
- 
      def __request_interrupt(self):
!         try:
!             self.rpcclt.asynccall("exec", "interrupt_the_server", (), {})
!         except:
!             pass
  
      def interrupt_subprocess(self):
!         # XXX KBK 22Mar03 Use interrupt message on all platforms for now.
!         # XXX if hasattr(os, "kill"):
!         if False:
!             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 __request_shutdown(self):
!         try:
!             self.rpcclt.asynccall("exec", "shutdown_the_server", (), {})
!         except:
!             pass
  
!     def shutdown_subprocess(self):
!         t = threading.Thread(target=self.__request_shutdown)
!         t.start()
!         t.join()
  
      def transfer_path(self):
--- 392,420 ----
              debug.load_breakpoints()
  
      def __request_interrupt(self):
!         self.rpcclt.remotecall("exec", "interrupt_the_server", (), {})
  
      def interrupt_subprocess(self):
!         threading.Thread(target=self.__request_interrupt).start()
  
!     def kill_subprocess(self):
!         self.rpcclt.close()
!         self.unix_terminate()
!         self.tkconsole.executing = False
!         self.rpcclt = None
  
!     def unix_terminate(self):
!         "UNIX: make sure subprocess is terminated and collect status"
!         if hasattr(os, 'kill'):
!             try:
!                 os.kill(self.rpcpid, SIGTERM)
!             except OSError:
!                 # process already terminated:
!                 return
!             else:
!                 try:
!                     os.waitpid(self.rpcpid, 0)
!                 except OSError:
!                     return
  
      def transfer_path(self):
***************
*** 446,464 ****
              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)
          if response:
              self.tkconsole.resetoutput()
--- 435,447 ----
              return
          try:
!             response = clt.pollresponse(self.active_seq, wait=0.05)
!         except (EOFError, IOError, KeyboardInterrupt):
!             # lost connection or subprocess terminated itself, restart
!             # [the KBI is from rpc.SocketIO.handle_EOF()]
              if self.tkconsole.closing:
                  return
              response = None
              self.restart_subprocess()
              self.tkconsole.endexecuting()
          if response:
              self.tkconsole.resetoutput()
***************
*** 478,488 ****
              # we received a response to the currently active seq number:
              self.tkconsole.endexecuting()
! 
!     def kill_subprocess(self):
!         clt = self.rpcclt
!         if clt is not None:
!             self.shutdown_subprocess()
!             clt.close()
!         self.rpcclt = None
  
      debugger = None
--- 461,466 ----
              # we received a response to the currently active seq number:
              self.tkconsole.endexecuting()
!         # Reschedule myself in 50 ms
!         self.tkconsole.text.after(50, self.poll_subprocess)
  
      debugger = None
***************
*** 496,500 ****
      def remote_stack_viewer(self):
          import RemoteObjectBrowser
!         oid = self.rpcclt.remotecall("exec", "stackviewer", ("flist",), {})
          if oid is None:
              self.tkconsole.root.bell()
--- 474,478 ----
      def remote_stack_viewer(self):
          import RemoteObjectBrowser
!         oid = self.rpcclt.remotequeue("exec", "stackviewer", ("flist",), {})
          if oid is None:
              self.tkconsole.root.bell()
***************
*** 629,633 ****
              return 0
          if self.rpcclt:
!             self.rpcclt.remotecall("exec", "runcode", (code,), {})
          else:
              exec code in self.locals
--- 607,611 ----
              return 0
          if self.rpcclt:
!             self.rpcclt.remotequeue("exec", "runcode", (code,), {})
          else:
              exec code in self.locals
***************
*** 646,650 ****
          try:
              if not debugger and self.rpcclt is not None:
!                 self.active_seq = self.rpcclt.asynccall("exec", "runcode",
                                                          (code,), {})
              elif debugger:
--- 624,628 ----
          try:
              if not debugger and self.rpcclt is not None:
!                 self.active_seq = self.rpcclt.asyncqueue("exec", "runcode",
                                                          (code,), {})
              elif debugger:
***************
*** 713,717 ****
          text.bind("<<end-of-file>>", self.eof_callback)
          text.bind("<<open-stack-viewer>>", self.open_stack_viewer)
!         text.bind("<<toggle-debugger>>", self.toggle_debugger)
          text.bind("<<open-python-shell>>", self.flist.open_shell)
          text.bind("<<toggle-jit-stack-viewer>>", self.toggle_jit_stack_viewer)
--- 691,695 ----
          text.bind("<<end-of-file>>", self.eof_callback)
          text.bind("<<open-stack-viewer>>", self.open_stack_viewer)
!         ##text.bind("<<toggle-debugger>>", self.toggle_debugger)
          text.bind("<<open-python-shell>>", self.flist.open_shell)
          text.bind("<<toggle-jit-stack-viewer>>", self.toggle_jit_stack_viewer)
***************
*** 800,810 ****
          self.resetoutput()
          self.executing = 1
-         ##self._cancel_check = self.cancel_check
-         ##sys.settrace(self._cancel_check)
  
      def endexecuting(self):
          "Helper for ModifiedInterpreter"
-         ##sys.settrace(None)
-         ##self._cancel_check = None
          self.executing = 0
          self.canceled = 0
--- 778,784 ----
***************
*** 823,827 ****
              # interrupt the subprocess
              self.closing = True
-             self.cancel_callback()
              self.endexecuting()
          return EditorWindow.close(self)
--- 797,800 ----
***************
*** 1017,1037 ****
          line = line[:i]
          more = self.interp.runsource(line)
- 
-     def cancel_check(self, frame, what, args,
-                      dooneevent=tkinter.dooneevent,
-                      dontwait=tkinter.DONT_WAIT):
-         # Hack -- use the debugger hooks to be able to handle events
-         # and interrupt execution at any time.
-         # This slows execution down quite a bit, so you may want to
-         # disable this (by not calling settrace() in beginexecuting() and
-         # endexecuting() for full-bore (uninterruptable) speed.)
-         # XXX This should become a user option.
-         if self.canceled:
-             return
-         dooneevent(dontwait)
-         if self.canceled:
-             self.canceled = 0
-             raise KeyboardInterrupt
-         return self._cancel_check
  
      def open_stack_viewer(self, event=None):
--- 990,993 ----

Index: rpc.py
===================================================================
RCS file: /cvsroot/idlefork/idle/rpc.py,v
retrieving revision 1.22
retrieving revision 1.23
diff -C2 -r1.22 -r1.23
*** rpc.py	22 Mar 2003 20:11:14 -0000	1.22
--- rpc.py	8 May 2003 20:26:54 -0000	1.23
***************
*** 29,32 ****
--- 29,33 ----
  
  import sys
+ import os
  import socket
  import select
***************
*** 35,38 ****
--- 36,40 ----
  import cPickle as pickle
  import threading
+ import Queue
  import traceback
  import copy_reg
***************
*** 40,43 ****
--- 42,47 ----
  import marshal
  
+ import interrupt
+ 
  def unpickle_code(ms):
      co = marshal.loads(ms)
***************
*** 99,104 ****
          except SystemExit:
              raise
-         except EOFError:
-             pass
          except:
              erf = sys.__stderr__
--- 103,106 ----
***************
*** 111,119 ****
              print>>erf, '\n*** Unrecoverable, server exiting!'
              print>>erf, '-'*40
-             import os
              os._exit(0)
  
  
  objecttable = {}
  
  class SocketIO:
--- 113,124 ----
              print>>erf, '\n*** Unrecoverable, server exiting!'
              print>>erf, '-'*40
              os._exit(0)
  
+ #----------------- end class RPCServer --------------------
  
  objecttable = {}
+ request_queue = Queue.Queue(0)
+ response_queue = Queue.Queue(0)
+ 
  
  class SocketIO:
***************
*** 122,126 ****
  
      def __init__(self, sock, objtable=None, debugging=None):
!         self.mainthread = threading.currentThread()
          if debugging is not None:
              self.debugging = debugging
--- 127,131 ----
  
      def __init__(self, sock, objtable=None, debugging=None):
!         self.sockthread = threading.currentThread()
          if debugging is not None:
              self.debugging = debugging
***************
*** 129,136 ****
              objtable = objecttable
          self.objtable = objtable
-         self.cvar = threading.Condition()
          self.responses = {}
          self.cvars = {}
-         self.interrupted = False
  
      def close(self):
--- 134,139 ----
***************
*** 140,143 ****
--- 143,150 ----
              sock.close()
  
+     def exithook(self):
+         "override for specific exit action"
+         os._exit()
+ 
      def debug(self, *args):
          if not self.debugging:
***************
*** 157,161 ****
              pass
  
!     def localcall(self, request):
          self.debug("localcall:", request)
          try:
--- 164,168 ----
              pass
  
!     def localcall(self, seq, request):
          self.debug("localcall:", request)
          try:
***************
*** 163,167 ****
          except TypeError:
              return ("ERROR", "Bad request format")
-         assert how == "call"
          if not self.objtable.has_key(oid):
              return ("ERROR", "Unknown object id: %s" % `oid`)
--- 170,173 ----
***************
*** 179,190 ****
          method = getattr(obj, methodname)
          try:
!             ret = method(*args, **kwargs)
!             if isinstance(ret, RemoteObject):
!                 ret = remoteref(ret)
!             return ("OK", ret)
          except SystemExit:
              raise
          except socket.error:
!             pass
          except:
              self.debug("localcall:EXCEPTION")
--- 185,202 ----
          method = getattr(obj, methodname)
          try:
!             if how == 'CALL':
!                 ret = method(*args, **kwargs)
!                 if isinstance(ret, RemoteObject):
!                     ret = remoteref(ret)
!                 return ("OK", ret)
!             elif how == 'QUEUE':
!                 request_queue.put((seq, (method, args, kwargs)))
!                 return("QUEUED", None)
!             else:
!                 return ("ERROR", "Unsupported message type: %s" % how)
          except SystemExit:
              raise
          except socket.error:
!             raise
          except:
              self.debug("localcall:EXCEPTION")
***************
*** 194,215 ****
      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)
  
      def asynccall(self, oid, methodname, args, kwargs):
!         request = ("call", (oid, methodname, args, kwargs))
          seq = self.newseq()
          self.debug(("asynccall:%d:" % seq), oid, methodname, args, kwargs)
          self.putmessage((seq, request))
          return seq
  
      def asyncreturn(self, seq):
          self.debug("asyncreturn:%d:call getresponse(): " % seq)
!         response = self.getresponse(seq, wait=None)
          self.debug(("asyncreturn:%d:response: " % seq), response)
          return self.decoderesponse(response)
--- 206,240 ----
      def remotecall(self, oid, methodname, args, kwargs):
          self.debug("remotecall:asynccall: ", oid, methodname)
          seq = self.asynccall(oid, methodname, args, kwargs)
          return self.asyncreturn(seq)
  
+     def remotequeue(self, oid, methodname, args, kwargs):
+         self.debug("remotequeue:asyncqueue: ", oid, methodname)
+         seq = self.asyncqueue(oid, methodname, args, kwargs)
+         return self.asyncreturn(seq)
+ 
      def asynccall(self, oid, methodname, args, kwargs):
!         request = ("CALL", (oid, methodname, args, kwargs))
          seq = self.newseq()
+         if threading.currentThread() != self.sockthread:
+             cvar = threading.Condition()
+             self.cvars[seq] = cvar
          self.debug(("asynccall:%d:" % seq), oid, methodname, args, kwargs)
          self.putmessage((seq, request))
          return seq
  
+     def asyncqueue(self, oid, methodname, args, kwargs):
+         request = ("QUEUE", (oid, methodname, args, kwargs))
+         seq = self.newseq()
+         if threading.currentThread() != self.sockthread:
+             cvar = threading.Condition()
+             self.cvars[seq] = cvar
+         self.debug(("asyncqueue:%d:" % seq), oid, methodname, args, kwargs)
+         self.putmessage((seq, request))
+         return seq
+ 
      def asyncreturn(self, seq):
          self.debug("asyncreturn:%d:call getresponse(): " % seq)
!         response = self.getresponse(seq, wait=0.05)
          self.debug(("asyncreturn:%d:response: " % seq), response)
          return self.decoderesponse(response)
***************
*** 219,225 ****
--- 244,256 ----
          if how == "OK":
              return what
+         if how == "QUEUED":
+             return None
          if how == "EXCEPTION":
              self.debug("decoderesponse: EXCEPTION")
              return None
+         if how == "EOF":
+             self.debug("decoderesponse: EOF")
+             self.decode_interrupthook()
+             return None
          if how == "ERROR":
              self.debug("decoderesponse: Internal ERROR:", what)
***************
*** 227,241 ****
          raise SystemError, (how, what)
  
      def mainloop(self):
          """Listen on socket until I/O not ready or EOF
  
!         Main thread pollresponse() will loop looking for seq number None, which
          never comes, and exit on EOFError.
  
          """
          try:
!             self.getresponse(myseq=None, wait=None)
          except EOFError:
!             pass
  
      def getresponse(self, myseq, wait):
--- 258,277 ----
          raise SystemError, (how, what)
  
+     def decode_interrupthook(self):
+         ""
+         raise EOFError
+ 
      def mainloop(self):
          """Listen on socket until I/O not ready or EOF
  
!         pollresponse() will loop looking for seq number None, which
          never comes, and exit on EOFError.
  
          """
          try:
!             self.getresponse(myseq=None, wait=0.05)
          except EOFError:
!             self.debug("mainloop:return")
!             return
  
      def getresponse(self, myseq, wait):
***************
*** 257,263 ****
      def _getresponse(self, myseq, wait):
          self.debug("_getresponse:myseq:", myseq)
!         if threading.currentThread() is self.mainthread:
!             # Main thread: does all reading of requests or responses
!             # Loop here, blocking each time until socket is ready.
              while 1:
                  response = self.pollresponse(myseq, wait)
--- 293,298 ----
      def _getresponse(self, myseq, wait):
          self.debug("_getresponse:myseq:", myseq)
!         if threading.currentThread() is self.sockthread:
!             # this thread does all reading of requests or responses
              while 1:
                  response = self.pollresponse(myseq, wait)
***************
*** 265,277 ****
                      return response
          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
  
--- 300,314 ----
                      return response
          else:
!             # wait for notification from socket handling thread
!             cvar = self.cvars[myseq]
!             cvar.acquire()
              while not self.responses.has_key(myseq):
!                 cvar.wait()
              response = self.responses[myseq]
+             self.debug("_getresponse:%s: thread woke up: response: %s" %
+                        (myseq, response))
              del self.responses[myseq]
              del self.cvars[myseq]
!             cvar.release()
              return response
  
***************
*** 284,288 ****
          try:
              s = pickle.dumps(message)
!         except:
              print >>sys.__stderr__, "Cannot pickle:", `message`
              raise
--- 321,325 ----
          try:
              s = pickle.dumps(message)
!         except pickle.UnpicklingError:
              print >>sys.__stderr__, "Cannot pickle:", `message`
              raise
***************
*** 294,301 ****
                  # socket was closed
                  raise IOError
              else:
                  s = s[n:]
  
!     def ioready(self, wait=0.0):
          r, w, x = select.select([self.sock.fileno()], [], [], wait)
          return len(r)
--- 331,341 ----
                  # socket was closed
                  raise IOError
+             except socket.error:
+                 self.debug("putmessage:socketerror:pid:%s" % os.getpid())
+                 os._exit(0)
              else:
                  s = s[n:]
  
!     def ioready(self, wait):
          r, w, x = select.select([self.sock.fileno()], [], [], wait)
          return len(r)
***************
*** 305,309 ****
      bufstate = 0 # meaning: 0 => reading count; 1 => reading data
  
!     def pollpacket(self, wait=0.0):
          self._stage0()
          if len(self.buffer) < self.bufneed:
--- 345,349 ----
      bufstate = 0 # meaning: 0 => reading count; 1 => reading data
  
!     def pollpacket(self, wait):
          self._stage0()
          if len(self.buffer) < self.bufneed:
***************
*** 335,339 ****
              return packet
  
!     def pollmessage(self, wait=0.0):
          packet = self.pollpacket(wait)
          if packet is None:
--- 375,379 ----
              return packet
  
!     def pollmessage(self, wait):
          packet = self.pollpacket(wait)
          if packet is None:
***************
*** 349,391 ****
          return message
  
!     def pollresponse(self, myseq, wait=0.0):
          """Handle messages received on the socket.
  
!         Some messages received may be asynchronous 'call' commands, and
!         some may be responses intended for other threads.
  
!         Loop until message with myseq sequence number is received.  Save others
!         in self.responses and notify the owning thread, except that 'call'
!         commands are handed off to localcall() and the response sent back
!         across the link with the appropriate sequence number.
  
          """
          while 1:
!             message = self.pollmessage(wait)
!             if message is None:  # socket not ready
                  return None
-             #wait = 0.0  # poll on subsequent passes instead of blocking
              seq, resq = message
              self.debug("pollresponse:%d:myseq:%s" % (seq, myseq))
!             if resq[0] == "call":
                  self.debug("pollresponse:%d:localcall:call:" % seq)
!                 response = self.localcall(resq)
                  self.debug("pollresponse:%d:localcall:response:%s"
                             % (seq, response))
!                 self.putmessage((seq, response))
                  continue
              elif seq == myseq:
                  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
  
  #----------------- end class SocketIO --------------------
  
--- 389,483 ----
          return message
  
!     def pollresponse(self, myseq, wait):
          """Handle messages received on the socket.
  
!         Some messages received may be asynchronous 'call' or 'queue' requests,
!         and some may be responses for other threads.
! 
!         'call' requests are passed to self.localcall() with the expectation of
!         immediate execution, during which time the socket is not serviced.
  
!         'queue' requests are used for tasks (which may block or hang) to be
!         processed in a different thread.  These requests are fed into
!         request_queue by self.localcall().  Responses to queued requests are
!         taken from response_queue and sent across the link with the associated
!         sequence numbers.  Messages in the queues are (sequence_number,
!         request/response) tuples and code using this module removing messages
!         from the request_queue is responsible for returning the correct
!         sequence number in the response_queue.
! 
!         pollresponse() will loop until a response message with the myseq
!         sequence number is received, and will save other responses in
!         self.responses and notify the owning thread.
  
          """
          while 1:
!             # send queued response if there is one available
!             try:
!                 qmsg = response_queue.get(0)
!             except Queue.Empty:
!                 pass
!             else:
!                 seq, response = qmsg
!                 message = (seq, ('OK', response))
!                 self.putmessage(message)
!             # poll for message on link
!             try:
!                 message = self.pollmessage(wait)
!                 if message is None:  # socket not ready
!                     return None
!             except EOFError:
!                 self.handle_EOF()
!                 return None
!             except AttributeError:
                  return None
              seq, resq = message
+             how = resq[0]
              self.debug("pollresponse:%d:myseq:%s" % (seq, myseq))
!             # process or queue a request
!             if how in ("CALL", "QUEUE"):
                  self.debug("pollresponse:%d:localcall:call:" % seq)
!                 response = self.localcall(seq, resq)
                  self.debug("pollresponse:%d:localcall:response:%s"
                             % (seq, response))
!                 if how == "CALL":
!                     self.putmessage((seq, response))
!                 elif how == "QUEUE":
!                     # don't acknowledge the 'queue' request!
!                     pass
                  continue
+             # return if completed message transaction
              elif seq == myseq:
                  return resq
+             # must be a response for a different thread:
              else:
!                 cv = self.cvars.get(seq, None)
                  # response involving unknown sequence number is discarded,
!                 # probably intended for prior incarnation of server
                  if cv is not None:
+                     cv.acquire()
                      self.responses[seq] = resq
                      cv.notify()
!                     cv.release()
                  continue
  
+     def handle_EOF(self):
+         "action taken upon link being closed by peer"
+         self.EOFhook()
+         self.debug("handle_EOF")
+         for key in self.cvars:
+             cv = self.cvars[key]
+             cv.acquire()
+             self.responses[key] = ('EOF', None)
+             cv.notify()
+             cv.release()
+         interrupt.interrupt_main()
+         # call our (possibly overridden) exit function
+         self.exithook()
+ 
+     def EOFhook(self):
+         "Classes using rpc client/server can override to augment EOF action"
+         pass
+ 
  #----------------- end class SocketIO --------------------
  
***************
*** 466,470 ****
          if not self.__attributes.has_key(name):
              raise AttributeError, name
!     __getattr__.DebuggerStepThrough=1
  
      def __getattributes(self):
--- 558,563 ----
          if not self.__attributes.has_key(name):
              raise AttributeError, name
! 
!     __getattr__.DebuggerStepThrough = 1
  
      def __getattributes(self):

Index: run.py
===================================================================
RCS file: /cvsroot/idlefork/idle/run.py,v
retrieving revision 1.14
retrieving revision 1.15
diff -C2 -r1.14 -r1.15
*** run.py	22 Mar 2003 19:40:19 -0000	1.14
--- run.py	8 May 2003 20:26:55 -0000	1.15
***************
*** 1,3 ****
--- 1,4 ----
  import sys
+ import os
  import time
  import socket
***************
*** 21,30 ****
  # completion and exit flags:
  
- server = None                # RPCServer instance
- queue = Queue.Queue(0)
- execution_finished = False
  exit_requested = False
  
- 
  def main():
      """Start the Python execution server in a subprocess
--- 22,27 ----
***************
*** 45,50 ****
  
      """
-     global queue, execution_finished, exit_requested
- 
      port = 8833
      if sys.argv[1:]:
--- 42,45 ----
***************
*** 59,77 ****
          try:
              if exit_requested:
!                 sys.exit()
!             # XXX KBK 22Mar03 eventually check queue here!
!             pass
!             time.sleep(0.05)
          except KeyboardInterrupt:
-             ##execution_finished = True
              continue
  
  def manage_socket(address):
-     global server, exit_requested
- 
      for i in range(6):
          time.sleep(i)
          try:
!             server = rpc.RPCServer(address, MyHandler)
              break
          except socket.error, err:
--- 54,74 ----
          try:
              if exit_requested:
!                 os._exit(0)
!             try:
!                 seq, request = rpc.request_queue.get(0)
!             except Queue.Empty:
!                 time.sleep(0.05)
!                 continue
!             method, args, kwargs = request
!             ret = method(*args, **kwargs)
!             rpc.response_queue.put((seq, ret))
          except KeyboardInterrupt:
              continue
  
  def manage_socket(address):
      for i in range(6):
          time.sleep(i)
          try:
!             server = MyRPCServer(address, MyHandler)
              break
          except socket.error, err:
***************
*** 83,90 ****
--- 80,118 ----
      else:
          print>>sys.__stderr__, "\nConnection to Idle failed, exiting."
+         global exit_requested
          exit_requested = True
+         return
      server.handle_request() # A single request only
  
  
+ class MyRPCServer(rpc.RPCServer):
+ 
+     def handle_error(self, request, client_address):
+         """Override RPCServer method for IDLE
+ 
+         Interrupt the MainThread and exit server if link is dropped.
+ 
+         """
+         try:
+             raise
+         except SystemExit:
+             raise
+         except EOFError:
+             global exit_requested
+             exit_requested = True
+             interrupt.interrupt_main()
+         except:
+             erf = sys.__stderr__
+             print>>erf, '\n' + '-'*40
+             print>>erf, 'Unhandled server exception!'
+             print>>erf, 'Thread: %s' % threading.currentThread().getName()
+             print>>erf, 'Client Address: ', client_address
+             print>>erf, 'Request: ', repr(request)
+             traceback.print_exc(file=erf)
+             print>>erf, '\n*** Unrecoverable, server exiting!'
+             print>>erf, '-'*40
+             os._exit(0)
+ 
+ 
  class MyHandler(rpc.RPCHandler):
  
***************
*** 96,100 ****
          sys.stdout = self.get_remote_proxy("stdout")
          sys.stderr = self.get_remote_proxy("stderr")
!         rpc.RPCHandler.getresponse(self, myseq=None, wait=0.5)
  
  
--- 124,141 ----
          sys.stdout = self.get_remote_proxy("stdout")
          sys.stderr = self.get_remote_proxy("stderr")
!         rpc.RPCHandler.getresponse(self, myseq=None, wait=0.05)
! 
!     def exithook(self):
!         "override SocketIO method - wait for MainThread to shut us down"
!         while 1: pass
! 
!     def EOFhook(self):
!         "Override SocketIO method - terminate wait on callback and exit thread"
!         global exit_requested
!         exit_requested = True
! 
!     def decode_interrupthook(self):
!         "interrupt awakened thread"
!         interrupt.interrupt_main()
  
  
***************
*** 107,148 ****
  
      def runcode(self, code):
-         global queue, execution_finished
- 
-         execution_finished = False
-         queue.put(code)
-         # dequeue and run in subthread
-         self.runcode_from_queue()
-         while not execution_finished:
-             time.sleep(0.05)
- 
-     def runcode_from_queue(self):
-         global queue, execution_finished
- 
-         # poll until queue has code object, using threads, just block?
-         while True:
-             try:
-                 code = queue.get(0)
-                 break
-             except Queue.Empty:
-                 time.sleep(0.05)
          try:
              exec code in self.locals
          except:
!             self.flush_stdout()
!             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)
!             traceback.print_list(tbe, file=efile)
!             lines = traceback.format_exception_only(typ, val)
!             for line in lines:
!                 print>>efile, line,
!             execution_finished = True
          else:
              self.flush_stdout()
-             execution_finished = True
  
      def flush_stdout(self):
--- 148,175 ----
  
      def runcode(self, code):
          try:
              exec code in self.locals
          except:
!             try:
!                 if exit_requested:
!                     os._exit(0)
!                 self.flush_stdout()
!                 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", "threading.py",
!                            "RemoteDebugger.py", "bdb.py")
!                 self.cleanup_traceback(tbe, exclude)
!                 traceback.print_list(tbe, file=efile)
!                 lines = traceback.format_exception_only(typ, val)
!                 for line in lines:
!                     print>>efile, line,
!             except:
!                 sys.stderr = sys.__stderr__
!                 raise
          else:
              self.flush_stdout()
  
      def flush_stdout(self):
***************
*** 185,196 ****
  
      def interrupt_the_server(self):
-         self.rpchandler.interrupted = True
-         ##print>>sys.__stderr__, "** Interrupt main!"
          interrupt.interrupt_main()
- 
-     def shutdown_the_server(self):
-         global exit_requested
- 
-         exit_requested = True
  
      def start_the_debugger(self, gui_adap_oid):
--- 212,216 ----