# -*- coding: utf-8 -*-
# Copyright (c) 2002, 2003 Detlev Offenbach <detlev@die-offenbachs.de>
# Copyright (c) 2000 Phil Thompson <phil@river-bank.demon.co.uk>
#
"""
Module implementing the multithreaded Qt version of the debug client.
"""
import sys
import socket
import select
import codeop
import traceback
import os
import string
import thread
from qt import PYSIGNAL
from DebugProtocol import *
from AsyncIO import *
from Config import ConfigVarTypeStrings
from DebugThread import DebugThread
import PyCoverage
import PyProfile
def _debugclient_start_new_thread(target, args, kwargs={}):
"""
Module function used to allow for debugging of multiple threads.
The way it works is that below, we reset thread._start_new_thread to
this function object. Thus, providing a hook for us to see when
threads are started. From here we forward the request onto the
DebugClient which will create a DebugThread object to allow tracing
of the thread then start up the thread.
See DebugClient.attachThread and DebugThread.DebugThread in
DebugThread.py
"""
if DebugClientInstance is not None:
return DebugClientInstance.attachThread(target, args, kwargs)
else:
return apply(_original_start_thread, args, kwargs)
# make thread hooks available to system
_original_start_thread = thread.start_new_thread
thread.start_new_thread = _debugclient_start_new_thread
DebugClientInstance = None
def printerr(s):
"""
Module function used for debugging the debug client.
Arguments
s -- the data to be printed
"""
sys.__stderr__.write('%s\n' % str(s))
sys.__stderr__.flush()
def DebugClientQAppHook():
"""
Module function called by PyQt when the QApplication instance has been created.
"""
if DebugClientInstance is not None:
AsyncIO.setNotifiers(DebugClientInstance)
# Make the hook available to PyQt.
try:
__builtins__.__dict__['__pyQtQAppHook__'] = DebugClientQAppHook
except:
import __main__
__main__.__builtins__.__dict__['__pyQtQAppHook__'] = DebugClientQAppHook
def DebugClientRawInput(prompt):
"""
Replacement for the standard raw_input builtin.
This function works with the Qt event loop.
"""
if DebugClientInstance is None:
return DebugClientOrigRawInput(prompt)
return DebugClientInstance.raw_input(prompt)
# Use our own raw_input().
try:
DebugClientOrigRawInput = __builtins__.__dict__['raw_input']
__builtins__.__dict__['raw_input'] = DebugClientRawInput
except:
DebugClientOrigRawInput = __main__.__builtins__.__dict__['raw_input']
__main__.__builtins__.__dict__['raw_input'] = DebugClientRawInput
class DebugClient(AsyncIO):
"""
Class implementing the client side of the debugger.
It provides access to the Python interpeter from a debugger running in another
process whether or not the Qt event loop is running.
The protocol between the debugger and the client assumes that there will be
a single source of debugger commands and a single source of Python
statements. Commands and statement are always exactly one line and may be
interspersed.
The protocol is as follows. First the client opens a connection to the
debugger and then sends a series of one line commands. A command is either
>Load<, >Step<, >StepInto<, ... or a Python statement. See DebugProtocol.py
for a listing of valid protocol tokens.
A Python statement consists of the statement to execute, followed (in a
separate line) by >OK?<. If the statement was incomplete then the response
is >Continue<. If there was an exception then the response is >Exception<.
Otherwise the response is >OK<. The reason for the >OK?< part is to
provide a sentinal (ie. the responding >OK<) after any possible output as a
result of executing the command.
The client may send any other lines at any other time which should be
interpreted as program output.
If the debugger closes the session there is no response from the client.
The client may close the session at any time as a result of the script
being debugged closing or crashing.
"""
def __init__(self):
"""
Constructor
"""
AsyncIO.__init__(self)
# multi-threading support
# NOTE: import threading here AFTER above hook, as threading cache's
# thread._start_new_thread.
from threading import RLock
# protection lock for synchronization
self.clientLock = RLock()
# dictionary of all threads running
self.threads = {}
# the "current" thread, basically the thread we are at a breakpoint for.
self.currentThread = None
# special objects representing the main scripts thread and frame
self.mainThread = None
self.mainFrame = None
# The context to run the debugged program in.
self.context = {'__name__' : '__main__'}
# The list of complete lines to execute.
self.buffer = ''
self.connect(self,PYSIGNAL('lineReady'),self.handleLine)
self.connect(self,PYSIGNAL('gotEOF'),self.sessionClose)
self.pendingResponse = ResponseOK
self.fncache = {}
self.dircache = []
# Global breakpoints.
# NOTE: this and above fncache is shared by all threads.
# See DebugThread.DebugThread.__init__ to see dictionary assignment.
self.breakpoints = {}
self.inRawMode = 0
self.mainProcStr = None # used for the passive mode
self.passive = 0 # used to indicate the passive mode
self.running = None
self.qt_gui_threadid = None
self.readstream = None
self.writestream = None
self.errorstream = None
# So much for portability.
if sys.platform == 'win32':
self.skipdir = sys.prefix
else:
self.skipdir = os.path.join(sys.prefix,'lib/python' + sys.version[0:3])
def attachThread(self, target=None, args=None, kwargs=None, mainThread=0):
"""
Public method to setup a thread for DebugClient to debug.
If mainThread is non-zero, then we are attaching to the already
started mainthread of the app and the rest of the args are ignored.
Arguments
target -- the start function of the target thread (i.e. the user code)
args -- arguments to pass to target
kwargs -- keyword arguments to pass to target
mainThread -- non-zero, if we are attaching to the already
started mainthread of the app
Returns
The identifier of the created thread
"""
try:
self.lockClient()
newThread = DebugThread(self, target, args, kwargs, mainThread)
ident = -1
if mainThread:
ident = thread.get_ident()
self.mainThread = newThread
else:
ident = _original_start_thread(newThread.bootstrap, ())
newThread.set_ident(ident)
self.threads[newThread.get_ident()] = newThread
finally:
self.unlockClient()
return ident
def threadTerminated(self, dbgThread):
"""
Public method called when a DebugThread has exited.
Arguments
dbgThread -- the DebugThread that has exited
"""
try:
self.lockClient()
try:
del self.threads[dbgThread.get_ident()]
except:
pass
finally:
self.unlockClient()
def raw_input(self,prompt):
"""
Public method to implement raw_input() using the event loop.
Arguments
prompt -- the prompt to be shown (string)
Returns
the entered string
"""
self.write("%s%s\n" % (ResponseRaw, prompt))
self.inRawMode = 1
self.eventLoop()
return self.rawLine
def lockClient(self):
"""
Public method to acquire the lock for this client.
"""
self.clientLock.acquire()
def unlockClient(self):
"""
Public method to release the lock for this client.
"""
self.clientLock.release()
def handleException(self):
"""
Private method called in the case of an exception
It ensures that the debug server is informed of the raised exception.
"""
self.pendingResponse = ResponseException
def sessionClose(self):
"""
Private method to close the session with the debugger and terminate.
"""
try:
self.set_quit()
except:
pass
# clean up asyncio.
self.disconnect()
# make sure we close down our end of the socket
# might be overkill as normally stdin, stdout and stderr
# SHOULD be closed on exit, but it does not hurt to do it here
self.readstream.close()
self.writestream.close()
self.errorstream.close()
# Ok, go away.
sys.exit()
def setCurrentThread(self, id):
"""
Private method to set the current thread.
Arguments
id -- the id the current thread should be set to.
"""
self.lockClient()
try:
if id is None:
self.currentThread = None
else:
self.currentThread = self.threads[id]
finally:
self.unlockClient()
def handleLine(self,line):
"""
Private method to handle the receipt of a complete line.
It first looks for a valid protocol token at the start of the line. Thereafter
it trys to execute the lines accumulated so far.
Arguments
line -- the received line
"""
# Remove any newline.
if line[-1] == '\n':
line = line[:-1]
##~ printerr(line) ##debug
eoc = line.find('<')
if eoc >= 0 and line[0] == '>':
# Get the command part and any argument.
cmd = line[:eoc + 1]
arg = line[eoc + 1:]
if cmd == RequestVariables:
frmnr, scope, filter = eval(arg)
self.dumpVariables(int(frmnr), int(scope), filter)
return
if cmd == RequestStep:
self.currentThread.step(1)
self.eventExit = 1
return
if cmd == RequestStepOver:
self.currentThread.step(0)
self.eventExit = 1
return
if cmd == RequestStepOut:
self.currentThread.stepOut()
self.eventExit = 1
return
if cmd == RequestStepQuit:
if self.passive:
self.progTerminated(42)
else:
self.set_quit()
self.eventExit = 1
return
if cmd == RequestContinue:
self.currentThread.go()
self.eventExit = 1
return
if cmd == RequestOK:
self.write(self.pendingResponse + '\n')
self.pendingResponse = ResponseOK
return
if cmd == RequestLoad:
self.fncache = {}
self.dircache = []
sys.argv = []
wd, fn, args = arg.split('|')
sys.argv.append(fn)
sys.argv.extend(args.split())
sys.path[0] = os.path.dirname(sys.argv[0])
if wd == '':
os.chdir(sys.path[0])
else:
os.chdir(wd)
self.running = sys.argv[0]
self.inRawMode = 0
self.threads.clear()
self.mainFrame = None
self.attachThread(mainThread = 1)
# set the system exception handling function to ensure, that
# we report on all unhandled exceptions
sys.excepthook = self.unhandled_exception
# This will eventually enter a local event loop.
# Note the use of backquotes to cause a repr of self.running. The
# need for this is on Windows os where backslash is the path separator.
# They will get inadvertantly stripped away during the eval causing IOErrors
# if self.running is passed as a normal str.
self.mainThread.run('execfile(' + `self.running` + ')',self.context)
return
if cmd == RequestRun:
sys.argv = []
wd, fn, args = arg.split('|')
sys.argv.append(fn)
sys.argv.extend(args.split())
sys.path[0] = os.path.dirname(sys.argv[0])
if wd == '':
os.chdir(sys.path[0])
else:
os.chdir(wd)
# set the system exception handling function to ensure, that
# we report on all unhandled exceptions
sys.excepthook = self.unhandled_exception
execfile(sys.argv[0], self.context)
return
if cmd == RequestCoverage:
sys.argv = []
wd, fn, args, erase = arg.split('|')
sys.argv.append(fn)
sys.argv.extend(args.split())
sys.path[0] = os.path.dirname(sys.argv[0])
if wd == '':
os.chdir(sys.path[0])
else:
os.chdir(wd)
# set the system exception handling function to ensure, that
# we report on all unhandled exceptions
sys.excepthook = self.unhandled_exception
# generate a coverage object
self.cover = PyCoverage.coverage(sys.argv[0])
# register an exit handler to save the collected data
try:
import atexit
atexit.register(self.cover.save)
except ImportError:
sys.exitfunc = self.cover.save
if int(erase):
self.cover.erase()
self.cover.start()
execfile(sys.argv[0], self.context)
return
if cmd == RequestProfile:
sys.argv = []
wd, fn, args, erase = arg.split('|')
sys.argv.append(fn)
sys.argv.extend(args.split())
sys.path[0] = os.path.dirname(sys.argv[0])
if wd == '':
os.chdir(sys.path[0])
else:
os.chdir(wd)
# set the system exception handling function to ensure, that
# we report on all unhandled exceptions
sys.excepthook = self.unhandled_exception
# generate a profile object
self.prof = PyProfile.PyProfile(sys.argv[0])
if int(erase):
self.prof.erase()
self.prof.run('execfile(' + `sys.argv[0]` + ',' + `self.context` + ')')
return
if cmd == RequestShutdown:
self.sessionClose()
return
if cmd == RequestBreak:
fn, line, set, cond = arg.split(',')
line = int(line)
set = int(set)
if set:
if cond == 'None':
cond = None
self.mainThread.set_break(fn,line, 0, cond)
else:
self.mainThread.clear_break(fn,line)
return
if cmd == RequestEval:
try:
currentFrame = self.currentThread.getCurrentFrame()
value = eval(arg, currentFrame.f_globals,
currentFrame.f_locals)
except:
# Report the exception and the traceback
try:
type, value, tb = sys.exc_info()
sys.last_type = type
sys.last_value = value
sys.last_traceback = tb
tblist = traceback.extract_tb(tb)
del tblist[:1]
list = traceback.format_list(tblist)
if list:
list.insert(0, "Traceback (innermost last):\n")
list[len(list):] = traceback.format_exception_only(type, value)
finally:
tblist = tb = None
map(self.write,list)
self.write(ResponseException + '\n')
else:
self.write(str(value) + '\n')
self.write(ResponseOK + '\n')
return
if cmd == RequestExec:
globals = self.currentThread().getCurrentFrame().f_globals
locals = self.currentThread().getCurrentFrame().f_locals
try:
code = compile(arg + '\n', '<stdin>', 'single')
exec code in globals, locals
except:
# Report the exception and the traceback
try:
type, value, tb = sys.exc_info()
sys.last_type = type
sys.last_value = value
sys.last_traceback = tb
tblist = traceback.extract_tb(tb)
del tblist[:1]
list = traceback.format_list(tblist)
if list:
list.insert(0, "Traceback (innermost last):\n")
list[len(list):] = traceback.format_exception_only(type, value)
finally:
tblist = tb = None
map(self.write,list)
self.write(ResponseException + '\n')
return
if cmd == RequestBanner:
self.write('%s%s\n' % (ResponseBanner,
str((sys.version, sys.platform, 'Qt-Version, threaded'))))
return
# If we are handling raw mode input then reset the mode and break out
# of the current event loop.
if self.inRawMode:
self.inRawMode = 0
self.rawLine = line
self.eventExit = 1
return
if self.buffer:
self.buffer = self.buffer + '\n' + line
else:
self.buffer = line
try:
code = codeop.compile_command(self.buffer,self.readstream.name)
except (OverflowError, SyntaxError, ValueError):
# Report the exception
sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
map(self.write,traceback.format_exception_only(sys.last_type,sys.last_value))
self.buffer = ''
self.handleException()
else:
if code is None:
self.pendingResponse = ResponseContinue
else:
self.buffer = ''
try:
if self.running is None:
exec code in self.context
else:
globals = self.currentFrame.f_globals
locals = self.currentFrame.f_locals
exec code in globals, locals
except SystemExit:
self.sessionClose()
except:
# Report the exception and the traceback
try:
type, value, tb = sys.exc_info()
sys.last_type = type
sys.last_value = value
sys.last_traceback = tb
tblist = traceback.extract_tb(tb)
del tblist[:1]
list = traceback.format_list(tblist)
if list:
list.insert(0, "Traceback (innermost last):\n")
list[len(list):] = traceback.format_exception_only(type, value)
finally:
tblist = tb = None
map(self.write,list)
self.handleException()
def write(self,s):
"""
Private method to write data to the output stream.
Arguments
s -- data to be written (string)
"""
self.writestream.write(s)
self.writestream.flush()
def interact(self):
"""
Private method to Interact with the debugger.
Returns
Never
"""
# Set the descriptors now and the notifiers when the QApplication
# instance is created.
global DebugClientInstance
self.setDescriptors(self.readstream,self.writestream)
DebugClientInstance = self
if not self.passive:
# At this point the Qt event loop isn't running, so simulate it.
self.eventLoop()
def eventLoop(self):
"""
Private method implementing our event loop.
"""
self.eventExit = None
# make sure we set the current thread appropriately
threadid = thread.get_ident()
self.setCurrentThread(threadid)
while self.eventExit is None:
wrdy = []
if AsyncPendingWrite(self.writestream):
wrdy.append(self.writestream)
if AsyncPendingWrite(self.errorstream):
wrdy.append(self.errorstream)
rrdy, wrdy, xrdy = select.select([self.readstream],wrdy,[])
if self.readstream in rrdy:
self.readReady(self.readstream.fileno())
if self.writestream in wrdy:
self.writeReady(self.writestream.fileno())
if self.errorstream in wrdy:
self.writeReady(self.errorstream.fileno())
self.setCurrentThread(None)
self.eventExit = None
def connectDebugger(self,port,remoteAddress=None):
"""
Public method to establish a session with the debugger.
It opens a network connection to the debugger, connects it to stdin,
stdout and stderr and saves these file objects in case the application
being debugged redirects them itself.
Arguments
port -- the port number to connect to (int)
remoteAddress -- the network address of the debug server host (string)
"""
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
if remoteAddress is None:
sock.connect((DebugAddress,port))
else:
sock.connect((remoteAddress,port))
sock.setblocking(0)
sys.stdin = AsyncFile(sock,sys.stdin.mode,sys.stdin.name)
sys.stdout = AsyncFile(sock,sys.stdout.mode,sys.stdout.name)
sys.stderr = AsyncFile(sock,sys.stderr.mode,sys.stderr.name)
# save the stream in case a program redirects them
self.readstream = sys.stdin
self.writestream = sys.stdout
self.errorstream = sys.stderr
# attach to the main thread here
self.attachThread(mainThread=1)
def absPath(self,fn):
"""
Private method to convert a filename to an absolute name.
sys.path is used as a set of possible prefixes. The name stays
relative if a file could not be found.
Arguments
fn -- filename (string)
Returns
the converted filename (string)
"""
if os.path.isabs(fn):
return fn
# Check the cache.
if self.fncache.has_key(fn):
return self.fncache[fn]
# Search sys.path.
for p in sys.path:
afn = os.path.abspath(os.path.join(p,fn))
if os.path.exists(afn):
self.fncache[fn] = afn
d = os.path.dirname(afn)
if (d not in sys.path) and (d not in self.dircache):
self.dircache.append(d)
return afn
# Search the additional directory cache
for p in self.dircache:
afn = os.path.abspath(os.path.join(p,fn))
if os.path.exists(afn):
self.fncache[fn] = afn
return afn
# Nothing found.
return fn
def shouldSkip(self, fn):
"""
Public method to check if a file should be skipped.
Arguments
fn -- filename to be checked
Returns
non-zero if fn represents a file we are 'skipping', zero otherwise.
"""
# Eliminate anything that is part of the Python installation.
#XXX - this should be a user option, or an extension of the meaning of
# 'step into', or a separate type of step altogether, or have a
#configurable set of ignored directories.
afn = self.absPath(fn)
if afn.find(self.skipdir) == 0:
return 1
return 0
def getRunning(self):
"""
Public method to return the main script we are currently running.
"""
return self.running
def set_quit(self):
"""
Private method to do a 'set' quit on all threads.
"""
try:
self.lockClient()
try:
for key in self.threads.keys():
self.threads[key].set_quit()
except:
pass
finally:
self.unlockClient()
def unhandled_exception(self, exctype, excval, exctb):
"""
Private method called to report an uncaught exception.
Arguments
exctype -- the type of the exception
excval -- data about the exception
exctb -- traceback for the exception
"""
self.mainThread.user_exception(None, (exctype,excval,exctb), 1)
def progTerminated(self,status):
"""
Private method to tell the debugger that the program has terminated.
Arguments
status -- the return status
"""
if status is None:
status = 0
else:
try:
int(status)
except:
status = 1
self.set_quit()
self.running = None
self.write('%s%d\n' % (ResponseExit,status))
def formatVariablesList(self, keylist, dict, filter = [], classdict = 0, prefix = ''):
"""
Private method to produce a formated variables list.
The dictionary passed in to it is scanned. If classdict is false,
it builds a list of all class instances in dict. If it is
true, we are formatting a class dictionary. In this case
we prepend prefix to the variable names. Variables are
only added to the list, if their type is not contained
in the filter list. The formated variables list (a list of
tuples of 3 values) and the list of class instances is returned.
Arguments
keylist -- keys of the dictionary
dict -- the dictionary to be scanned
filter -- the indices of variable types to be filtered. Variables are
only added to the list, if their type is not contained
in the filter list.
classdict -- boolean indicating the formating of a class or
module dictionary. If classdict is false,
it builds a list of all class instances in dict. If it is
true, we are formatting a class dictionary. In this case
we prepend prefix to the variable names.
prefix -- prefix to prepend to the variable names (string)
Returns
A tuple consisting of a list of formatted variables and a list of
class instances. Each variable entry is a tuple of three elements,
the variable name, its type and value.
"""
varlist = []
classes = []
for key in keylist:
# filter hidden attributes (filter #0)
if 0 in filter and str(key)[:2] == '__':
continue
# special handling for '__builtins__' (it's way too big)
if key == '__builtins__':
rvalue = '<module __builtin__ (built-in)>'
valtype = 'module'
else:
value = dict[key]
valtypestr = str(type(value))[1:-1]
if string.split(valtypestr,' ',1)[0] == 'class':
# handle new class type of python 2.2+
if ConfigVarTypeStrings.index('instance') in filter:
continue
valtype = valtypestr[7:-1]
classes.append(key)
else:
valtype = valtypestr[6:-1]
try:
if ConfigVarTypeStrings.index(valtype) in filter:
continue
if valtype in ['instance', 'module']:
classes.append(key)
except ValueError:
if ConfigVarTypeStrings.index('other') in filter:
continue
try:
rvalue = repr(value)
except:
rvalue = ''
if classdict:
key = prefix + '.' + key
varlist.append((key, valtype, rvalue))
return (varlist, classes)
def dumpVariables(self, frmnr, scope, filter):
"""
Private method to return the variables of a frame to the debug server.
Arguments
frmnr -- distance of frame reported on. 0 is the current frame (int)
scope -- 1 to report global variables, 0 for local variables (int)
filter -- the indices of variable types to be filtered (list of int)
"""
f = self.currentThread.getCurrentFrame()
while f is not None and frmnr > 0:
f = f.f_back
frmnr -= 1
if f is None:
return
if scope:
dict = f.f_globals
else:
dict = f.f_locals
if f.f_globals is f.f_locals:
scope = -1
varlist = [scope]
if scope != -1:
keylist = dict.keys()
(vlist, klist) = self.formatVariablesList(keylist, dict, filter)
varlist = varlist + vlist
if len(klist):
for key in klist:
cdict = None
try:
cdict = dict[key].__dict__
except:
try:
slv = dict[key].__slots__
cdict = {}
for v in slv:
exec 'cdict[v] = dict[key].%s' % v
except:
pass
if cdict is not None:
keylist = cdict.keys()
(vlist, clist) = \
self.formatVariablesList(keylist, cdict, filter, 1, key)
varlist = varlist + vlist
self.write('%s%s\n' % (ResponseVariables, str(varlist)))
def startDebugger(self, filename=None, host=None, port=None):
"""
Method used to start the remote debugger.
Arguments
filename -- the program to be debugged (string)
host -- hostname of the debug server (string)
port -- portnumber of the debug server (int)
"""
global debugClient
if host is None:
host = os.getenv('ERICHOST', 'localhost')
if port is None:
port = os.getenv('ERICPORT', 42424)
self.connectDebugger(port, socket.gethostbyname(host))
if filename is not None:
self.running = os.path.abspath(filename)
else:
try:
self.running = os.path.abspath(sys.argv[0])
except:
pass
self.passive = 1
self.write(PassiveStartup + '\n')
self.interact()
# setup the debugger variables
self.fncache = {}
self.dircache = []
self.inRawMode = 0
self.mainFrame = None
self.attachThread(mainThread=1)
# set the system exception handling function to ensure, that
# we report on all unhandled exceptions
sys.excepthook = self.unhandled_exception
# now start debugging
self.mainThread.set_trace()
# We are normally called by the debugger to execute directly.
if __name__ == '__main__':
try:
port = int(sys.argv[1])
except:
port = -1
try:
remoteAddress = sys.argv[2]
except:
remoteAddress = None
sys.argv = ['']
sys.path[0] = ''
debugClient = DebugClient()
if port >= 0:
debugClient.connectDebugger(port, remoteAddress)
debugClient.interact()
|