# -*- 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 debug thread.
"""
import bdb
import types
import os
import traceback
import sys
from DebugProtocol import *
def printerr(s):
import sys
"""
Module function used for debugging the debug threads.
Arguments
s -- the data to be printed
"""
sys.__stderr__.write('%s\n' % str(s))
sys.__stderr__.flush()
class DebugThread(bdb.Bdb):
"""
Class implementing a debug thread.
It represents a thread in the python interpreter that we are tracing.
Provides simple wrapper methods around bdb for the 'owning' client to
call to step etc.
"""
def __init__(self, dbgClient, targ=None, args=None, kwargs=None, mainThread=0):
"""
Constructor
Arguments
dbgClient -- the owning client
targ -- the target method in the run thread
args -- arguments to be passed to the thread
kwargs -- arguments to be passed to the thread
mainThread -- 0 if this thread is not the mainscripts thread
"""
bdb.Bdb.__init__(self)
self._dbgClient = dbgClient
self._target = targ
self._args = args
self._kwargs = kwargs
self._dbgClient = dbgClient
self._mainThread = mainThread
# thread running tracks execution state of client code
# it will always be 0 for main thread as that is tracked
# by DebugClientThreads and Bdb...
self._threadRunning = 0
self.currentFrame = None # current frame we are at
self.ident = None # id of this thread.
self.stepFrame = None # frame that we are stepping in, can be different than currentFrame
#
# point breakpoints list at class level dictionary so that
# threads share breakpoints. If we ever want thread level
# breakpoints, then remove this and adjust DebugClient accordingly
# when a breakpoint request is sent.
# In addition, point the file name cache at the global client cache...
#
self.breaks = self._dbgClient.breakpoints
self.fncache = self._dbgClient.fncache
def set_ident(self, id):
"""
Public method to set the id for this thread.
Arguments
id -- id for this thread (int)
"""
self.ident = id
def get_ident(self):
"""
Public method to return the id for this thread.
Returns
the id of this thread (int)
"""
return self.ident
def getCurrentFrame(self):
"""
Public method to return the current frame.
Returns
the current frame
"""
return self.currentFrame
def step(self, traceMode):
"""
Public method to perform a step operation in this thread.
Arguments
tracemode -- if it is non-zero, then the step is a step into,
otherwise it is a step over.
"""
self.stepFrame = self.currentFrame
if traceMode:
self.currentFrame = None
self.set_step()
else:
self.set_next(self.currentFrame)
def stepOut(self):
"""
Public method to perform a step out of the current call.
"""
self.stepFrame = self.currentFrame
self.set_return(self.currentFrame)
def set_quit(self):
"""
Public method to quit thread.
It wraps call to bdb to clear the current frame properly.
"""
self.currentFrame = None
bdb.Bdb.set_quit(self)
def go(self):
"""
Public method to resume the thread.
It resumes the thread stopping only at breakpoints or exceptions.
"""
self.currentFrame = None
self.set_continue()
def traceThread(self):
"""
Private method to setup tracing for this thread.
"""
self.set_trace()
if not self._mainThread:
self.set_continue()
def bootstrap(self):
"""
Private method to bootstrap the thread.
It wraps the call to the user function to enable tracing
before hand.
"""
try:
try:
self._threadRunning = 1
self.traceThread()
apply(self._target, self._args, self._kwargs)
except bdb.BdbQuit:
pass
finally:
self._threadRunning = 0
self.quitting = 1
self._dbgClient.threadTerminated(self)
sys.settrace(None)
def trace_dispatch(self, frame, event, arg):
"""
Private method wraping the trace_dispatch of bdb.py.
It wraps the call to dispatch tracing into
bdb to make sure we have locked the client to prevent multiple
threads from entering the client event loop.
"""
try:
self._dbgClient.lockClient()
# if this thread came out of a lock, and we are quitting
# and we are still running, then get rid of tracing for this thread
# and raise a quit to abort the thread.
if self.quitting and self._threadRunning:
sys.settrace(None)
raise bdb.BdbQuit
retval = bdb.Bdb.trace_dispatch(self, frame, event, arg)
finally:
self._dbgClient.unlockClient()
return retval
def user_line(self,frame):
"""
Reimplemented to handle the program about to execute a particular line.
Arguments
frame -- the frame object
"""
line = frame.f_lineno
# We never stop an line 0.
if line == 0:
return
fn = self._dbgClient.absPath(self.fix_frame_filename(frame))
# See if we are skipping at the start of a newly loaded program.
if self._dbgClient.mainFrame is None:
if fn != self._dbgClient.getRunning():
return
self._dbgClient.mainFrame = frame
self.currentFrame = frame
fr = frame
stack = []
while fr is not None:
# Reset the trace function so we can be sure
# to trace all functions up the stack... This gets around
# problems where an exception/breakpoint has occurred
# but we had disabled tracing along the way via a None
# return from dispatch_call
fr.f_trace = self.trace_dispatch
fname = self._dbgClient.absPath(self.fix_frame_filename(fr))
fline = fr.f_lineno
ffunc = fr.f_code.co_name
if ffunc == '?':
ffunc = ''
stack.append((fname, fline, ffunc))
if fr == self._dbgClient.mainFrame:
fr = None
else:
fr = fr.f_back
self._dbgClient.write('%s%s\n' % (ResponseLine,str(stack)))
self._dbgClient.eventLoop()
def fix_frame_filename(self, frame):
"""
Protected method used to fixup the filename for a given frame.
The logic employed here is that if a module was loaded
from a .pyc file, then the correct .py to operate with
should be in the same path as the .pyc. The reason this
logic is needed is that when a .pyc file is generated, the
filename embedded and thus what is readable in the code object
of the frame object is the fully qualified filepath when the
pyc is generated. If files are moved from machine to machine
this can break debugging as the .pyc will refer to the .py
on the original machine. Another case might be sharing
code over a network... This logic deals with that.
Arguments
frame -- the frame object
"""
# get module name from __file__
if frame.f_globals.has_key('__file__'):
root, ext = os.path.splitext(frame.f_globals['__file__'])
if ext == '.pyc' or ext == '.py':
fixedName = root + '.py'
if os.path.exists(fixedName):
return fixedName
return frame.f_code.co_filename
def break_here(self, frame):
"""
Reimplemented from bdb.py to fix the filename from the frame.
See fix_frame_filename for more info.
Arguments
frame -- the frame object
Returns
flag indicating the break status (boolean)
"""
filename = self.canonic(self.fix_frame_filename(frame))
if not self.breaks.has_key(filename):
return 0
lineno = frame.f_lineno
if not lineno in self.breaks[filename]:
return 0
# flag says ok to delete temp. bp
(bp, flag) = bdb.effective(filename, lineno, frame)
if bp:
self.currentbp = bp.number
if (flag and bp.temporary):
self.do_clear(str(bp.number))
return 1
else:
return 0
def break_anywhere(self, frame):
"""
Reimplemented from bdb.py to fix the filename from the frame.
See fix_frame_filename for more info.
Arguments
frame -- the frame object
Returns
flag indicating the break status (boolean)
"""
return self.breaks.has_key(
self.canonic(self.fix_frame_filename(frame)))
def dispatch_return(self, frame, arg):
"""
Reimplemented from bdb.py to handle passive mode cleanly.
"""
if self.stop_here(frame) or frame == self.returnframe:
self.user_return(frame, arg)
if self.quitting and not self._dbgClient.passive:
raise bdb.BdbQuit
return self.trace_dispatch
def dispatch_exception(self, frame, arg):
"""
Reimplemented from bdb.py to always call user_exception.
"""
self.user_exception(frame, arg)
if self.quitting: raise bdb.BdbQuit
return self.trace_dispatch
def set_continue(self):
"""
Reimplemented from bdb.py to always get informed of exceptions.
"""
# Modified version of the one found in bdb.py
# Here we leave tracing switched on in order to get
# informed of exceptions
self.stopframe = self.botframe
self.returnframe = None
self.quitting = 0
def user_exception(self,frame,(exctype,excval,exctb),unhandled=0):
"""
Reimplemented to report an exception to the debug server.
Arguments
frame -- the frame object
exctype -- the type of the exception
excval -- data about the exception
exctb -- traceback for the exception
unhandled -- flag indicating an uncaught exception
"""
if exctype == SystemExit:
self._dbgClient.progTerminated(excval)
elif exctype in [SyntaxError, IndentationError]:
try:
message, (filename, linenr, charnr, text) = excval
except:
exclist = []
else:
exclist = [message, (filename, linenr, charnr)]
self._dbgClient.write("%s%s\n" % (ResponseSyntax, str(exclist)))
else:
self.currentFrame = frame
if type(exctype) == types.ClassType:
exctype = exctype.__name__
if excval is None:
excval = ''
if unhandled:
exclist = ["unhandled %s" % str(exctype), str(excval)]
else:
exclist = [str(exctype), str(excval)]
frlist = self.extract_stack(exctb)
frlist.reverse()
for fr in frlist:
filename = self._dbgClient.absPath(self.fix_frame_filename(fr))
linenr = fr.f_lineno
exclist.append((filename, linenr))
self._dbgClient.write("%s%s\n" % (ResponseException, str(exclist)))
self._dbgClient.eventLoop()
def extract_stack(self, exctb):
"""
Protected member to return a list of stack frames.
Arguments
exctb -- exception traceback
Returns
list of stack frames
"""
tb = exctb
stack = []
while tb is not None:
stack.append(tb.tb_frame)
tb = tb.tb_next
tb = None
return stack
def user_return(self,frame,retval):
"""
Reimplemented to report program termination to the debug server.
Arguments
frame -- the frame object
retval -- the return value of the program
"""
# The program has finished if we have just left the first frame.
if frame == self._dbgClient.mainFrame and \
self._mainThread:
self._dbgClient.progTerminated(retval)
elif frame is not self.stepFrame:
self.stepFrame = None
self.user_line(frame)
def stop_here(self,frame):
"""
Reimplemented to filter out debugger files.
Tracing is turned off for files that are part of the
debugger that are called from the application being debugged.
Arguments
frame -- the frame object
Returns
flag indicating whether the debugger should stop here
"""
fn = self.fix_frame_filename(frame)
# Eliminate things like <string> and <stdin>.
if fn[0] == '<':
return 0
#XXX - think of a better way to do this. It's only a convience for
#debugging the debugger - when the debugger code is in the current
#directory.
if os.path.basename(fn) in ['AsyncIO.py', 'DebugClientThreads.py', 'DebugThread.py']:
return 0
if self._dbgClient.shouldSkip(fn):
return 0
return bdb.Bdb.stop_here(self,frame)
|