493 lines
17 KiB
Python
493 lines
17 KiB
Python
# The debugger.py file is open sourced as GNU GPL v2.
|
|
# License details found here: http://www.gnu.org/licenses/gpl-2.0.html
|
|
# The debugger.py contains methods from the Python built-in pdb debugger.
|
|
# It is also inspired, and contains code snippets from the GNU GPL v2 jpydbg remote debugging library plugin for jedit and netbeans.
|
|
|
|
import bdb
|
|
import sys
|
|
import os
|
|
import traceback
|
|
import socket
|
|
import types
|
|
import __builtin__
|
|
import json
|
|
import copy
|
|
|
|
|
|
# Server class heavily inspired by jpydbg, a GPL'd debugging framework for
|
|
# python that interfaces with java front-ends
|
|
# http://jpydbg.cvs.sourceforge.net/viewvc/jpydbg/jpydebugforge/src/python/jpydbg/dbgutils.py?view=markup
|
|
class Server:
|
|
def __init__(self, connection):
|
|
self._connection = connection
|
|
self._buffer = ''
|
|
|
|
def send_json(self, data):
|
|
res = json.dumps(data, indent=None)
|
|
try:
|
|
self._connection.send(res + '\n')
|
|
except socket.error:
|
|
#not much we can do here...maybe TODO...
|
|
pass
|
|
except IOError:
|
|
pass
|
|
|
|
def read_net_buffer(self):
|
|
""" reading on network socket """
|
|
try:
|
|
if (self._buffer.find('\n') != -1):
|
|
return self._buffer ; # buffer still contains commands
|
|
networkData = self._connection.recv(1024)
|
|
if not networkData: # capture network interuptions if any
|
|
return None
|
|
data = self._buffer + networkData
|
|
return data
|
|
except socket.error, (errno, strerror):
|
|
#print "recv interupted errno(%s) : %s" % (errno, strerror)
|
|
return None
|
|
|
|
def receive(self):
|
|
""" receive a command back """
|
|
data = self.read_net_buffer() ;
|
|
# data reception from Ip
|
|
while (data != None and data):
|
|
eocPos = data.find('\n')
|
|
nextPos = eocPos ;
|
|
while (nextPos < len(data) and \
|
|
(data[nextPos] == '\n' or data[nextPos] == '\r')): # ignore consecutive \n\r
|
|
nextPos = nextPos + 1
|
|
if (eocPos != -1): # full command received in buffer
|
|
self._buffer = data[nextPos:] # cleanup received command from buffer
|
|
returned = data[:eocPos]
|
|
if (returned[-1] == '\r'):
|
|
return returned[:-1]
|
|
return returned
|
|
data = self.read_net_buffer() ;
|
|
# returning None on Ip Exception
|
|
return None
|
|
|
|
def close(self):
|
|
self._connection.close()
|
|
|
|
|
|
# Debugger is inspired by pdb and jpydbg. Methods used, and modified. Mostly a big fork of jpydbg
|
|
class Debugger(bdb.Bdb):
|
|
def __init__(self, skip=None):
|
|
bdb.Bdb.__init__(self, skip=skip)
|
|
self._wait_for_mainpyfile = 0
|
|
self._debug_active = 0
|
|
self.exception_raised = 0
|
|
self._connection = None
|
|
self._socket = None
|
|
self.cmd = None
|
|
self.saved_stdout = None
|
|
self.saved_stderr = None
|
|
self.saved_stdin = None
|
|
|
|
def reset(self):
|
|
bdb.Bdb.reset(self)
|
|
self.forget()
|
|
|
|
#def close(self):
|
|
# if (self._connection):
|
|
# self._connection.close()
|
|
|
|
def forget(self):
|
|
self.lineno = None
|
|
self.stack = []
|
|
self.curindex = 0
|
|
self.curframe = None
|
|
|
|
def setup(self, f, t):
|
|
self.forget()
|
|
self.stack, self.curindex = self.get_stack(f, t)
|
|
self.curframe = self.stack[self.curindex][0]
|
|
# The f_locals dictionary is updated from the actual frame
|
|
# locals whenever the .f_locals accessor is called, so we
|
|
# cache it here to ensure that modifications are not overwritten.
|
|
self.curframe_locals = self.curframe.f_locals
|
|
|
|
def user_call(self, frame, argument_list):
|
|
name = frame.f_code.co_name
|
|
if not name:
|
|
name = '???'
|
|
fn = self.canonic(frame.f_code.co_filename)
|
|
#print "user_call"
|
|
if not fn:
|
|
fn = '???'
|
|
"""This method is called when there is the remote possibility
|
|
that we ever need to stop in this function."""
|
|
if self._wait_for_mainpyfile:
|
|
return
|
|
if self.stop_here(frame):
|
|
local_variables = self.get_variables("LOCALS", frame)
|
|
global_variables = self.get_variables("GLOBALS", frame)
|
|
#print self.stdout
|
|
ret = dict(cmd=__builtin__.str(self.cmd), method="user_call", fn=fn, name=name,
|
|
line_no=__builtin__.str(frame.f_lineno), locals=local_variables, globals=global_variables)
|
|
self._connection.send_json(ret)
|
|
self.interaction(frame, None)
|
|
|
|
def user_line(self, frame):
|
|
#print "user_line"
|
|
"""This function is called when we stop or break at this line."""
|
|
if self._wait_for_mainpyfile:
|
|
if (self.mainpyfile != self.canonic(frame.f_code.co_filename)
|
|
or frame.f_lineno <= 0):
|
|
return
|
|
self._wait_for_mainpyfile = 0
|
|
#if self.bp_commands(frame):
|
|
import linecache
|
|
name = frame.f_code.co_name
|
|
if not name:
|
|
name = '???'
|
|
fn = self.canonic(frame.f_code.co_filename)
|
|
if not fn:
|
|
fn = '???'
|
|
# populate info to client side
|
|
line = linecache.getline(fn, frame.f_lineno)
|
|
local_variables = self.get_variables("LOCALS", frame)
|
|
global_variables = self.get_variables("GLOBALS", frame)
|
|
ret = dict(cmd=__builtin__.str(self.cmd), fn=fn, name=name,
|
|
line=line, line_no=__builtin__.str(frame.f_lineno), locals=local_variables, globals=global_variables)
|
|
self._connection.send_json(ret)
|
|
self.interaction(frame, None)
|
|
|
|
def bp_commands(self, frame):
|
|
"""Call every command that was set for the current active breakpoint
|
|
(if there is one).
|
|
|
|
Returns True if the normal interaction function must be called,
|
|
False otherwise."""
|
|
# self.currentbp is set in bdb in Bdb.break_here if a breakpoint was hit
|
|
if getattr(self, "currentbp", False) and \
|
|
self.currentbp in self.commands:
|
|
currentbp = self.currentbp
|
|
self.currentbp = 0
|
|
lastcmd_back = self.lastcmd
|
|
self.setup(frame, None)
|
|
for line in self.commands[currentbp]:
|
|
self.onecmd(line)
|
|
self.lastcmd = lastcmd_back
|
|
#if not self.commands_silent[currentbp]:
|
|
#self.print_stack_entry(self.stack[self.curindex])
|
|
if self.commands_doprompt[currentbp]:
|
|
self.cmdloop()
|
|
self.forget()
|
|
return
|
|
return 1
|
|
|
|
def user_return(self, frame, return_value):
|
|
#print "user_return"
|
|
"""This function is called when a return trap is set here."""
|
|
if self._wait_for_mainpyfile:
|
|
return
|
|
import linecache
|
|
fn = self.canonic(frame.f_code.co_filename)
|
|
if not fn:
|
|
fn = '???'
|
|
frame.f_locals['__return__'] = return_value
|
|
line = linecache.getline(fn, frame.f_lineno)
|
|
local_variables = self.get_variables("LOCALS", frame)
|
|
global_variables = self.get_variables("GLOBALS", frame)
|
|
ret = dict(cmd=__builtin__.str(self.cmd), fn=fn, return_value=__builtin__.str(return_value),
|
|
line=line, line_no=__builtin__.str(frame.f_lineno), locals=local_variables, globals=global_variables)
|
|
self._connection.send_json(ret)
|
|
self.interaction(frame, None)
|
|
|
|
def user_exception(self, frame, exc_info):
|
|
#print "user_exception"
|
|
"""This function is called if an exception occurs,
|
|
but only if we are to stop at or just below this level."""
|
|
if self._wait_for_mainpyfile:
|
|
return
|
|
if self.cmd == "NEXT" or self.cmd == "STEP":
|
|
self.set_step()
|
|
sys.settrace(self.trace_dispatch)
|
|
else:
|
|
self.populate_exception(exc_info)
|
|
self.set_continue()
|
|
#self.interaction(frame, None)
|
|
|
|
def populate_exception(self, exc_info):
|
|
# self.trace("exception populated")
|
|
if (self.exceptionRaised == 0): # exception not yet processed
|
|
extype = exc_info[0]
|
|
details = exc_info[1]
|
|
|
|
# Deal With SystemExit in specific way to reflect debuggee's return
|
|
if issubclass(extype, SystemExit):
|
|
content = 'System Exit REQUESTED BY DEBUGGEE =' + str(details)
|
|
elif issubclass(extype, SyntaxError):
|
|
content = __builtin__.str(details)
|
|
error = details[0]
|
|
compd = details[1]
|
|
content = 'SOURCE:SYNTAXERROR:"' + \
|
|
__builtin__.str(compd[0]) + '":(' + \
|
|
__builtin__.str(compd[1]) + ',' + \
|
|
__builtin__.str(compd[2]) + \
|
|
')' + ':' + error
|
|
elif issubclass(extype, NameError):
|
|
content = 'SOURCE:NAMEERROR:' + __builtin__.str(details)
|
|
elif issubclass(extype, ImportError):
|
|
content = 'SOURCE::IMPORTERROR:' + __builtin__.str(details)
|
|
else:
|
|
content = __builtin__.str(details)
|
|
|
|
self.send_exception(content)
|
|
self.exception_raised = 1 # set ExceptionFlag On
|
|
|
|
def send_exception(self, exc_details):
|
|
ret = dict(cmd="EXCEPTION", content=exc_details)
|
|
self._connection.send_json(ret)
|
|
|
|
def interaction(self, frame, traceback):
|
|
self.setup(frame, traceback)
|
|
self.loop()
|
|
|
|
def loop(self):
|
|
while (self.wait_for_input(self._connection.receive())):
|
|
pass
|
|
|
|
def wait_for_input(self, input):
|
|
try:
|
|
cmd, arg = input.split("\t")
|
|
except:
|
|
cmd = input
|
|
arg = ''
|
|
#print cmd
|
|
#print arg
|
|
|
|
if cmd:
|
|
return self.handle_input(cmd, arg)
|
|
else:
|
|
#connection reset, let's setup a new listener
|
|
self._socket.close()
|
|
self.start_debugger()
|
|
return 1
|
|
|
|
def handle_input(self, cmd, arg):
|
|
if cmd == "DEBUG":
|
|
self.start_debug(arg)
|
|
return 1
|
|
elif cmd == "NEXT":
|
|
return self.next()
|
|
elif cmd == "STEP":
|
|
return self.step()
|
|
elif cmd == "RUN":
|
|
return self.start_run()
|
|
elif cmd == "ADD_BP":
|
|
self.add_breakpoint(arg)
|
|
return 1
|
|
elif cmd == "REMOVE_BP":
|
|
self.remove_breakpoint(arg)
|
|
return 1
|
|
elif cmd == "LOCALS":
|
|
self.get_variables(cmd)
|
|
return 1
|
|
elif cmd == "GLOBALS":
|
|
self.get_variables(cmd)
|
|
return 1
|
|
elif cmd == "QUIT":
|
|
return self.quit()
|
|
return 1
|
|
|
|
def start_debug(self, filename):
|
|
self.cmd = "DEBUG"
|
|
self._debug_active = 1
|
|
self.saved_stdout = sys.stdout
|
|
self.saved_stderr = sys.stderr
|
|
self.saved_stdin = sys.stdin
|
|
sys.stdout = self
|
|
sys.stderr = self
|
|
sys.stdin = self
|
|
self._wait_for_mainpyfile = 1
|
|
self.mainpyfile = self.canonic(filename)
|
|
self._user_requested_quit = 0
|
|
statement = 'execfile(%r)' % filename
|
|
self.reset()
|
|
#change running directory to location of file
|
|
debug_path = os.path.dirname(filename)
|
|
sys.path.insert(0, debug_path)
|
|
os.chdir(debug_path)
|
|
try:
|
|
self.run(statement)
|
|
self._connection.send_json(dict(cmd="COMPLETE", content="EMPTY"))
|
|
sys.stdout = self.saved_stdout
|
|
sys.stderr = self.saved_stderr
|
|
sys.stdin = self.saved_stdin
|
|
self._debug_active = 0
|
|
except:
|
|
tb, exctype, value = sys.exc_info()
|
|
exc_trace = traceback.format_exception(tb, exctype, value)
|
|
self.send_exception(exc_trace)
|
|
self._connection.send_json(dict(cmd="ERROR_COMPLETE", content="EMPTY"))
|
|
self._debug_active = 0
|
|
pass
|
|
|
|
def start_run(self):
|
|
if self._debug_active:
|
|
self.cmd = "RUN"
|
|
self.set_continue()
|
|
|
|
def next(self):
|
|
if self._debug_active:
|
|
self.cmd = "NEXT"
|
|
self.set_next(self.curframe)
|
|
return 0
|
|
return 1
|
|
|
|
def step(self):
|
|
if self._debug_active:
|
|
self.cmd = "STEP"
|
|
self.set_step()
|
|
return 0
|
|
return 1
|
|
|
|
def add_breakpoint(self, arg):
|
|
filename, line_no = arg.split('~')
|
|
canonic_file = self.canonic(filename)
|
|
self.set_break(canonic_file, int(line_no))
|
|
|
|
def remove_breakpoint(self, arg):
|
|
filename, line_no = arg.split('~')
|
|
canonic_file = self.canonic(filename)
|
|
self.clear_break(canonic_file, int(line_no))
|
|
|
|
def get_variables(self, cmd_type, frame):
|
|
self.stack, self.curindex = self.get_stack(frame, None)
|
|
self.curframe = self.stack[self.curindex][0]
|
|
|
|
if cmd_type == "LOCALS":
|
|
variables = copy.copy(self.curframe.f_locals)
|
|
elif cmd_type == "GLOBALS":
|
|
variables = copy.copy(self.curframe.f_globals)
|
|
variable_list = []
|
|
#print variables
|
|
blocked_variables = set(['bdb', '__builtins__', 'socket', '__file__', '__builtin__', 'types', '__package__',
|
|
'Server', 'sys', 'copy', 'Debugger', 'dbg', '__name__', 'traceback', 'json', 'os', '__doc__'])
|
|
for key in variables.keys():
|
|
if key in blocked_variables:
|
|
variables.pop(key)
|
|
|
|
for variable in variables.items():
|
|
variable_list.append(dict(name=variable[0], content=__builtin__.str(variable[1]), type=self.get_var_type(variable[1])))
|
|
#res = dict(cmd=cmd_type, variables=variable_list)
|
|
#self._connection.send_json(res)
|
|
return variable_list
|
|
|
|
# acting as stdout => redirect to client side
|
|
def write(self, toPrint):
|
|
# transform eol pattern
|
|
#if (toPrint == "\n"):
|
|
#toPrint = "/EOL/"
|
|
self._connection.send_json(dict(cmd="STDOUT", content=toPrint))
|
|
|
|
# acting as stdout => redirect to client side
|
|
def writeline(self, toPrint):
|
|
# stdout redirection
|
|
self.write(toPrint)
|
|
self.write('\n')
|
|
|
|
# stdout flush override
|
|
def flush(self):
|
|
pass
|
|
|
|
def quit(self):
|
|
if self._debug_active:
|
|
self.cmd = "QUIT"
|
|
sys.stdout = self.saved_stdout
|
|
sys.stderr = self.saved_stderr
|
|
sys.stdin = self.saved_stdin
|
|
self.reset()
|
|
self.set_quit()
|
|
self._debug_active = 0
|
|
return 0
|
|
else:
|
|
return 1
|
|
|
|
def start_debugger(self):
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
s.bind(('127.0.0.1', 5000))
|
|
s.listen(1)
|
|
print "DEBUGGER READY"
|
|
sys.stdout.flush()
|
|
connection, addr = s.accept()
|
|
self._socket = s
|
|
self._connection = Server(connection)
|
|
self.loop()
|
|
|
|
# return true when selected element is composite candidate
|
|
def is_composite(self, value):
|
|
if isinstance(value, types.DictType):
|
|
return 0
|
|
elif isinstance(value, types.ListType):
|
|
return 0
|
|
elif isinstance(value, types.TupleType):
|
|
return 0
|
|
elif not (isinstance(value, types.StringType) or \
|
|
isinstance(value, types.ComplexType) or \
|
|
isinstance(value, types.FloatType) or \
|
|
isinstance(value, types.IntType) or \
|
|
isinstance(value, types.LongType) or \
|
|
isinstance(value, types.NoneType) or \
|
|
isinstance(value, types.UnicodeType)):
|
|
return 1
|
|
else:
|
|
return 0
|
|
|
|
# return true when selected element is composite candidate
|
|
def get_simple_type(self, value):
|
|
if isinstance(value, types.StringType):
|
|
return 'String'
|
|
elif isinstance(value, types.ComplexType):
|
|
return 'ComplexNumber'
|
|
elif isinstance(value, types.FloatType):
|
|
return 'Float'
|
|
elif isinstance(value, types.IntType):
|
|
return 'Integer'
|
|
elif isinstance(value, types.LongType):
|
|
return 'Long'
|
|
elif isinstance(value, types.NoneType):
|
|
return 'None'
|
|
elif isinstance(value, types.UnicodeType):
|
|
return 'Unicode'
|
|
else:
|
|
return 'UNMANAGED DATA TYPE'
|
|
|
|
# return true when selected element is map
|
|
def is_map(self, value):
|
|
if isinstance(value, types.DictType):
|
|
return 1
|
|
return 0
|
|
|
|
# return true when selected element is List
|
|
def is_list(self, value):
|
|
if isinstance(value, types.ListType):
|
|
return 1
|
|
return 0
|
|
|
|
# return true when selected element is List
|
|
def is_tuple(self, value):
|
|
if isinstance(value, types.TupleType):
|
|
return 1
|
|
return 0
|
|
|
|
# return true when selected element is composite candidate
|
|
def get_var_type(self, value):
|
|
if self.is_composite(value):
|
|
return 'COMPOSITE'
|
|
else:
|
|
if self.is_map(value):
|
|
return 'MAP'
|
|
elif self.is_list(value):
|
|
return 'LIST'
|
|
elif self.is_tuple(value):
|
|
return 'TUPLE'
|
|
return self.get_simple_type(value)
|
|
|
|
# When invoked as main program, invoke the debugger on a script
|
|
if __name__ == '__main__':
|
|
dbg = Debugger()
|
|
dbg.start_debugger()
|