--- threaddbg.py	2006/08/07 02:25:13	1.3
+++ threaddbg.py	2006/08/19 20:57:38
@@ -9,9 +9,23 @@
 ### - 
 
 import bdb, inspect, os, pydb, sys
-
+from bdb import Breakpoint, checkfuncname
+from pydb.pydbbdb import checkline
 import thread, threading
 
+class TBreakpoint(Breakpoint):
+
+    """Per-thread Breakpoint class."""
+
+    next = 1
+    bplist = {}
+    bpbynumber = [None]
+
+    def __init__(self, file, line, temporary=0, cond=None, funcname=None,
+                 threadname=None):
+        Breakpoint.__init__(self, file, line, temporary, cond, funcname)
+        self.threadname = threadname
+    
 class threadDbg(pydb.Pdb):
 
     def __init__(self, completekey='tab', stdin=None, stdout=None):
@@ -20,6 +34,8 @@
         self.add_hook()
         self.stack = self.curframe = self.botframe = None
 
+        self.t_breaks = {'MainThread': {}}
+
         # desired_thread is the thread we want to switch to after issuing
         # a "thread <thread-name>" command.
         self.desired_thread=None
@@ -193,6 +209,7 @@
         # Record in my own table a list of thread names
         if not thread_name in self.traced.keys():
             self.traced[thread_name] = thread.get_ident()
+            self.t_breaks[thread_name] = {}
 
         # See if there was a request to switch to a specific thread
         while self.desired_thread not in (None,
@@ -205,11 +222,9 @@
         self.threading_lock.acquire()
 
         if self.desired_thread != None:
-            print "Hey we just did a switch to %s!" % self.desired_thread
             self.threading_cond.acquire()
             self.threading_cond.notifyAll()
             self.threading_cond.release()
-            print "notify done from %s" % threading.currentThread().getName()
 
         self.desired_thread = None
 
@@ -256,3 +271,249 @@
         statement = 'execfile( "%s")' % filename
         self.run(statement, globals=globals_, locals=locals_)
 
+    def info_breakpoints(self, arg):
+        threadname = threading.currentThread().getName()
+        if not self.breaks and not self.t_breaks[threadname]:
+            self.msg('No breakpoints')
+            return
+
+        have_breaks = False
+        if self.breaks:
+            pydb.Pdb.info_breakpoints(self, arg)
+            have_breaks = True 
+        if self.t_breaks[threadname]:
+            self.msg("\nThread-specific breakpoints:")
+            self.msg("Num Type          Disp Enb    Where                       Thread")
+            for bp in TBreakpoint.bpbynumber:
+                if bp:
+                    self.tbpprint(bp)
+        else:
+            if not have_breaks: self.msg("No breakpoints.")
+
+    info_breakpoints.__doc__ = pydb.Pdb.info_breakpoints.__doc__
+
+    def set_break(self, filename, lineno, temporary=0, cond=None, funcname=None,
+                  threadname=None):
+        if threadname is None:
+            pydb.Pdb.set_break(self, filename, lineno, temporary, cond, funcname)
+        else:
+            filename = self.canonic(filename)
+            import linecache
+            line = linecache.getline(filename, lineno)
+            if not line:
+                return 'Line %s:%d does not exist' % (filename, lineno)
+            
+            if not filename in self.t_breaks:
+                self.t_breaks[threadname][filename] = []
+            blist = self.t_breaks[threadname][filename]
+            if not lineno in blist:
+                blist.append(lineno)
+            bp = TBreakpoint(filename, lineno, temporary, cond, funcname, threadname)
+
+    def do_break(self, arg, temporary=0):
+        if 'thread' not in arg:
+            # We only deal with thread-spefic breakpoints. pydb handles
+            # all others.
+            pydb.Pdb.do_break(self, arg)
+            return
+
+        if not self.curframe:
+            self.msg("No stack.")
+            return
+        # Pull the 'thread' command and threadname out of the args
+        threadname = arg[arg.find('thread')+7:]
+        print threadname.__repr__()
+        arg = arg[:arg.find('thread')-1]
+        cond = None
+        funcname = None
+        if not arg:
+            if self.lineno is None:
+                lineno = max(1, inspect.getlineno(self.curframe))
+            else:
+                lineno = self.lineno + 1
+            filename = self.curframe.f_code.co_filename
+        else:
+            filename = None
+            lineno = None
+            comma = arg.find(',')
+            if comma > 0:  
+                cond = arg[comma+1:].lstrip() 
+                arg = arg[:comma].rstrip()
+            (funcname, filename, lineno) = self.parse_fileops(arg)
+    
+        if not filename:
+            filename = self.defaultFile()
+
+        line = checkline(self, filename, lineno)
+        if line:
+            try:
+                err = self.set_break(filename, line, temporary, cond, funcname, threadname)
+            except TypeError:
+                err = self.set_break(filename, line, temporary, cond, threadname)
+
+            if err: self.errmsg(err)
+            else:
+                bp = self.get_breaks(filename, line, threadname)[-1]
+                self.msg("Breakpoint %d set in file %s, line %d, thread %s."
+                         % (bp.number, self.filename(bp.file), bp.line, bp.threadname))
+
+    do_break.__doc__ = pydb.Pdb.do_break.__doc__
+
+    # XXX This is an _exact_ copy of __parse_fileops from gdb.py
+    # It's only included because of the name mangling
+    def parse_fileops(self, arg):
+        colon = arg.find(':')
+        if colon >= 0:
+            filename = arg[:colon].rstrip()
+            f = self.lookupmodule(filename)
+            if not f:
+                self.errmsg("%s not found on sys.path" %
+                            self.saferepr(filename))
+                return (None, None, None)
+            else:
+                filename = f
+                arg = arg[colon+1:].lstrip()
+            try:
+                lineno = int(arg)
+            except TypeError:
+                self.errmsg("Bad lineno: %s", str(arg))
+                return (None, filename, None)
+            return (None, filename, lineno)
+        else:
+            # no colon: can be lineno or function
+            return self.get_brkpt_lineno(arg)
+
+    # Likewise, this method is only included because of the namespace mangling
+    def get_brkpt_lineno(self, arg):
+        funcname, filename = (None, None)
+        try:
+            lineno = int(arg)
+            filename = self.curframe.f_code.co_filename
+        except ValueError:
+            try:
+                func = eval(arg, self.curframe.f_globals,
+                            self.curframe.f_locals)
+            except:
+                func = arg
+            try:
+                if hasattr(func, 'im_func'):
+                    func = func.im_func
+                code = func.func_code
+                lineno = code.co_firstlineno
+                filename = code.co_filename 
+            except:
+                (ok, filename, ln) = self.lineinfo(arg)
+                if not ok:
+                    self.errmsg(('The specified object %s is not'
+                                +' a fucntion, or not found'
+                                +' along sys.path or not line given.')
+                                % str(repr(arg)))
+                    return (None, None, None)
+                funcname = ok
+                lineno = int(ln)
+        return (funcname, filename, lineno)
+
+    def get_breaks(self, filename, lineno, threadname=None):
+        if threadname is None:
+            return pydb.Pdb.get_breaks(self, filename, lineno)
+        filename = self.canonic(filename)
+        return filename in self.t_breaks[threadname] and \
+            lineno in self.t_breaks[threadname][filename] and \
+            TBreakpoint.bplist[filename, lineno] or []
+
+    def tbpprint(self, bp, out=None):
+        if bp.temporary:      
+            disp = 'del  '    
+        else:
+            disp = 'keep '
+        if bp.enabled:
+            disp = disp + 'y  '            
+        else:
+            disp = disp + 'n  '            
+        self.msg('%-4dbreakpoint    %s at %s:%d in %s' %
+                 (bp.number, disp, self.filename(bp.file), bp.line, bp.threadname), out)
+        if bp.cond:
+            self.msg('\tstop only if %s' % (bp.cond))
+        if bp.ignore:
+            self.msg('\tignore next %d hits' % (bp.ignore), out)
+        if (bp.hits):
+            if (bp.hits > 1): ss = 's'     
+            else: ss = ''
+            self.msg('\tbreakpoint already hit %d time%s' %
+                     (bp.hits, ss), out)             
+
+    def break_here(self, frame):
+        if pydb.Pdb.break_here(self, frame):
+            return True
+
+        threadname = threading.currentThread().getName()
+        filename = self.canonic(frame.f_code.co_filename)
+        if not filename in self.t_breaks[threadname]:
+            return False
+        lineno = frame.f_lineno
+        if not lineno in self.t_breaks[threadname][filename]:
+            # The line itself has no breakpoint, but maybe the line is the
+            # first line of a function with breakpoint set by function name.
+            lineno = frame.f_code.co_firstlineno
+            if not lineno in self.t_breaks[threadname][filename]:
+                return False
+
+        # flag says ok to delete temp. bp
+        (bp, flag) = effective(filename, lineno, frame)
+        if bp:
+            self.currentbp = bp.number
+            if (flag and bp.temporary):
+                self.do_clear(str(bp.number))
+            return True
+        else:
+            return False
+
+def effective(file, line, frame):
+    """Determine which breakpoint for this file:line is to be acted upon.
+
+    Called only if we know there is a bpt at this
+    location.  Returns breakpoint that was triggered and a flag
+    that indicates if it is ok to delete a temporary bp.
+
+    """
+    possibles = TBreakpoint.bplist[file,line]
+    for i in range(0, len(possibles)):
+        b = possibles[i]
+        if b.enabled == 0:
+            continue
+        if not checkfuncname(b, frame):
+            continue
+        # Count every hit when bp is enabled
+        b.hits = b.hits + 1
+        if not b.cond:
+            # If unconditional, and ignoring,
+            # go on to next, else break
+            if b.ignore > 0:
+                b.ignore = b.ignore -1
+                continue
+            else:
+                # breakpoint and marker that's ok
+                # to delete if temporary
+                return (b,1)
+        else:
+            # Conditional bp.
+            # Ignore count applies only to those bpt hits where the
+            # condition evaluates to true.
+            try:
+                val = eval(b.cond, frame.f_globals,
+                    frame.f_locals)
+                if val:
+                    if b.ignore > 0:
+                        b.ignore = b.ignore -1
+                        # continue
+                    else:
+                        return (b,1)
+            except:
+                # if eval fails, most conservative
+                # thing is to stop on breakpoint
+                # regardless of ignore count.
+                # Don't delete temporary,
+                # as another hint to user.
+                return (b,0)
+    return (None, None)
+
