| """ |
| GDB extension that adds Cython support. |
| """ |
|
|
|
|
| import sys |
| import textwrap |
| import functools |
| import itertools |
| import collections |
|
|
| import gdb |
|
|
| try: |
| from lxml import etree |
| have_lxml = True |
| except ImportError: |
| from xml.etree import ElementTree as etree |
| have_lxml = False |
|
|
| try: |
| import pygments.lexers |
| import pygments.formatters |
| except ImportError: |
| pygments = None |
| sys.stderr.write("Install pygments for colorized source code.\n") |
|
|
| if hasattr(gdb, 'string_to_argv'): |
| from gdb import string_to_argv |
| else: |
| from shlex import split as string_to_argv |
|
|
| from Cython.Debugger import libpython |
|
|
| |
| CObject = 'CObject' |
| PythonObject = 'PythonObject' |
|
|
| _data_types = dict(CObject=CObject, PythonObject=PythonObject) |
| _filesystemencoding = sys.getfilesystemencoding() or 'UTF-8' |
|
|
|
|
| |
|
|
| def default_selected_gdb_frame(err=True): |
| def decorator(function): |
| @functools.wraps(function) |
| def wrapper(self, frame=None, *args, **kwargs): |
| try: |
| frame = frame or gdb.selected_frame() |
| except RuntimeError: |
| raise gdb.GdbError("No frame is currently selected.") |
|
|
| if err and frame.name() is None: |
| raise NoFunctionNameInFrameError() |
|
|
| return function(self, frame, *args, **kwargs) |
| return wrapper |
| return decorator |
|
|
|
|
| def require_cython_frame(function): |
| @functools.wraps(function) |
| @require_running_program |
| def wrapper(self, *args, **kwargs): |
| frame = kwargs.get('frame') or gdb.selected_frame() |
| if not self.is_cython_function(frame): |
| raise gdb.GdbError('Selected frame does not correspond with a ' |
| 'Cython function we know about.') |
| return function(self, *args, **kwargs) |
| return wrapper |
|
|
|
|
| def dispatch_on_frame(c_command, python_command=None): |
| def decorator(function): |
| @functools.wraps(function) |
| def wrapper(self, *args, **kwargs): |
| is_cy = self.is_cython_function() |
| is_py = self.is_python_function() |
|
|
| if is_cy or (is_py and not python_command): |
| function(self, *args, **kwargs) |
| elif is_py: |
| gdb.execute(python_command) |
| elif self.is_relevant_function(): |
| gdb.execute(c_command) |
| else: |
| raise gdb.GdbError("Not a function cygdb knows about. " |
| "Use the normal GDB commands instead.") |
|
|
| return wrapper |
| return decorator |
|
|
|
|
| def require_running_program(function): |
| @functools.wraps(function) |
| def wrapper(*args, **kwargs): |
| try: |
| gdb.selected_frame() |
| except RuntimeError: |
| raise gdb.GdbError("No frame is currently selected.") |
|
|
| return function(*args, **kwargs) |
| return wrapper |
|
|
|
|
| def gdb_function_value_to_unicode(function): |
| @functools.wraps(function) |
| def wrapper(self, string, *args, **kwargs): |
| if isinstance(string, gdb.Value): |
| string = string.string() |
|
|
| return function(self, string, *args, **kwargs) |
| return wrapper |
|
|
|
|
| |
| |
|
|
| def simple_repr(self, renamed=None, state=True): |
| """Prints out all instance variables needed to recreate an object. |
| |
| Following the python convention for __repr__, this function prints all the |
| information stored in an instance as opposed to its class. The working |
| assumption is that most initialization arguments are stored as a property |
| using the same name. |
| |
| The object contents are displayed as the initialization call followed by, |
| optionally, the value of each of the instance's properties in the form: |
| ``` |
| ClassName( |
| init_arg_1 = "repr of some example str", |
| ... |
| ) |
| self.state_based_property = ... |
| ``` |
| |
| Function arguments: |
| self Instance to be represented |
| |
| renamed Dictionary of initialization arguments that are stored under a |
| different property name in the form { argument: property } |
| |
| state Boolean representing whether properties outside the |
| initialization parameters should be printed (self.prop = ...). |
| Using `False` may make the class more amenable to recursive repr |
| """ |
| import inspect |
| init_arg_names = tuple(inspect.signature(self.__init__).parameters) |
| init_attrs = [renamed.get(arg, arg) for arg in init_arg_names] \ |
| if renamed else init_arg_names |
| state_repr = () |
| if state: |
| instance_attrs = sorted(vars(self).keys()) |
| state_repr = [attr for attr in instance_attrs if attr not in init_attrs] |
|
|
| def names_and_values(prefix, attrs, args=None): |
| for attr, arg in zip(attrs, args or attrs): |
| param = repr(getattr(self, attr)).replace("\n", "\n\t\t") |
| yield f'{prefix}{arg} = {param}' |
|
|
| return "".join([ |
| self.__class__.__qualname__, "(", |
| ",".join(names_and_values("\n\t\t", init_attrs, init_arg_names)), |
| "\n\t)", *names_and_values("\nself.", state_repr) |
| ]) |
|
|
|
|
| class CythonModule: |
| def __init__(self, module_name, filename, c_filename): |
| self.name = module_name |
| self.filename = filename |
| self.c_filename = c_filename |
| self.globals = {} |
| |
| self.lineno_cy2c = {} |
| |
| self.lineno_c2cy = {} |
| self.functions = {} |
|
|
| def __repr__(self): |
| return simple_repr(self, renamed={"module_name": "name"}, state=False) |
|
|
|
|
| class CythonVariable: |
|
|
| def __init__(self, name, cname, qualified_name, type, lineno): |
| self.name = name |
| self.cname = cname |
| self.qualified_name = qualified_name |
| self.type = type |
| self.lineno = int(lineno) |
|
|
| def __repr__(self): |
| return simple_repr(self) |
|
|
|
|
| class CythonFunction(CythonVariable): |
| def __init__(self, |
| module, |
| name, |
| cname, |
| pf_cname, |
| qualified_name, |
| lineno, |
| type=CObject, |
| is_initmodule_function="False"): |
| super().__init__(name, |
| cname, |
| qualified_name, |
| type, |
| lineno) |
| self.module = module |
| self.pf_cname = pf_cname |
| self.is_initmodule_function = is_initmodule_function == "True" |
| self.locals = {} |
| self.arguments = [] |
| self.step_into_functions = set() |
|
|
|
|
| |
|
|
| frame_repr_whitelist = { |
| "Frame.is_valid", |
| "Frame.name", |
| "Frame.architecture", |
| "Frame.type", |
| "Frame.pc", |
| "Frame.block", |
| "Frame.function", |
| "Frame.older", |
| "Frame.newer", |
| "Frame.find_sal", |
| "Frame.select", |
| "Frame.static_link", |
| "Frame.level", |
| "Frame.language", |
| "Symbol.is_valid", |
| "Symbol.value", |
| "Symtab_and_line.is_valid", |
| "Symtab.is_valid", |
| "Symtab.fullname", |
| "Symtab.global_block", |
| "Symtab.static_block", |
| "Symtab.linetable", |
| } |
|
|
| def frame_repr(frame): |
| """Returns a string representing the internal state of a provided GDB frame |
| https://sourceware.org/gdb/current/onlinedocs/gdb.html/Frames-In-Python.html |
| |
| Created to serve as GDB.Frame.__repr__ for debugging purposes. GDB has many |
| layers of abstraction separating the state of the debugger from the |
| corresponding source code. This prints a tree of instance properties, |
| expanding the values for Symtab_and_line, Symbol, and Symtab. |
| |
| Most of these properties require computation to determine, meaning much of |
| relevant info is behind a monad, a subset of which are evaluated. |
| |
| Arguments |
| frame The GDB.Frame instance to be represented as a string |
| """ |
| res = f"{frame}\n" |
| for attribute in sorted(dir(frame)): |
| if attribute.startswith("__"): |
| continue |
| value = getattr(frame, attribute) |
| if callable(value) and value.__qualname__ in frame_repr_whitelist: |
| value = value() |
|
|
| if type(value) in [gdb.Symtab_and_line, gdb.Symbol, gdb.Symtab]: |
| |
| value = frame_repr(value).rstrip("\n").replace("\n", "\n\t") |
| res += f"{attribute}: " + ( |
| f"{value:x}\n" if isinstance(value, int) and attribute != "line" |
| else f"{value}\n") |
| return res |
|
|
| class CythonBase: |
|
|
| @default_selected_gdb_frame(err=False) |
| def is_cython_function(self, frame): |
| return frame.name() in self.cy.functions_by_cname |
|
|
| @default_selected_gdb_frame(err=False) |
| def is_python_function(self, frame): |
| """ |
| Tells if a frame is associated with a Python function. |
| If we can't read the Python frame information, don't regard it as such. |
| """ |
| if frame.name() == 'PyEval_EvalFrameEx': |
| pyframe = libpython.Frame(frame).get_pyop() |
| return pyframe and not pyframe.is_optimized_out() |
| return False |
|
|
| @default_selected_gdb_frame() |
| def get_c_function_name(self, frame): |
| return frame.name() |
|
|
| @default_selected_gdb_frame() |
| def get_c_lineno(self, frame): |
| return frame.find_sal().line |
|
|
| @default_selected_gdb_frame() |
| def get_cython_function(self, frame): |
| result = self.cy.functions_by_cname.get(frame.name()) |
| if result is None: |
| raise NoCythonFunctionInFrameError() |
|
|
| return result |
|
|
| @default_selected_gdb_frame() |
| def get_cython_lineno(self, frame): |
| """ |
| Get the current Cython line number. Returns ("<no filename>", 0) if there is no |
| correspondence between the C and Cython code. |
| """ |
| cyfunc = self.get_cython_function(frame) |
| return cyfunc.module.lineno_c2cy.get(self.get_c_lineno(frame), ("<no filename>", 0)) |
|
|
| @default_selected_gdb_frame() |
| def get_source_desc(self, frame): |
| filename = lineno = lexer = None |
| if self.is_cython_function(frame): |
| filename = self.get_cython_function(frame).module.filename |
| filename_and_lineno = self.get_cython_lineno(frame) |
| assert filename == filename_and_lineno[0] |
| lineno = filename_and_lineno[1] |
| if pygments: |
| lexer = pygments.lexers.CythonLexer(stripall=False) |
| elif self.is_python_function(frame): |
| pyframeobject = libpython.Frame(frame).get_pyop() |
|
|
| if not pyframeobject: |
| raise gdb.GdbError( |
| 'Unable to read information on python frame') |
|
|
| filename = pyframeobject.filename() |
| lineno = pyframeobject.current_line_num() |
|
|
| if pygments: |
| lexer = pygments.lexers.PythonLexer(stripall=False) |
| else: |
| symbol_and_line_obj = frame.find_sal() |
| if not symbol_and_line_obj or not symbol_and_line_obj.symtab: |
| filename = None |
| lineno = 0 |
| else: |
| filename = symbol_and_line_obj.symtab.fullname() |
| lineno = symbol_and_line_obj.line |
| if pygments: |
| lexer = pygments.lexers.CLexer(stripall=False) |
|
|
| return SourceFileDescriptor(filename, lexer), lineno |
|
|
| @default_selected_gdb_frame() |
| def get_source_line(self, frame): |
| source_desc, lineno = self.get_source_desc() |
| return source_desc.get_source(lineno) |
|
|
| @default_selected_gdb_frame() |
| def is_relevant_function(self, frame): |
| """ |
| returns whether we care about a frame on the user-level when debugging |
| Cython code |
| """ |
| name = frame.name() |
| older_frame = frame.older() |
| if self.is_cython_function(frame) or self.is_python_function(frame): |
| return True |
| elif older_frame and self.is_cython_function(older_frame): |
| |
| cython_func = self.get_cython_function(older_frame) |
| return name in cython_func.step_into_functions |
|
|
| return False |
|
|
| @default_selected_gdb_frame(err=False) |
| def print_stackframe(self, frame, index, is_c=False): |
| """ |
| Print a C, Cython or Python stack frame and the line of source code |
| if available. |
| """ |
| |
| |
| selected_frame = gdb.selected_frame() |
| frame.select() |
|
|
| try: |
| source_desc, lineno = self.get_source_desc(frame) |
| except NoFunctionNameInFrameError: |
| print('#%-2d Unknown Frame (compile with -g)' % index) |
| return |
|
|
| if not is_c and self.is_python_function(frame): |
| pyframe = libpython.Frame(frame).get_pyop() |
| if pyframe is None or pyframe.is_optimized_out(): |
| |
| return self.print_stackframe(frame, index, is_c=True) |
|
|
| func_name = pyframe.co_name |
| func_cname = 'PyEval_EvalFrameEx' |
| func_args = [] |
| elif self.is_cython_function(frame): |
| cyfunc = self.get_cython_function(frame) |
| f = lambda arg: self.cy.cy_cvalue.invoke(arg, frame=frame) |
|
|
| func_name = cyfunc.name |
| func_cname = cyfunc.cname |
| func_args = [] |
| else: |
| source_desc, lineno = self.get_source_desc(frame) |
| func_name = frame.name() |
| func_cname = func_name |
| func_args = [] |
|
|
| try: |
| gdb_value = gdb.parse_and_eval(func_cname) |
| except RuntimeError: |
| func_address = 0 |
| else: |
| func_address = gdb_value.address |
| if not isinstance(func_address, int): |
| |
| if not isinstance(func_address, (str, bytes)): |
| func_address = str(func_address) |
| func_address = int(func_address.split()[0], 0) |
|
|
| a = ', '.join('%s=%s' % (name, val) for name, val in func_args) |
| sys.stdout.write('#%-2d 0x%016x in %s(%s)' % (index, func_address, func_name, a)) |
|
|
| if source_desc.filename is not None: |
| sys.stdout.write(' at %s:%s' % (source_desc.filename, lineno)) |
|
|
| sys.stdout.write('\n') |
|
|
| try: |
| sys.stdout.write(f' {source_desc.get_source(lineno)}\n') |
| except gdb.GdbError: |
| pass |
|
|
| selected_frame.select() |
|
|
| def get_remote_cython_globals_dict(self): |
| m = gdb.parse_and_eval('__pyx_m') |
|
|
| try: |
| PyModuleObject = gdb.lookup_type('PyModuleObject') |
| except RuntimeError: |
| raise gdb.GdbError(textwrap.dedent("""\ |
| Unable to lookup type PyModuleObject, did you compile python |
| with debugging support (-g)?""")) |
|
|
| m = m.cast(PyModuleObject.pointer()) |
| return m['md_dict'] |
|
|
|
|
| def get_cython_globals_dict(self): |
| """ |
| Get the Cython globals dict where the remote names are turned into |
| local strings. |
| """ |
| remote_dict = self.get_remote_cython_globals_dict() |
| pyobject_dict = libpython.PyObjectPtr.from_pyobject_ptr(remote_dict) |
|
|
| result = {} |
| seen = set() |
| for k, v in pyobject_dict.iteritems(): |
| result[k.proxyval(seen)] = v |
|
|
| return result |
|
|
| def print_gdb_value(self, name, value, max_name_length=None, prefix=''): |
| if libpython.pretty_printer_lookup(value): |
| typename = '' |
| else: |
| typename = '(%s) ' % (value.type,) |
|
|
| if max_name_length is None: |
| print('%s%s = %s%s' % (prefix, name, typename, value)) |
| else: |
| print('%s%-*s = %s%s' % (prefix, max_name_length, name, typename, value)) |
|
|
| def is_initialized(self, cython_func, local_name): |
| cyvar = cython_func.locals[local_name] |
| cur_lineno = self.get_cython_lineno()[1] |
|
|
| if '->' in cyvar.cname: |
| |
| if cur_lineno > cython_func.lineno: |
| if cyvar.type == PythonObject: |
| return int(gdb.parse_and_eval(cyvar.cname)) |
| return True |
| return False |
|
|
| return cur_lineno > cyvar.lineno |
|
|
|
|
| class SourceFileDescriptor: |
| def __init__(self, filename, lexer, formatter=None): |
| self.filename = filename |
| self.lexer = lexer |
| self.formatter = formatter |
|
|
| def valid(self): |
| return self.filename is not None |
|
|
| def lex(self, code): |
| if pygments and self.lexer and parameters.colorize_code: |
| bg = parameters.terminal_background.value |
| if self.formatter is None: |
| formatter = pygments.formatters.TerminalFormatter(bg=bg) |
| else: |
| formatter = self.formatter |
|
|
| return pygments.highlight(code, self.lexer, formatter) |
|
|
| return code |
|
|
| def _get_source(self, start, stop, lex_source, mark_line, lex_entire): |
| with open(self.filename) as f: |
| |
| |
| |
|
|
| if lex_source and lex_entire: |
| f = self.lex(f.read()).splitlines() |
|
|
| slice = itertools.islice(f, start - 1, stop - 1) |
|
|
| for idx, line in enumerate(slice): |
| if start + idx == mark_line: |
| prefix = '>' |
| else: |
| prefix = ' ' |
|
|
| if lex_source and not lex_entire: |
| line = self.lex(line) |
|
|
| yield '%s %4d %s' % (prefix, start + idx, line.rstrip()) |
|
|
| def get_source(self, start, stop=None, lex_source=True, mark_line=0, |
| lex_entire=False): |
| exc = gdb.GdbError('Unable to retrieve source code') |
|
|
| if not self.filename: |
| raise exc |
|
|
| start = max(start, 1) |
| if stop is None: |
| stop = start + 1 |
|
|
| try: |
| return '\n'.join( |
| self._get_source(start, stop, lex_source, mark_line, lex_entire)) |
| except OSError: |
| raise exc |
|
|
|
|
| |
|
|
| class CyGDBError(gdb.GdbError): |
| """ |
| Base class for Cython-command related errors |
| """ |
|
|
| def __init__(self, *args): |
| args = args or (self.msg,) |
| super().__init__(*args) |
|
|
|
|
| class NoCythonFunctionInFrameError(CyGDBError): |
| """ |
| raised when the user requests the current cython function, which is |
| unavailable |
| """ |
| msg = "Current function is a function cygdb doesn't know about" |
|
|
|
|
| class NoFunctionNameInFrameError(NoCythonFunctionInFrameError): |
| """ |
| raised when the name of the C function could not be determined |
| in the current C stack frame |
| """ |
| msg = ('C function name could not be determined in the current C stack ' |
| 'frame') |
|
|
|
|
| |
|
|
| class CythonParameter(gdb.Parameter): |
| """ |
| Base class for cython parameters |
| """ |
|
|
| def __init__(self, name, command_class, parameter_class, default=None): |
| self.show_doc = self.set_doc = self.__class__.__doc__ |
| super().__init__(name, command_class, |
| parameter_class) |
| if default is not None: |
| self.value = default |
|
|
| def __bool__(self): |
| return bool(self.value) |
|
|
| __nonzero__ = __bool__ |
|
|
|
|
|
|
| class CompleteUnqualifiedFunctionNames(CythonParameter): |
| """ |
| Have 'cy break' complete unqualified function or method names. |
| """ |
|
|
|
|
| class ColorizeSourceCode(CythonParameter): |
| """ |
| Tell cygdb whether to colorize source code. |
| """ |
|
|
|
|
| class TerminalBackground(CythonParameter): |
| """ |
| Tell cygdb about the user's terminal background (light or dark). |
| """ |
|
|
|
|
| class CythonParameters: |
| """ |
| Simple container class that might get more functionality in the distant |
| future (mostly to remind us that we're dealing with parameters). |
| """ |
|
|
| def __init__(self): |
| self.complete_unqualified = CompleteUnqualifiedFunctionNames( |
| 'cy_complete_unqualified', |
| gdb.COMMAND_BREAKPOINTS, |
| gdb.PARAM_BOOLEAN, |
| True) |
| self.colorize_code = ColorizeSourceCode( |
| 'cy_colorize_code', |
| gdb.COMMAND_FILES, |
| gdb.PARAM_BOOLEAN, |
| True) |
| self.terminal_background = TerminalBackground( |
| 'cy_terminal_background_color', |
| gdb.COMMAND_FILES, |
| gdb.PARAM_STRING, |
| "dark") |
|
|
| parameters = CythonParameters() |
|
|
|
|
| |
|
|
| class CythonCommand(gdb.Command, CythonBase): |
| """ |
| Base class for Cython commands |
| """ |
|
|
| command_class = gdb.COMMAND_NONE |
|
|
| @classmethod |
| def _register(cls, clsname, args, kwargs): |
| if not hasattr(cls, 'completer_class'): |
| return cls(clsname, cls.command_class, *args, **kwargs) |
| else: |
| return cls(clsname, cls.command_class, cls.completer_class, |
| *args, **kwargs) |
|
|
| @classmethod |
| def register(cls, *args, **kwargs): |
| alias = getattr(cls, 'alias', None) |
| if alias: |
| cls._register(cls.alias, args, kwargs) |
|
|
| return cls._register(cls.name, args, kwargs) |
|
|
|
|
| class CyCy(CythonCommand): |
| """ |
| Invoke a Cython command. Available commands are: |
| |
| cy import |
| cy break |
| cy step |
| cy next |
| cy run |
| cy cont |
| cy finish |
| cy up |
| cy down |
| cy select |
| cy bt / cy backtrace |
| cy list |
| cy print |
| cy set |
| cy locals |
| cy globals |
| cy exec |
| """ |
|
|
| name = 'cy' |
| command_class = gdb.COMMAND_NONE |
| completer_class = gdb.COMPLETE_COMMAND |
|
|
| def __init__(self, name, command_class, completer_class): |
| |
| super(CythonCommand, self).__init__(name, command_class, |
| completer_class, prefix=True) |
|
|
| commands = dict( |
| |
| import_ = CyImport.register(), |
| break_ = CyBreak.register(), |
| step = CyStep.register(), |
| next = CyNext.register(), |
| run = CyRun.register(), |
| cont = CyCont.register(), |
| finish = CyFinish.register(), |
| up = CyUp.register(), |
| down = CyDown.register(), |
| select = CySelect.register(), |
| bt = CyBacktrace.register(), |
| list = CyList.register(), |
| print_ = CyPrint.register(), |
| locals = CyLocals.register(), |
| globals = CyGlobals.register(), |
| exec_ = libpython.FixGdbCommand('cy exec', '-cy-exec'), |
| _exec = CyExec.register(), |
| set = CySet.register(), |
|
|
| |
| cy_cname = CyCName('cy_cname'), |
| cy_cvalue = CyCValue('cy_cvalue'), |
| cy_lineno = CyLine('cy_lineno'), |
| cy_eval = CyEval('cy_eval'), |
| ) |
|
|
| for command_name, command in commands.items(): |
| command.cy = self |
| setattr(self, command_name, command) |
|
|
| self.cy = self |
|
|
| |
| self.cython_namespace = {} |
|
|
| |
| |
| self.functions_by_qualified_name = {} |
|
|
| |
| self.functions_by_cname = {} |
|
|
| |
| |
| self.functions_by_name = collections.defaultdict(list) |
|
|
|
|
| class CyImport(CythonCommand): |
| """ |
| Import debug information outputted by the Cython compiler |
| Example: cy import FILE... |
| """ |
|
|
| name = 'cy import' |
| command_class = gdb.COMMAND_STATUS |
| completer_class = gdb.COMPLETE_FILENAME |
|
|
| @libpython.dont_suppress_errors |
| def invoke(self, args, from_tty): |
| if isinstance(args, bytes): |
| args = args.decode(_filesystemencoding) |
| for arg in string_to_argv(args): |
| try: |
| f = open(arg) |
| except OSError as e: |
| raise gdb.GdbError('Unable to open file %r: %s' % (args, e.args[1])) |
|
|
| t = etree.parse(f) |
|
|
| for module in t.getroot(): |
| cython_module = CythonModule(**module.attrib) |
| self.cy.cython_namespace[cython_module.name] = cython_module |
|
|
| for variable in module.find('Globals'): |
| d = variable.attrib |
| cython_module.globals[d['name']] = CythonVariable(**d) |
|
|
| for function in module.find('Functions'): |
| cython_function = CythonFunction(module=cython_module, |
| **function.attrib) |
|
|
| |
| name = cython_function.name |
| qname = cython_function.qualified_name |
|
|
| self.cy.functions_by_name[name].append(cython_function) |
| self.cy.functions_by_qualified_name[ |
| cython_function.qualified_name] = cython_function |
| self.cy.functions_by_cname[ |
| cython_function.cname] = cython_function |
|
|
| d = cython_module.functions[qname] = cython_function |
|
|
| for local in function.find('Locals'): |
| d = local.attrib |
| cython_function.locals[d['name']] = CythonVariable(**d) |
|
|
| for step_into_func in function.find('StepIntoFunctions'): |
| d = step_into_func.attrib |
| cython_function.step_into_functions.add(d['name']) |
|
|
| cython_function.arguments.extend( |
| funcarg.tag for funcarg in function.find('Arguments')) |
|
|
| for marker in module.find('LineNumberMapping'): |
| src_lineno = int(marker.attrib['src_lineno']) |
| src_path = marker.attrib['src_path'] |
| c_linenos = list(map(int, marker.attrib['c_linenos'].split())) |
| cython_module.lineno_cy2c[src_path, src_lineno] = min(c_linenos) |
| for c_lineno in c_linenos: |
| cython_module.lineno_c2cy[c_lineno] = (src_path, src_lineno) |
|
|
|
|
| class CyBreak(CythonCommand): |
| """ |
| Set a breakpoint for Cython code using Cython qualified name notation, e.g.: |
| |
| cy break cython_modulename.ClassName.method_name... |
| |
| or normal notation: |
| |
| cy break function_or_method_name... |
| |
| or for a line number: |
| |
| cy break cython_module:lineno... |
| |
| Set a Python breakpoint: |
| Break on any function or method named 'func' in module 'modname' |
| |
| cy break -p modname.func... |
| |
| Break on any function or method named 'func' |
| |
| cy break -p func... |
| """ |
|
|
| name = 'cy break' |
| command_class = gdb.COMMAND_BREAKPOINTS |
|
|
| def _break_pyx(self, name): |
| modulename, _, lineno = name.partition(':') |
| lineno = int(lineno) |
| if modulename: |
| cython_module = self.cy.cython_namespace[modulename] |
| else: |
| cython_module = self.get_cython_function().module |
|
|
| if (cython_module.filename, lineno) in cython_module.lineno_cy2c: |
| c_lineno = cython_module.lineno_cy2c[cython_module.filename, lineno] |
| breakpoint = '%s:%s' % (cython_module.c_filename, c_lineno) |
| gdb.execute('break ' + breakpoint) |
| else: |
| raise gdb.GdbError("Not a valid line number. " |
| "Does it contain actual code?") |
|
|
| def _break_funcname(self, funcname): |
| func = self.cy.functions_by_qualified_name.get(funcname) |
|
|
| if func and func.is_initmodule_function: |
| func = None |
|
|
| break_funcs = [func] |
|
|
| if not func: |
| funcs = self.cy.functions_by_name.get(funcname) or [] |
| funcs = [f for f in funcs if not f.is_initmodule_function] |
|
|
| if not funcs: |
| gdb.execute('break ' + funcname) |
| return |
|
|
| if len(funcs) > 1: |
| |
| print('There are multiple such functions:') |
| for idx, func in enumerate(funcs): |
| print('%3d) %s' % (idx, func.qualified_name)) |
|
|
| while True: |
| try: |
| result = input( |
| "Select a function, press 'a' for all " |
| "functions or press 'q' or '^D' to quit: ") |
| except EOFError: |
| return |
| else: |
| if result.lower() == 'q': |
| return |
| elif result.lower() == 'a': |
| break_funcs = funcs |
| break |
| elif (result.isdigit() and |
| 0 <= int(result) < len(funcs)): |
| break_funcs = [funcs[int(result)]] |
| break |
| else: |
| print('Not understood...') |
| else: |
| break_funcs = [funcs[0]] |
|
|
| for func in break_funcs: |
| gdb.execute('break %s' % func.cname) |
| if func.pf_cname: |
| gdb.execute('break %s' % func.pf_cname) |
|
|
| @libpython.dont_suppress_errors |
| def invoke(self, function_names, from_tty): |
| if isinstance(function_names, bytes): |
| function_names = function_names.decode(_filesystemencoding) |
| argv = string_to_argv(function_names) |
| if function_names.startswith('-p'): |
| argv = argv[1:] |
| python_breakpoints = True |
| else: |
| python_breakpoints = False |
|
|
| for funcname in argv: |
| if python_breakpoints: |
| gdb.execute('py-break %s' % funcname) |
| elif ':' in funcname: |
| self._break_pyx(funcname) |
| else: |
| self._break_funcname(funcname) |
|
|
| @libpython.dont_suppress_errors |
| def complete(self, text, word): |
| |
| word = word or "" |
| |
| |
| names = [n for n, L in self.cy.functions_by_name.items() |
| if any(not f.is_initmodule_function for f in L)] |
| qnames = [n for n, f in self.cy.functions_by_qualified_name.items() |
| if not f.is_initmodule_function] |
|
|
| if parameters.complete_unqualified: |
| all_names = itertools.chain(qnames, names) |
| else: |
| all_names = qnames |
|
|
| words = text.strip().split() |
| if not words or '.' not in words[-1]: |
| |
| seen = set(text[:-len(word)].split()) |
| return [n for n in all_names |
| if n.startswith(word) and n not in seen] |
|
|
| |
| lastword = words[-1] |
| compl = [n for n in qnames if n.startswith(lastword)] |
|
|
| if len(lastword) > len(word): |
| |
| |
| strip_prefix_length = len(lastword) - len(word) |
| compl = [n[strip_prefix_length:] for n in compl] |
|
|
| return compl |
|
|
|
|
| class CythonInfo(CythonBase, libpython.PythonInfo): |
| """ |
| Implementation of the interface dictated by libpython.LanguageInfo. |
| """ |
|
|
| def lineno(self, frame): |
| |
| |
| |
| |
| if self.is_cython_function(frame): |
| return self.get_cython_lineno(frame)[1] |
| return super().lineno(frame) |
|
|
| def get_source_line(self, frame): |
| try: |
| line = super().get_source_line(frame) |
| except gdb.GdbError: |
| return None |
| else: |
| return line.strip() or None |
|
|
| def exc_info(self, frame): |
| if self.is_python_function: |
| return super().exc_info(frame) |
|
|
| def runtime_break_functions(self): |
| if self.is_cython_function(): |
| return self.get_cython_function().step_into_functions |
| return () |
|
|
| def static_break_functions(self): |
| result = ['PyEval_EvalFrameEx'] |
| result.extend(self.cy.functions_by_cname) |
| return result |
|
|
|
|
| class CythonExecutionControlCommand(CythonCommand, |
| libpython.ExecutionControlCommandBase): |
|
|
| @classmethod |
| def register(cls): |
| return cls(cls.name, cython_info) |
|
|
|
|
| class CyStep(CythonExecutionControlCommand, libpython.PythonStepperMixin): |
| "Step through Cython, Python or C code." |
|
|
| name = 'cy -step' |
| stepinto = True |
|
|
| @libpython.dont_suppress_errors |
| def invoke(self, args, from_tty): |
| if self.is_python_function(): |
| self.python_step(self.stepinto) |
| elif not self.is_cython_function(): |
| if self.stepinto: |
| command = 'step' |
| else: |
| command = 'next' |
|
|
| self.finish_executing(gdb.execute(command, to_string=True)) |
| else: |
| self.step(stepinto=self.stepinto) |
|
|
|
|
| class CyNext(CyStep): |
| "Step-over Cython, Python or C code." |
|
|
| name = 'cy -next' |
| stepinto = False |
|
|
|
|
| class CyRun(CythonExecutionControlCommand): |
| """ |
| Run a Cython program. This is like the 'run' command, except that it |
| displays Cython or Python source lines as well |
| """ |
|
|
| name = 'cy run' |
|
|
| invoke = libpython.dont_suppress_errors(CythonExecutionControlCommand.run) |
|
|
|
|
| class CyCont(CythonExecutionControlCommand): |
| """ |
| Continue a Cython program. This is like the 'run' command, except that it |
| displays Cython or Python source lines as well. |
| """ |
|
|
| name = 'cy cont' |
| invoke = libpython.dont_suppress_errors(CythonExecutionControlCommand.cont) |
|
|
|
|
| class CyFinish(CythonExecutionControlCommand): |
| """ |
| Execute until the function returns. |
| """ |
| name = 'cy finish' |
|
|
| invoke = libpython.dont_suppress_errors(CythonExecutionControlCommand.finish) |
|
|
|
|
| class CyUp(CythonCommand): |
| """ |
| Go up a Cython, Python or relevant C frame. |
| """ |
| name = 'cy up' |
| _command = 'up' |
|
|
| @libpython.dont_suppress_errors |
| def invoke(self, *args): |
| try: |
| gdb.execute(self._command, to_string=True) |
| while not self.is_relevant_function(gdb.selected_frame()): |
| gdb.execute(self._command, to_string=True) |
| except RuntimeError as e: |
| raise gdb.GdbError(*e.args) |
|
|
| frame = gdb.selected_frame() |
| index = 0 |
| while frame: |
| frame = frame.older() |
| index += 1 |
|
|
| self.print_stackframe(index=index - 1) |
|
|
|
|
| class CyDown(CyUp): |
| """ |
| Go down a Cython, Python or relevant C frame. |
| """ |
|
|
| name = 'cy down' |
| _command = 'down' |
|
|
|
|
| class CySelect(CythonCommand): |
| """ |
| Select a frame. Use frame numbers as listed in `cy backtrace`. |
| This command is useful because `cy backtrace` prints a reversed backtrace. |
| """ |
|
|
| name = 'cy select' |
|
|
| @libpython.dont_suppress_errors |
| def invoke(self, stackno, from_tty): |
| try: |
| stackno = int(stackno) |
| except ValueError: |
| raise gdb.GdbError("Not a valid number: %r" % (stackno,)) |
|
|
| frame = gdb.selected_frame() |
| while frame.newer(): |
| frame = frame.newer() |
|
|
| stackdepth = libpython.stackdepth(frame) |
|
|
| try: |
| gdb.execute('select %d' % (stackdepth - stackno - 1,)) |
| except RuntimeError as e: |
| raise gdb.GdbError(*e.args) |
|
|
|
|
| class CyBacktrace(CythonCommand): |
| 'Print the Cython stack' |
|
|
| name = 'cy bt' |
| alias = 'cy backtrace' |
| command_class = gdb.COMMAND_STACK |
| completer_class = gdb.COMPLETE_NONE |
|
|
| @libpython.dont_suppress_errors |
| @require_running_program |
| def invoke(self, args, from_tty): |
| |
| frame = gdb.selected_frame() |
| while frame.older(): |
| frame = frame.older() |
|
|
| print_all = args == '-a' |
|
|
| index = 0 |
| while frame: |
| try: |
| is_relevant = self.is_relevant_function(frame) |
| except CyGDBError: |
| is_relevant = False |
|
|
| if print_all or is_relevant: |
| self.print_stackframe(frame, index) |
|
|
| index += 1 |
| frame = frame.newer() |
|
|
|
|
| class CyList(CythonCommand): |
| """ |
| List Cython source code. To disable to customize colouring see the cy_* |
| parameters. |
| """ |
|
|
| name = 'cy list' |
| command_class = gdb.COMMAND_FILES |
| completer_class = gdb.COMPLETE_NONE |
|
|
| @libpython.dont_suppress_errors |
| |
| def invoke(self, _, from_tty): |
| sd, lineno = self.get_source_desc() |
| source = sd.get_source(lineno - 5, lineno + 5, mark_line=lineno, |
| lex_entire=True) |
| print(source) |
|
|
|
|
| class CyPrint(CythonCommand): |
| """ |
| Print a Cython variable using 'cy-print x' or 'cy-print module.function.x' |
| """ |
|
|
| name = 'cy print' |
| command_class = gdb.COMMAND_DATA |
|
|
| @libpython.dont_suppress_errors |
| def invoke(self, name, from_tty): |
| global_python_dict = self.get_cython_globals_dict() |
| module_globals = self.get_cython_function().module.globals |
|
|
| if name in global_python_dict: |
| value = global_python_dict[name].get_truncated_repr(libpython.MAX_OUTPUT_LEN) |
| print('%s = %s' % (name, value)) |
| |
| |
| elif name in module_globals: |
| cname = module_globals[name].cname |
| try: |
| value = gdb.parse_and_eval(cname) |
| except RuntimeError: |
| print("unable to get value of %s" % name) |
| else: |
| if not value.is_optimized_out: |
| self.print_gdb_value(name, value) |
| else: |
| print("%s is optimized out" % name) |
| elif self.is_python_function(): |
| return gdb.execute('py-print ' + name) |
| elif self.is_cython_function(): |
| value = self.cy.cy_cvalue.invoke(name.lstrip('*')) |
| for c in name: |
| if c == '*': |
| value = value.dereference() |
| else: |
| break |
|
|
| self.print_gdb_value(name, value) |
| else: |
| gdb.execute('print ' + name) |
|
|
| def complete(self): |
| if self.is_cython_function(): |
| f = self.get_cython_function() |
| return list(itertools.chain(f.locals, f.globals)) |
| else: |
| return [] |
|
|
|
|
| sortkey = lambda item: item[0].lower() |
|
|
|
|
| class CyLocals(CythonCommand): |
| """ |
| List the locals from the current Cython frame. |
| """ |
|
|
| name = 'cy locals' |
| command_class = gdb.COMMAND_STACK |
| completer_class = gdb.COMPLETE_NONE |
|
|
| @libpython.dont_suppress_errors |
| @dispatch_on_frame(c_command='info locals', python_command='py-locals') |
| def invoke(self, args, from_tty): |
| cython_function = self.get_cython_function() |
|
|
| if cython_function.is_initmodule_function: |
| self.cy.globals.invoke(args, from_tty) |
| return |
|
|
| local_cython_vars = cython_function.locals |
| max_name_length = len(max(local_cython_vars, key=len)) |
| for name, cyvar in sorted(local_cython_vars.items(), key=sortkey): |
| if self.is_initialized(self.get_cython_function(), cyvar.name): |
| value = gdb.parse_and_eval(cyvar.cname) |
| if not value.is_optimized_out: |
| self.print_gdb_value(cyvar.name, value, |
| max_name_length, '') |
|
|
|
|
| class CyGlobals(CyLocals): |
| """ |
| List the globals from the current Cython module. |
| """ |
|
|
| name = 'cy globals' |
| command_class = gdb.COMMAND_STACK |
| completer_class = gdb.COMPLETE_NONE |
|
|
| @libpython.dont_suppress_errors |
| @dispatch_on_frame(c_command='info variables', python_command='py-globals') |
| def invoke(self, args, from_tty): |
| global_python_dict = self.get_cython_globals_dict() |
| module_globals = self.get_cython_function().module.globals |
|
|
| max_globals_len = 0 |
| max_globals_dict_len = 0 |
| if module_globals: |
| max_globals_len = len(max(module_globals, key=len)) |
| if global_python_dict: |
| max_globals_dict_len = len(max(global_python_dict)) |
|
|
| max_name_length = max(max_globals_len, max_globals_dict_len) |
|
|
| seen = set() |
| print('Python globals:') |
|
|
| for k, v in sorted(global_python_dict.items(), key=sortkey): |
| v = v.get_truncated_repr(libpython.MAX_OUTPUT_LEN) |
| seen.add(k) |
| print(' %-*s = %s' % (max_name_length, k, v)) |
|
|
| print('C globals:') |
| for name, cyvar in sorted(module_globals.items(), key=sortkey): |
| if name not in seen: |
| try: |
| value = gdb.parse_and_eval(cyvar.cname) |
| except RuntimeError: |
| pass |
| else: |
| if not value.is_optimized_out: |
| self.print_gdb_value(cyvar.name, value, |
| max_name_length, ' ') |
|
|
|
|
| class EvaluateOrExecuteCodeMixin: |
| """ |
| Evaluate or execute Python code in a Cython or Python frame. The 'evalcode' |
| method evaluations Python code, prints a traceback if an exception went |
| uncaught, and returns any return value as a gdb.Value (NULL on exception). |
| """ |
|
|
| def _fill_locals_dict(self, executor, local_dict_pointer): |
| "Fill a remotely allocated dict with values from the Cython C stack" |
| cython_func = self.get_cython_function() |
|
|
| for name, cyvar in cython_func.locals.items(): |
| if (cyvar.type == PythonObject |
| and self.is_initialized(cython_func, name)): |
|
|
| try: |
| val = gdb.parse_and_eval(cyvar.cname) |
| except RuntimeError: |
| continue |
| else: |
| if val.is_optimized_out: |
| continue |
|
|
| pystringp = executor.alloc_pystring(name) |
| code = ''' |
| (PyObject *) PyDict_SetItem( |
| (PyObject *) %d, |
| (PyObject *) %d, |
| (PyObject *) %s) |
| ''' % (local_dict_pointer, pystringp, cyvar.cname) |
|
|
| try: |
| if gdb.parse_and_eval(code) < 0: |
| gdb.parse_and_eval('PyErr_Print()') |
| raise gdb.GdbError("Unable to execute Python code.") |
| finally: |
| |
| executor.xdecref(pystringp) |
|
|
| def _find_first_cython_or_python_frame(self): |
| frame = gdb.selected_frame() |
| while frame: |
| if (self.is_cython_function(frame) |
| or self.is_python_function(frame)): |
| frame.select() |
| return frame |
|
|
| frame = frame.older() |
|
|
| raise gdb.GdbError("There is no Cython or Python frame on the stack.") |
|
|
| def _evalcode_cython(self, executor, code, input_type): |
| with libpython.FetchAndRestoreError(): |
| |
| |
| global_dict = gdb.parse_and_eval( |
| '(PyObject *) PyModule_GetDict(__pyx_m)') |
| local_dict = gdb.parse_and_eval('(PyObject *) PyDict_New()') |
|
|
| try: |
| self._fill_locals_dict(executor, |
| libpython.pointervalue(local_dict)) |
| result = executor.evalcode(code, input_type, global_dict, |
| local_dict) |
| finally: |
| executor.xdecref(libpython.pointervalue(local_dict)) |
|
|
| return result |
|
|
| def evalcode(self, code, input_type): |
| """ |
| Evaluate `code` in a Python or Cython stack frame using the given |
| `input_type`. |
| """ |
| frame = self._find_first_cython_or_python_frame() |
| executor = libpython.PythonCodeExecutor() |
| if self.is_python_function(frame): |
| return libpython._evalcode_python(executor, code, input_type) |
| return self._evalcode_cython(executor, code, input_type) |
|
|
|
|
| class CyExec(CythonCommand, libpython.PyExec, EvaluateOrExecuteCodeMixin): |
| """ |
| Execute Python code in the nearest Python or Cython frame. |
| """ |
|
|
| name = '-cy-exec' |
| command_class = gdb.COMMAND_STACK |
| completer_class = gdb.COMPLETE_NONE |
|
|
| @libpython.dont_suppress_errors |
| def invoke(self, expr, from_tty): |
| expr, input_type = self.readcode(expr) |
| executor = libpython.PythonCodeExecutor() |
| executor.xdecref(self.evalcode(expr, executor.Py_file_input)) |
|
|
|
|
| class CySet(CythonCommand): |
| """ |
| Set a Cython variable to a certain value |
| |
| cy set my_cython_c_variable = 10 |
| cy set my_cython_py_variable = $cy_eval("{'doner': 'kebab'}") |
| |
| This is equivalent to |
| |
| set $cy_value("my_cython_variable") = 10 |
| """ |
|
|
| name = 'cy set' |
| command_class = gdb.COMMAND_DATA |
| completer_class = gdb.COMPLETE_NONE |
|
|
| @libpython.dont_suppress_errors |
| @require_cython_frame |
| def invoke(self, expr, from_tty): |
| name_and_expr = expr.split('=', 1) |
| if len(name_and_expr) != 2: |
| raise gdb.GdbError("Invalid expression. Use 'cy set var = expr'.") |
|
|
| varname, expr = name_and_expr |
| cname = self.cy.cy_cname.invoke(varname.strip()) |
| gdb.execute("set %s = %s" % (cname, expr)) |
|
|
|
|
| |
|
|
| class CyCName(gdb.Function, CythonBase): |
| """ |
| Get the C name of a Cython variable in the current context. |
| Examples: |
| |
| print $cy_cname("function") |
| print $cy_cname("Class.method") |
| print $cy_cname("module.function") |
| """ |
|
|
| @libpython.dont_suppress_errors |
| @require_cython_frame |
| @gdb_function_value_to_unicode |
| def invoke(self, cyname, frame=None): |
| frame = frame or gdb.selected_frame() |
| cname = None |
|
|
| if self.is_cython_function(frame): |
| cython_function = self.get_cython_function(frame) |
| if cyname in cython_function.locals: |
| cname = cython_function.locals[cyname].cname |
| elif cyname in cython_function.module.globals: |
| cname = cython_function.module.globals[cyname].cname |
| else: |
| qname = '%s.%s' % (cython_function.module.name, cyname) |
| if qname in cython_function.module.functions: |
| cname = cython_function.module.functions[qname].cname |
|
|
| if not cname: |
| cname = self.cy.functions_by_qualified_name.get(cyname) |
|
|
| if not cname: |
| raise gdb.GdbError('No such Cython variable: %s' % cyname) |
|
|
| return cname |
|
|
|
|
| class CyCValue(CyCName): |
| """ |
| Get the value of a Cython variable. |
| """ |
|
|
| @libpython.dont_suppress_errors |
| @require_cython_frame |
| @gdb_function_value_to_unicode |
| def invoke(self, cyname, frame=None): |
| globals_dict = self.get_cython_globals_dict() |
| cython_function = self.get_cython_function(frame) |
|
|
| if self.is_initialized(cython_function, cyname): |
| cname = super().invoke(cyname, frame=frame) |
| return gdb.parse_and_eval(cname) |
| elif cyname in globals_dict: |
| return globals_dict[cyname]._gdbval |
| else: |
| raise gdb.GdbError("Variable %s is not initialized." % cyname) |
|
|
|
|
| class CyLine(gdb.Function, CythonBase): |
| """ |
| Get the current Cython line. |
| """ |
|
|
| @libpython.dont_suppress_errors |
| @require_cython_frame |
| def invoke(self): |
| return self.get_cython_lineno()[1] |
|
|
|
|
| class CyEval(gdb.Function, CythonBase, EvaluateOrExecuteCodeMixin): |
| """ |
| Evaluate Python code in the nearest Python or Cython frame and return |
| """ |
|
|
| @libpython.dont_suppress_errors |
| @gdb_function_value_to_unicode |
| def invoke(self, python_expression): |
| input_type = libpython.PythonCodeExecutor.Py_eval_input |
| return self.evalcode(python_expression, input_type) |
|
|
|
|
| cython_info = CythonInfo() |
| cy = CyCy.register() |
| cython_info.cy = cy |
|
|
|
|
| def register_defines(): |
| libpython.source_gdb_script(textwrap.dedent("""\ |
| define cy step |
| cy -step |
| end |
| |
| define cy next |
| cy -next |
| end |
| |
| document cy step |
| %s |
| end |
| |
| document cy next |
| %s |
| end |
| """) % (CyStep.__doc__, CyNext.__doc__)) |
|
|
| register_defines() |
|
|