mirror of
https://github.com/python/cpython.git
synced 2025-09-26 10:19:53 +00:00
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().
This commit is contained in:
parent
f927f14eda
commit
003091cd51
5 changed files with 149 additions and 63 deletions
|
@ -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?
|
||||||
|
|
||||||
- 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
|
||||||
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):
|
||||||
# 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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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:
|
||||||
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):
|
||||||
"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:
|
||||||
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:
|
||||||
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:
|
||||||
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:
|
||||||
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:
|
||||||
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:
|
||||||
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:
|
||||||
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:
|
||||||
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:
|
||||||
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)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue