Commit 003091cd authored by Kurt B. Kaiser's avatar Kurt B. Kaiser

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().
parent f927f14e
...@@ -2,6 +2,32 @@ ...@@ -2,6 +2,32 @@
IDLEfork NEWS 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? What's New in IDLEfork 0.9 Alpha 2?
=================================== ===================================
...@@ -105,15 +131,6 @@ 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. - 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? What's New in IDLEfork 0.9 Alpha 1?
=================================== ===================================
......
...@@ -8,6 +8,7 @@ import getopt ...@@ -8,6 +8,7 @@ import getopt
import re import re
import socket import socket
import time import time
import threading
import traceback import traceback
import types import types
import exceptions import exceptions
...@@ -361,9 +362,23 @@ class ModifiedInterpreter(InteractiveInterpreter): ...@@ -361,9 +362,23 @@ class ModifiedInterpreter(InteractiveInterpreter):
# close only the subprocess debugger # close only the subprocess debugger
debug = self.getdebugger() debug = self.getdebugger()
if debug: if debug:
RemoteDebugger.close_subprocess_debugger(self.rpcclt) try:
# kill subprocess, spawn a new one, accept connection RemoteDebugger.close_subprocess_debugger(self.rpcclt)
self.rpcclt.close() 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.spawn_subprocess()
self.rpcclt.accept() self.rpcclt.accept()
self.transfer_path() self.transfer_path()
...@@ -374,12 +389,30 @@ class ModifiedInterpreter(InteractiveInterpreter): ...@@ -374,12 +389,30 @@ class ModifiedInterpreter(InteractiveInterpreter):
console.write(halfbar + ' RESTART ' + halfbar) console.write(halfbar + ' RESTART ' + halfbar)
console.text.mark_set("restart", "end-1c") console.text.mark_set("restart", "end-1c")
console.text.mark_gravity("restart", "left") console.text.mark_gravity("restart", "left")
# restart remote debugger # restart subprocess debugger
if debug: if debug:
gui = RemoteDebugger.restart_subprocess_debugger(self.rpcclt) gui = RemoteDebugger.restart_subprocess_debugger(self.rpcclt)
# reload remote debugger breakpoints for all PyShellEditWindows # reload remote debugger breakpoints for all PyShellEditWindows
debug.load_breakpoints() 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): def transfer_path(self):
self.runcommand("""if 1: self.runcommand("""if 1:
import sys as _sys import sys as _sys
...@@ -393,7 +426,20 @@ class ModifiedInterpreter(InteractiveInterpreter): ...@@ -393,7 +426,20 @@ class ModifiedInterpreter(InteractiveInterpreter):
clt = self.rpcclt clt = self.rpcclt
if clt is None: if clt is None:
return 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 # Reschedule myself in 50 ms
self.tkconsole.text.after(50, self.poll_subprocess) self.tkconsole.text.after(50, self.poll_subprocess)
if response: if response:
...@@ -571,8 +617,7 @@ class ModifiedInterpreter(InteractiveInterpreter): ...@@ -571,8 +617,7 @@ class ModifiedInterpreter(InteractiveInterpreter):
def runcode(self, code): def runcode(self, code):
"Override base class method" "Override base class method"
if self.tkconsole.executing: if self.tkconsole.executing:
self.display_executing_dialog() self.interp.restart_subprocess()
return
self.checklinecache() self.checklinecache()
if self.save_warnings_filters is not None: if self.save_warnings_filters is not None:
warnings.filters[:] = self.save_warnings_filters warnings.filters[:] = self.save_warnings_filters
...@@ -670,10 +715,11 @@ class PyShell(OutputWindow): ...@@ -670,10 +715,11 @@ class PyShell(OutputWindow):
if use_subprocess: if use_subprocess:
self.interp.start_subprocess() self.interp.start_subprocess()
reading = 0 reading = False
executing = 0 executing = False
canceled = 0 canceled = False
endoffile = 0 endoffile = False
closing = False
def toggle_debugger(self, event=None): def toggle_debugger(self, event=None):
if self.executing: if self.executing:
...@@ -748,17 +794,17 @@ class PyShell(OutputWindow): ...@@ -748,17 +794,17 @@ class PyShell(OutputWindow):
def close(self): def close(self):
"Extend EditorWindow.close()" "Extend EditorWindow.close()"
if self.executing: if self.executing:
# XXX Need to ask a question here response = tkMessageBox.askokcancel(
if not tkMessageBox.askokcancel(
"Kill?", "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", default="ok",
master=self.text): master=self.text)
if response == False:
return "cancel" return "cancel"
self.canceled = 1 # interrupt the subprocess
if self.reading: self.closing = True
self.top.quit() self.cancel_callback()
return "cancel" self.endexecuting()
return EditorWindow.close(self) return EditorWindow.close(self)
def _close(self): def _close(self):
...@@ -819,7 +865,7 @@ class PyShell(OutputWindow): ...@@ -819,7 +865,7 @@ class PyShell(OutputWindow):
def isatty(self): def isatty(self):
return True return True
def cancel_callback(self, event): def cancel_callback(self, event=None):
try: try:
if self.text.compare("sel.first", "!=", "sel.last"): if self.text.compare("sel.first", "!=", "sel.last"):
return # Active selection -- always use default binding return # Active selection -- always use default binding
...@@ -831,18 +877,11 @@ class PyShell(OutputWindow): ...@@ -831,18 +877,11 @@ class PyShell(OutputWindow):
self.showprompt() self.showprompt()
return "break" return "break"
self.endoffile = 0 self.endoffile = 0
self.canceled = 1
if self.reading: if self.reading:
self.canceled = 1
self.top.quit() self.top.quit()
elif (self.executing and self.interp.rpcclt and elif (self.executing and self.interp.rpcclt):
self.interp.rpcpid and hasattr(os, "kill")): self.interp.interrupt_subprocess()
try:
from signal import SIGINT
except ImportError:
SIGINT = 2
os.kill(self.interp.rpcpid, SIGINT)
else:
self.canceled = 1
return "break" return "break"
def eof_callback(self, event): def eof_callback(self, event):
...@@ -1020,12 +1059,14 @@ class PyShell(OutputWindow): ...@@ -1020,12 +1059,14 @@ class PyShell(OutputWindow):
sys.stdout.softspace = 0 sys.stdout.softspace = 0
def write(self, s, tags=()): def write(self, s, tags=()):
self.text.mark_gravity("iomark", "right") try:
OutputWindow.write(self, s, tags, "iomark") self.text.mark_gravity("iomark", "right")
self.text.mark_gravity("iomark", "left") OutputWindow.write(self, s, tags, "iomark")
self.text.mark_gravity("iomark", "left")
except:
pass
if self.canceled: if self.canceled:
self.canceled = 0 self.canceled = 0
raise KeyboardInterrupt
class PseudoFile: class PseudoFile:
......
...@@ -124,9 +124,6 @@ class ScriptBinding: ...@@ -124,9 +124,6 @@ class ScriptBinding:
flist = self.editwin.flist flist = self.editwin.flist
shell = flist.open_shell() shell = flist.open_shell()
interp = shell.interp interp = shell.interp
if interp.tkconsole.executing:
interp.display_executing_dialog()
return
interp.restart_subprocess() interp.restart_subprocess()
# XXX Too often this discards arguments the user just set... # XXX Too often this discards arguments the user just set...
interp.runcommand("""if 1: interp.runcommand("""if 1:
......
...@@ -86,6 +86,16 @@ class RPCServer(SocketServer.TCPServer): ...@@ -86,6 +86,16 @@ class RPCServer(SocketServer.TCPServer):
"Override TCPServer method, return already connected socket" "Override TCPServer method, return already connected socket"
return self.socket, self.server_address 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 = {} objecttable = {}
class SocketIO: class SocketIO:
...@@ -100,9 +110,10 @@ class SocketIO: ...@@ -100,9 +110,10 @@ class SocketIO:
if objtable is None: if objtable is None:
objtable = objecttable objtable = objecttable
self.objtable = objtable self.objtable = objtable
self.statelock = threading.Lock() self.cvar = threading.Condition()
self.responses = {} self.responses = {}
self.cvars = {} self.cvars = {}
self.interrupted = False
def close(self): def close(self):
sock = self.sock sock = self.sock
...@@ -153,13 +164,16 @@ class SocketIO: ...@@ -153,13 +164,16 @@ class SocketIO:
if isinstance(ret, RemoteObject): if isinstance(ret, RemoteObject):
ret = remoteref(ret) ret = remoteref(ret)
return ("OK", ret) return ("OK", ret)
except SystemExit:
raise
except: except:
self.debug("localcall:EXCEPTION") self.debug("localcall:EXCEPTION")
if self.debugging: traceback.print_exc(file=sys.__stderr__)
efile = sys.stderr efile = sys.stderr
typ, val, tb = info = sys.exc_info() typ, val, tb = info = sys.exc_info()
sys.last_type, sys.last_value, sys.last_traceback = info sys.last_type, sys.last_value, sys.last_traceback = info
tbe = traceback.extract_tb(tb) 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") exclude = ("run.py", "rpc.py", "RemoteDebugger.py", "bdb.py")
self.cleanup_traceback(tbe, exclude) self.cleanup_traceback(tbe, exclude)
traceback.print_list(tbe, file=efile) traceback.print_list(tbe, file=efile)
...@@ -186,9 +200,9 @@ class SocketIO: ...@@ -186,9 +200,9 @@ class SocketIO:
break break
del tb[-1] del tb[-1]
if len(tb) == 0: if len(tb) == 0:
# error was in RPC internals, don't prune! # exception was in RPC internals, don't prune!
tb[:] = orig_tb[:] tb[:] = orig_tb[:]
print>>sys.stderr, "** RPC Internal Error: ", tb print>>sys.stderr, "** IDLE RPC Internal Exception: "
for i in range(len(tb)): for i in range(len(tb)):
fn, ln, nm, line = tb[i] fn, ln, nm, line = tb[i]
if nm == '?': if nm == '?':
...@@ -199,7 +213,12 @@ class SocketIO: ...@@ -199,7 +213,12 @@ class SocketIO:
tb[i] = fn, ln, nm, line tb[i] = fn, ln, nm, line
def remotecall(self, oid, methodname, args, kwargs): 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) seq = self.asynccall(oid, methodname, args, kwargs)
return self.asyncreturn(seq) return self.asyncreturn(seq)
...@@ -221,7 +240,8 @@ class SocketIO: ...@@ -221,7 +240,8 @@ class SocketIO:
if how == "OK": if how == "OK":
return what return what
if how == "EXCEPTION": if how == "EXCEPTION":
raise Exception, "RPC SocketIO.decoderesponse exception" self.debug("decoderesponse: EXCEPTION")
return None
if how == "ERROR": if how == "ERROR":
self.debug("decoderesponse: Internal ERROR:", what) self.debug("decoderesponse: Internal ERROR:", what)
raise RuntimeError, what raise RuntimeError, what
...@@ -266,16 +286,15 @@ class SocketIO: ...@@ -266,16 +286,15 @@ class SocketIO:
return response return response
else: else:
# Auxiliary thread: wait for notification from main thread # Auxiliary thread: wait for notification from main thread
cvar = threading.Condition(self.statelock) self.cvar.acquire()
self.statelock.acquire() self.cvars[myseq] = self.cvar
self.cvars[myseq] = cvar
while not self.responses.has_key(myseq): while not self.responses.has_key(myseq):
cvar.wait() self.cvar.wait()
response = self.responses[myseq] response = self.responses[myseq]
del self.responses[myseq] del self.responses[myseq]
del self.cvars[myseq] del self.cvars[myseq]
self.statelock.release() self.cvar.release()
return response # might be None return response
def newseq(self): def newseq(self):
self.nextseq = seq = self.nextseq + 2 self.nextseq = seq = self.nextseq + 2
...@@ -290,8 +309,13 @@ class SocketIO: ...@@ -290,8 +309,13 @@ class SocketIO:
raise raise
s = struct.pack("<i", len(s)) + s s = struct.pack("<i", len(s)) + s
while len(s) > 0: while len(s) > 0:
n = self.sock.send(s) try:
s = s[n:] n = self.sock.send(s)
except AttributeError:
# socket was closed
raise IOError
else:
s = s[n:]
def ioready(self, wait=0.0): def ioready(self, wait=0.0):
r, w, x = select.select([self.sock.fileno()], [], [], wait) r, w, x = select.select([self.sock.fileno()], [], [], wait)
...@@ -374,12 +398,14 @@ class SocketIO: ...@@ -374,12 +398,14 @@ class SocketIO:
elif seq == myseq: elif seq == myseq:
return resq return resq
else: else:
self.statelock.acquire() self.cvar.acquire()
self.responses[seq] = resq
cv = self.cvars.get(seq) cv = self.cvars.get(seq)
# response involving unknown sequence number is discarded,
# probably intended for prior incarnation
if cv is not None: if cv is not None:
self.responses[seq] = resq
cv.notify() cv.notify()
self.statelock.release() self.cvar.release()
continue continue
#----------------- end class SocketIO -------------------- #----------------- end class SocketIO --------------------
......
...@@ -71,6 +71,11 @@ class Executive: ...@@ -71,6 +71,11 @@ class Executive:
def runcode(self, code): def runcode(self, code):
exec code in self.locals 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): def start_the_debugger(self, gui_adap_oid):
return RemoteDebugger.start_debugger(self.rpchandler, gui_adap_oid) return RemoteDebugger.start_debugger(self.rpchandler, gui_adap_oid)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment