diff options
author | alexv-smirnov <alex@ydb.tech> | 2023-03-15 19:59:12 +0300 |
---|---|---|
committer | alexv-smirnov <alex@ydb.tech> | 2023-03-15 19:59:12 +0300 |
commit | 056bb284ccf8dd6793ec3a54ffa36c4fb2b9ad11 (patch) | |
tree | 4740980126f32e3af7937ba0ca5f83e59baa4ab0 /contrib/tools/cython/Cython/Debugger | |
parent | 269126dcced1cc8b53eb4398b4a33e5142f10290 (diff) | |
download | ydb-056bb284ccf8dd6793ec3a54ffa36c4fb2b9ad11.tar.gz |
add library/cpp/actors, ymake build to ydb oss export
Diffstat (limited to 'contrib/tools/cython/Cython/Debugger')
12 files changed, 5370 insertions, 0 deletions
diff --git a/contrib/tools/cython/Cython/Debugger/Cygdb.py b/contrib/tools/cython/Cython/Debugger/Cygdb.py new file mode 100644 index 0000000000..45f31ce6f7 --- /dev/null +++ b/contrib/tools/cython/Cython/Debugger/Cygdb.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python + +""" +The Cython debugger + +The current directory should contain a directory named 'cython_debug', or a +path to the cython project directory should be given (the parent directory of +cython_debug). + +Additional gdb args can be provided only if a path to the project directory is +given. +""" + +import os +import sys +import glob +import tempfile +import textwrap +import subprocess +import optparse +import logging + +logger = logging.getLogger(__name__) + +def make_command_file(path_to_debug_info, prefix_code='', no_import=False): + if not no_import: + pattern = os.path.join(path_to_debug_info, + 'cython_debug', + 'cython_debug_info_*') + debug_files = glob.glob(pattern) + + if not debug_files: + sys.exit('%s.\nNo debug files were found in %s. Aborting.' % ( + usage, os.path.abspath(path_to_debug_info))) + + fd, tempfilename = tempfile.mkstemp() + f = os.fdopen(fd, 'w') + try: + f.write(prefix_code) + f.write(textwrap.dedent('''\ + # This is a gdb command file + # See https://sourceware.org/gdb/onlinedocs/gdb/Command-Files.html + + set breakpoint pending on + set print pretty on + + python + # Activate virtualenv, if we were launched from one + import os + virtualenv = os.getenv('VIRTUAL_ENV') + if virtualenv: + path_to_activate_this_py = os.path.join(virtualenv, 'bin', 'activate_this.py') + print("gdb command file: Activating virtualenv: %s; path_to_activate_this_py: %s" % ( + virtualenv, path_to_activate_this_py)) + with open(path_to_activate_this_py) as f: + exec(f.read(), dict(__file__=path_to_activate_this_py)) + + from Cython.Debugger import libcython, libpython + end + ''')) + + if no_import: + # don't do this, this overrides file command in .gdbinit + # f.write("file %s\n" % sys.executable) + pass + else: + path = os.path.join(path_to_debug_info, "cython_debug", "interpreter") + interpreter_file = open(path) + try: + interpreter = interpreter_file.read() + finally: + interpreter_file.close() + f.write("file %s\n" % interpreter) + f.write('\n'.join('cy import %s\n' % fn for fn in debug_files)) + f.write(textwrap.dedent('''\ + python + import sys + try: + gdb.lookup_type('PyModuleObject') + except RuntimeError: + sys.stderr.write( + 'Python was not compiled with debug symbols (or it was ' + 'stripped). Some functionality may not work (properly).\\n') + end + + source .cygdbinit + ''')) + finally: + f.close() + + return tempfilename + +usage = "Usage: cygdb [options] [PATH [-- GDB_ARGUMENTS]]" + +def main(path_to_debug_info=None, gdb_argv=None, no_import=False): + """ + Start the Cython debugger. This tells gdb to import the Cython and Python + extensions (libcython.py and libpython.py) and it enables gdb's pending + breakpoints. + + path_to_debug_info is the path to the Cython build directory + gdb_argv is the list of options to gdb + no_import tells cygdb whether it should import debug information + """ + parser = optparse.OptionParser(usage=usage) + parser.add_option("--gdb-executable", + dest="gdb", default='gdb', + help="gdb executable to use [default: gdb]") + parser.add_option("--verbose", "-v", + dest="verbosity", action="count", default=0, + help="Verbose mode. Multiple -v options increase the verbosity") + + (options, args) = parser.parse_args() + if path_to_debug_info is None: + if len(args) > 1: + path_to_debug_info = args[0] + else: + path_to_debug_info = os.curdir + + if gdb_argv is None: + gdb_argv = args[1:] + + if path_to_debug_info == '--': + no_import = True + + logging_level = logging.WARN + if options.verbosity == 1: + logging_level = logging.INFO + if options.verbosity >= 2: + logging_level = logging.DEBUG + logging.basicConfig(level=logging_level) + + logger.info("verbosity = %r", options.verbosity) + logger.debug("options = %r; args = %r", options, args) + logger.debug("Done parsing command-line options. path_to_debug_info = %r, gdb_argv = %r", + path_to_debug_info, gdb_argv) + + tempfilename = make_command_file(path_to_debug_info, no_import=no_import) + logger.info("Launching %s with command file: %s and gdb_argv: %s", + options.gdb, tempfilename, gdb_argv) + with open(tempfilename) as tempfile: + logger.debug('Command file (%s) contains: """\n%s"""', tempfilename, tempfile.read()) + logger.info("Spawning %s...", options.gdb) + p = subprocess.Popen([options.gdb, '-command', tempfilename] + gdb_argv) + logger.info("Spawned %s (pid %d)", options.gdb, p.pid) + while True: + try: + logger.debug("Waiting for gdb (pid %d) to exit...", p.pid) + ret = p.wait() + logger.debug("Wait for gdb (pid %d) to exit is done. Returned: %r", p.pid, ret) + except KeyboardInterrupt: + pass + else: + break + logger.debug("Closing temp command file with fd: %s", tempfile.fileno()) + logger.debug("Removing temp command file: %s", tempfilename) + os.remove(tempfilename) + logger.debug("Removed temp command file: %s", tempfilename) diff --git a/contrib/tools/cython/Cython/Debugger/DebugWriter.py b/contrib/tools/cython/Cython/Debugger/DebugWriter.py new file mode 100644 index 0000000000..876a3a2169 --- /dev/null +++ b/contrib/tools/cython/Cython/Debugger/DebugWriter.py @@ -0,0 +1,72 @@ +from __future__ import absolute_import + +import os +import sys +import errno + +try: + from lxml import etree + have_lxml = True +except ImportError: + have_lxml = False + try: + from xml.etree import cElementTree as etree + except ImportError: + try: + from xml.etree import ElementTree as etree + except ImportError: + etree = None + +from ..Compiler import Errors + + +class CythonDebugWriter(object): + """ + Class to output debugging information for cygdb + + It writes debug information to cython_debug/cython_debug_info_<modulename> + in the build directory. + """ + + def __init__(self, output_dir): + if etree is None: + raise Errors.NoElementTreeInstalledException() + + self.output_dir = os.path.join(output_dir or os.curdir, 'cython_debug') + self.tb = etree.TreeBuilder() + # set by Cython.Compiler.ParseTreeTransforms.DebugTransform + self.module_name = None + self.start('cython_debug', attrs=dict(version='1.0')) + + def start(self, name, attrs=None): + self.tb.start(name, attrs or {}) + + def end(self, name): + self.tb.end(name) + + def add_entry(self, name, **attrs): + self.tb.start(name, attrs) + self.tb.end(name) + + def serialize(self): + self.tb.end('Module') + self.tb.end('cython_debug') + xml_root_element = self.tb.close() + + try: + os.makedirs(self.output_dir) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + et = etree.ElementTree(xml_root_element) + kw = {} + if have_lxml: + kw['pretty_print'] = True + + fn = "cython_debug_info_" + self.module_name + et.write(os.path.join(self.output_dir, fn), encoding="UTF-8", **kw) + + interpreter_path = os.path.join(self.output_dir, 'interpreter') + with open(interpreter_path, 'w') as f: + f.write(sys.executable) diff --git a/contrib/tools/cython/Cython/Debugger/Tests/TestLibCython.py b/contrib/tools/cython/Cython/Debugger/Tests/TestLibCython.py new file mode 100644 index 0000000000..13560646ff --- /dev/null +++ b/contrib/tools/cython/Cython/Debugger/Tests/TestLibCython.py @@ -0,0 +1,274 @@ + +import os +import re +import sys +import shutil +import warnings +import textwrap +import unittest +import tempfile +import subprocess +#import distutils.core +#from distutils import sysconfig +from distutils import ccompiler + +import runtests +import Cython.Distutils.extension +import Cython.Distutils.old_build_ext as build_ext +from Cython.Debugger import Cygdb as cygdb + +root = os.path.dirname(os.path.abspath(__file__)) +codefile = os.path.join(root, 'codefile') +cfuncs_file = os.path.join(root, 'cfuncs.c') + +with open(codefile) as f: + source_to_lineno = dict((line.strip(), i + 1) for i, line in enumerate(f)) + + +have_gdb = None +def test_gdb(): + global have_gdb + if have_gdb is not None: + return have_gdb + + have_gdb = False + try: + p = subprocess.Popen(['gdb', '-nx', '--version'], stdout=subprocess.PIPE) + except OSError: + # gdb not found + gdb_version = None + else: + stdout, _ = p.communicate() + # Based on Lib/test/test_gdb.py + regex = r"GNU gdb [^\d]*(\d+)\.(\d+)" + gdb_version = re.match(regex, stdout.decode('ascii', 'ignore')) + + if gdb_version: + gdb_version_number = list(map(int, gdb_version.groups())) + if gdb_version_number >= [7, 2]: + have_gdb = True + with tempfile.NamedTemporaryFile(mode='w+') as python_version_script: + python_version_script.write( + 'python import sys; print("%s %s" % sys.version_info[:2])') + python_version_script.flush() + p = subprocess.Popen(['gdb', '-batch', '-x', python_version_script.name], + stdout=subprocess.PIPE) + stdout, _ = p.communicate() + try: + internal_python_version = list(map(int, stdout.decode('ascii', 'ignore').split())) + if internal_python_version < [2, 6]: + have_gdb = False + except ValueError: + have_gdb = False + + if not have_gdb: + warnings.warn('Skipping gdb tests, need gdb >= 7.2 with Python >= 2.6') + + return have_gdb + + +class DebuggerTestCase(unittest.TestCase): + + def setUp(self): + """ + Run gdb and have cygdb import the debug information from the code + defined in TestParseTreeTransforms's setUp method + """ + if not test_gdb(): + return + + self.tempdir = tempfile.mkdtemp() + self.destfile = os.path.join(self.tempdir, 'codefile.pyx') + self.debug_dest = os.path.join(self.tempdir, + 'cython_debug', + 'cython_debug_info_codefile') + self.cfuncs_destfile = os.path.join(self.tempdir, 'cfuncs') + + self.cwd = os.getcwd() + try: + os.chdir(self.tempdir) + + shutil.copy(codefile, self.destfile) + shutil.copy(cfuncs_file, self.cfuncs_destfile + '.c') + shutil.copy(cfuncs_file.replace('.c', '.h'), + self.cfuncs_destfile + '.h') + + compiler = ccompiler.new_compiler() + compiler.compile(['cfuncs.c'], debug=True, extra_postargs=['-fPIC']) + + opts = dict( + test_directory=self.tempdir, + module='codefile', + ) + + optimization_disabler = build_ext.Optimization() + + cython_compile_testcase = runtests.CythonCompileTestCase( + workdir=self.tempdir, + # we clean up everything (not only compiled files) + cleanup_workdir=False, + tags=runtests.parse_tags(codefile), + **opts + ) + + + new_stderr = open(os.devnull, 'w') + + stderr = sys.stderr + sys.stderr = new_stderr + + optimization_disabler.disable_optimization() + try: + cython_compile_testcase.run_cython( + targetdir=self.tempdir, + incdir=None, + annotate=False, + extra_compile_options={ + 'gdb_debug':True, + 'output_dir':self.tempdir, + }, + **opts + ) + + cython_compile_testcase.run_distutils( + incdir=None, + workdir=self.tempdir, + extra_extension_args={'extra_objects':['cfuncs.o']}, + **opts + ) + finally: + optimization_disabler.restore_state() + sys.stderr = stderr + new_stderr.close() + + # ext = Cython.Distutils.extension.Extension( + # 'codefile', + # ['codefile.pyx'], + # cython_gdb=True, + # extra_objects=['cfuncs.o']) + # + # distutils.core.setup( + # script_args=['build_ext', '--inplace'], + # ext_modules=[ext], + # cmdclass=dict(build_ext=Cython.Distutils.build_ext) + # ) + + except: + os.chdir(self.cwd) + raise + + def tearDown(self): + if not test_gdb(): + return + os.chdir(self.cwd) + shutil.rmtree(self.tempdir) + + +class GdbDebuggerTestCase(DebuggerTestCase): + + def setUp(self): + if not test_gdb(): + return + + super(GdbDebuggerTestCase, self).setUp() + + prefix_code = textwrap.dedent('''\ + python + + import os + import sys + import traceback + + def excepthook(type, value, tb): + traceback.print_exception(type, value, tb) + sys.stderr.flush() + sys.stdout.flush() + os._exit(1) + + sys.excepthook = excepthook + + # Have tracebacks end up on sys.stderr (gdb replaces sys.stderr + # with an object that calls gdb.write()) + sys.stderr = sys.__stderr__ + + end + ''') + + code = textwrap.dedent('''\ + python + + from Cython.Debugger.Tests import test_libcython_in_gdb + test_libcython_in_gdb.main(version=%r) + + end + ''' % (sys.version_info[:2],)) + + self.gdb_command_file = cygdb.make_command_file(self.tempdir, + prefix_code) + + with open(self.gdb_command_file, 'a') as f: + f.write(code) + + args = ['gdb', '-batch', '-x', self.gdb_command_file, '-n', '--args', + sys.executable, '-c', 'import codefile'] + + paths = [] + path = os.environ.get('PYTHONPATH') + if path: + paths.append(path) + paths.append(os.path.dirname(os.path.dirname( + os.path.abspath(Cython.__file__)))) + env = dict(os.environ, PYTHONPATH=os.pathsep.join(paths)) + + self.p = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env) + + def tearDown(self): + if not test_gdb(): + return + + try: + super(GdbDebuggerTestCase, self).tearDown() + if self.p: + try: self.p.stdout.close() + except: pass + try: self.p.stderr.close() + except: pass + self.p.wait() + finally: + os.remove(self.gdb_command_file) + + +class TestAll(GdbDebuggerTestCase): + + def test_all(self): + if not test_gdb(): + return + + out, err = self.p.communicate() + out = out.decode('UTF-8') + err = err.decode('UTF-8') + + exit_status = self.p.returncode + + if exit_status == 1: + sys.stderr.write(out) + sys.stderr.write(err) + elif exit_status >= 2: + border = u'*' * 30 + start = u'%s v INSIDE GDB v %s' % (border, border) + stderr = u'%s v STDERR v %s' % (border, border) + end = u'%s ^ INSIDE GDB ^ %s' % (border, border) + errmsg = u'\n%s\n%s%s\n%s%s' % (start, out, stderr, err, end) + + sys.stderr.write(errmsg) + + # FIXME: re-enable this to make the test fail on internal failures + #self.assertEqual(exit_status, 0) + + +if __name__ == '__main__': + unittest.main() diff --git a/contrib/tools/cython/Cython/Debugger/Tests/__init__.py b/contrib/tools/cython/Cython/Debugger/Tests/__init__.py new file mode 100644 index 0000000000..fa81adaff6 --- /dev/null +++ b/contrib/tools/cython/Cython/Debugger/Tests/__init__.py @@ -0,0 +1 @@ +# empty file diff --git a/contrib/tools/cython/Cython/Debugger/Tests/cfuncs.c b/contrib/tools/cython/Cython/Debugger/Tests/cfuncs.c new file mode 100644 index 0000000000..ccb42050bf --- /dev/null +++ b/contrib/tools/cython/Cython/Debugger/Tests/cfuncs.c @@ -0,0 +1,8 @@ +void +some_c_function(void) +{ + int a, b, c; + + a = 1; + b = 2; +} diff --git a/contrib/tools/cython/Cython/Debugger/Tests/cfuncs.h b/contrib/tools/cython/Cython/Debugger/Tests/cfuncs.h new file mode 100644 index 0000000000..3b21bc2d5f --- /dev/null +++ b/contrib/tools/cython/Cython/Debugger/Tests/cfuncs.h @@ -0,0 +1 @@ +void some_c_function(void); diff --git a/contrib/tools/cython/Cython/Debugger/Tests/codefile b/contrib/tools/cython/Cython/Debugger/Tests/codefile new file mode 100644 index 0000000000..6b4c6b6add --- /dev/null +++ b/contrib/tools/cython/Cython/Debugger/Tests/codefile @@ -0,0 +1,50 @@ +cdef extern from "stdio.h": + int puts(char *s) + +cdef extern from "cfuncs.h": + void some_c_function() + +import os + +cdef int c_var = 12 +python_var = 13 + +def spam(a=0): + cdef: + int b, c + + b = c = d = 0 + + b = 1 + c = 2 + int(10) + puts("spam") + os.path.join("foo", "bar") + some_c_function() + +cpdef eggs(): + pass + +cdef ham(): + pass + +cdef class SomeClass(object): + def spam(self): + pass + +def outer(): + cdef object a = "an object" + def inner(): + b = 2 + # access closed over variables + print a, b + return inner + + +outer()() + +spam() +print "bye!" + +def use_ham(): + ham() diff --git a/contrib/tools/cython/Cython/Debugger/Tests/test_libcython_in_gdb.py b/contrib/tools/cython/Cython/Debugger/Tests/test_libcython_in_gdb.py new file mode 100644 index 0000000000..bd7608d607 --- /dev/null +++ b/contrib/tools/cython/Cython/Debugger/Tests/test_libcython_in_gdb.py @@ -0,0 +1,496 @@ +""" +Tests that run inside GDB. + +Note: debug information is already imported by the file generated by +Cython.Debugger.Cygdb.make_command_file() +""" + +from __future__ import absolute_import + +import os +import re +import sys +import trace +import inspect +import warnings +import unittest +import textwrap +import tempfile +import functools +import traceback +import itertools +#from test import test_support + +import gdb + +from .. import libcython +from .. import libpython +from . import TestLibCython as test_libcython +from ...Utils import add_metaclass + +# for some reason sys.argv is missing in gdb +sys.argv = ['gdb'] + + +def print_on_call_decorator(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + _debug(type(self).__name__, func.__name__) + + try: + return func(self, *args, **kwargs) + except Exception: + _debug("An exception occurred:", traceback.format_exc()) + raise + + return wrapper + +class TraceMethodCallMeta(type): + + def __init__(self, name, bases, dict): + for func_name, func in dict.items(): + if inspect.isfunction(func): + setattr(self, func_name, print_on_call_decorator(func)) + + +@add_metaclass(TraceMethodCallMeta) +class DebugTestCase(unittest.TestCase): + """ + Base class for test cases. On teardown it kills the inferior and unsets + all breakpoints. + """ + + def __init__(self, name): + super(DebugTestCase, self).__init__(name) + self.cy = libcython.cy + self.module = libcython.cy.cython_namespace['codefile'] + self.spam_func, self.spam_meth = libcython.cy.functions_by_name['spam'] + self.ham_func = libcython.cy.functions_by_qualified_name[ + 'codefile.ham'] + self.eggs_func = libcython.cy.functions_by_qualified_name[ + 'codefile.eggs'] + + def read_var(self, varname, cast_to=None): + result = gdb.parse_and_eval('$cy_cvalue("%s")' % varname) + if cast_to: + result = cast_to(result) + + return result + + def local_info(self): + return gdb.execute('info locals', to_string=True) + + def lineno_equals(self, source_line=None, lineno=None): + if source_line is not None: + lineno = test_libcython.source_to_lineno[source_line] + frame = gdb.selected_frame() + self.assertEqual(libcython.cython_info.lineno(frame), lineno) + + def break_and_run(self, source_line): + break_lineno = test_libcython.source_to_lineno[source_line] + gdb.execute('cy break codefile:%d' % break_lineno, to_string=True) + gdb.execute('run', to_string=True) + + def tearDown(self): + gdb.execute('delete breakpoints', to_string=True) + try: + gdb.execute('kill inferior 1', to_string=True) + except RuntimeError: + pass + + gdb.execute('set args -c "import codefile"') + + +class TestDebugInformationClasses(DebugTestCase): + + def test_CythonModule(self): + "test that debug information was parsed properly into data structures" + self.assertEqual(self.module.name, 'codefile') + global_vars = ('c_var', 'python_var', '__name__', + '__builtins__', '__doc__', '__file__') + assert set(global_vars).issubset(self.module.globals) + + def test_CythonVariable(self): + module_globals = self.module.globals + c_var = module_globals['c_var'] + python_var = module_globals['python_var'] + self.assertEqual(c_var.type, libcython.CObject) + self.assertEqual(python_var.type, libcython.PythonObject) + self.assertEqual(c_var.qualified_name, 'codefile.c_var') + + def test_CythonFunction(self): + self.assertEqual(self.spam_func.qualified_name, 'codefile.spam') + self.assertEqual(self.spam_meth.qualified_name, + 'codefile.SomeClass.spam') + self.assertEqual(self.spam_func.module, self.module) + + assert self.eggs_func.pf_cname, (self.eggs_func, self.eggs_func.pf_cname) + assert not self.ham_func.pf_cname + assert not self.spam_func.pf_cname + assert not self.spam_meth.pf_cname + + self.assertEqual(self.spam_func.type, libcython.CObject) + self.assertEqual(self.ham_func.type, libcython.CObject) + + self.assertEqual(self.spam_func.arguments, ['a']) + self.assertEqual(self.spam_func.step_into_functions, + set(['puts', 'some_c_function'])) + + expected_lineno = test_libcython.source_to_lineno['def spam(a=0):'] + self.assertEqual(self.spam_func.lineno, expected_lineno) + self.assertEqual(sorted(self.spam_func.locals), list('abcd')) + + +class TestParameters(unittest.TestCase): + + def test_parameters(self): + gdb.execute('set cy_colorize_code on') + assert libcython.parameters.colorize_code + gdb.execute('set cy_colorize_code off') + assert not libcython.parameters.colorize_code + + +class TestBreak(DebugTestCase): + + def test_break(self): + breakpoint_amount = len(gdb.breakpoints() or ()) + gdb.execute('cy break codefile.spam') + + self.assertEqual(len(gdb.breakpoints()), breakpoint_amount + 1) + bp = gdb.breakpoints()[-1] + self.assertEqual(bp.type, gdb.BP_BREAKPOINT) + assert self.spam_func.cname in bp.location + assert bp.enabled + + def test_python_break(self): + gdb.execute('cy break -p join') + assert 'def join(' in gdb.execute('cy run', to_string=True) + + def test_break_lineno(self): + beginline = 'import os' + nextline = 'cdef int c_var = 12' + + self.break_and_run(beginline) + self.lineno_equals(beginline) + step_result = gdb.execute('cy step', to_string=True) + self.lineno_equals(nextline) + assert step_result.rstrip().endswith(nextline) + + +class TestKilled(DebugTestCase): + + def test_abort(self): + gdb.execute("set args -c 'import os; os.abort()'") + output = gdb.execute('cy run', to_string=True) + assert 'abort' in output.lower() + + +class DebugStepperTestCase(DebugTestCase): + + def step(self, varnames_and_values, source_line=None, lineno=None): + gdb.execute(self.command) + for varname, value in varnames_and_values: + self.assertEqual(self.read_var(varname), value, self.local_info()) + + self.lineno_equals(source_line, lineno) + + +class TestStep(DebugStepperTestCase): + """ + Test stepping. Stepping happens in the code found in + Cython/Debugger/Tests/codefile. + """ + + def test_cython_step(self): + gdb.execute('cy break codefile.spam') + + gdb.execute('run', to_string=True) + self.lineno_equals('def spam(a=0):') + + gdb.execute('cy step', to_string=True) + self.lineno_equals('b = c = d = 0') + + self.command = 'cy step' + self.step([('b', 0)], source_line='b = 1') + self.step([('b', 1), ('c', 0)], source_line='c = 2') + self.step([('c', 2)], source_line='int(10)') + self.step([], source_line='puts("spam")') + + gdb.execute('cont', to_string=True) + self.assertEqual(len(gdb.inferiors()), 1) + self.assertEqual(gdb.inferiors()[0].pid, 0) + + def test_c_step(self): + self.break_and_run('some_c_function()') + gdb.execute('cy step', to_string=True) + self.assertEqual(gdb.selected_frame().name(), 'some_c_function') + + def test_python_step(self): + self.break_and_run('os.path.join("foo", "bar")') + + result = gdb.execute('cy step', to_string=True) + + curframe = gdb.selected_frame() + self.assertEqual(curframe.name(), 'PyEval_EvalFrameEx') + + pyframe = libpython.Frame(curframe).get_pyop() + # With Python 3 inferiors, pyframe.co_name will return a PyUnicodePtr, + # be compatible + frame_name = pyframe.co_name.proxyval(set()) + self.assertEqual(frame_name, 'join') + assert re.match(r'\d+ def join\(', result), result + + +class TestNext(DebugStepperTestCase): + + def test_cython_next(self): + self.break_and_run('c = 2') + + lines = ( + 'int(10)', + 'puts("spam")', + 'os.path.join("foo", "bar")', + 'some_c_function()', + ) + + for line in lines: + gdb.execute('cy next') + self.lineno_equals(line) + + +class TestLocalsGlobals(DebugTestCase): + + def test_locals(self): + self.break_and_run('int(10)') + + result = gdb.execute('cy locals', to_string=True) + assert 'a = 0', repr(result) + assert 'b = (int) 1', result + assert 'c = (int) 2' in result, repr(result) + + def test_globals(self): + self.break_and_run('int(10)') + + result = gdb.execute('cy globals', to_string=True) + assert '__name__ ' in result, repr(result) + assert '__doc__ ' in result, repr(result) + assert 'os ' in result, repr(result) + assert 'c_var ' in result, repr(result) + assert 'python_var ' in result, repr(result) + + +class TestBacktrace(DebugTestCase): + + def test_backtrace(self): + libcython.parameters.colorize_code.value = False + + self.break_and_run('os.path.join("foo", "bar")') + + def match_backtrace_output(result): + assert re.search(r'\#\d+ *0x.* in spam\(\) at .*codefile\.pyx:22', + result), result + assert 'os.path.join("foo", "bar")' in result, result + + result = gdb.execute('cy bt', to_string=True) + match_backtrace_output(result) + + result = gdb.execute('cy bt -a', to_string=True) + match_backtrace_output(result) + + # Apparently not everyone has main() + # assert re.search(r'\#0 *0x.* in main\(\)', result), result + + +class TestFunctions(DebugTestCase): + + def test_functions(self): + self.break_and_run('c = 2') + result = gdb.execute('print $cy_cname("b")', to_string=True) + assert re.search('__pyx_.*b', result), result + + result = gdb.execute('print $cy_lineno()', to_string=True) + supposed_lineno = test_libcython.source_to_lineno['c = 2'] + assert str(supposed_lineno) in result, (supposed_lineno, result) + + result = gdb.execute('print $cy_cvalue("b")', to_string=True) + assert '= 1' in result + + +class TestPrint(DebugTestCase): + + def test_print(self): + self.break_and_run('c = 2') + result = gdb.execute('cy print b', to_string=True) + self.assertEqual('b = (int) 1\n', result) + + +class TestUpDown(DebugTestCase): + + def test_updown(self): + self.break_and_run('os.path.join("foo", "bar")') + gdb.execute('cy step') + self.assertRaises(RuntimeError, gdb.execute, 'cy down') + + result = gdb.execute('cy up', to_string=True) + assert 'spam()' in result + assert 'os.path.join("foo", "bar")' in result + + +class TestExec(DebugTestCase): + + def setUp(self): + super(TestExec, self).setUp() + self.fd, self.tmpfilename = tempfile.mkstemp() + self.tmpfile = os.fdopen(self.fd, 'r+') + + def tearDown(self): + super(TestExec, self).tearDown() + + try: + self.tmpfile.close() + finally: + os.remove(self.tmpfilename) + + def eval_command(self, command): + gdb.execute('cy exec open(%r, "w").write(str(%s))' % + (self.tmpfilename, command)) + return self.tmpfile.read().strip() + + def test_cython_exec(self): + self.break_and_run('os.path.join("foo", "bar")') + + # test normal behaviour + self.assertEqual("[0]", self.eval_command('[a]')) + + # test multiline code + result = gdb.execute(textwrap.dedent('''\ + cy exec + pass + + "nothing" + end + ''')) + result = self.tmpfile.read().rstrip() + self.assertEqual('', result) + + def test_python_exec(self): + self.break_and_run('os.path.join("foo", "bar")') + gdb.execute('cy step') + + gdb.execute('cy exec some_random_var = 14') + self.assertEqual('14', self.eval_command('some_random_var')) + + +class CySet(DebugTestCase): + + def test_cyset(self): + self.break_and_run('os.path.join("foo", "bar")') + + gdb.execute('cy set a = $cy_eval("{None: []}")') + stringvalue = self.read_var("a", cast_to=str) + self.assertEqual(stringvalue, "{None: []}") + + +class TestCyEval(DebugTestCase): + "Test the $cy_eval() gdb function." + + def test_cy_eval(self): + # This function leaks a few objects in the GDB python process. This + # is no biggie + self.break_and_run('os.path.join("foo", "bar")') + + result = gdb.execute('print $cy_eval("None")', to_string=True) + assert re.match(r'\$\d+ = None\n', result), result + + result = gdb.execute('print $cy_eval("[a]")', to_string=True) + assert re.match(r'\$\d+ = \[0\]', result), result + + +class TestClosure(DebugTestCase): + + def break_and_run_func(self, funcname): + gdb.execute('cy break ' + funcname) + gdb.execute('cy run') + + def test_inner(self): + self.break_and_run_func('inner') + self.assertEqual('', gdb.execute('cy locals', to_string=True)) + + # Allow the Cython-generated code to initialize the scope variable + gdb.execute('cy step') + + self.assertEqual(str(self.read_var('a')), "'an object'") + print_result = gdb.execute('cy print a', to_string=True).strip() + self.assertEqual(print_result, "a = 'an object'") + + def test_outer(self): + self.break_and_run_func('outer') + self.assertEqual('', gdb.execute('cy locals', to_string=True)) + + # Initialize scope with 'a' uninitialized + gdb.execute('cy step') + self.assertEqual('', gdb.execute('cy locals', to_string=True)) + + # Initialize 'a' to 1 + gdb.execute('cy step') + print_result = gdb.execute('cy print a', to_string=True).strip() + self.assertEqual(print_result, "a = 'an object'") + + +_do_debug = os.environ.get('GDB_DEBUG') +if _do_debug: + _debug_file = open('/dev/tty', 'w') + +def _debug(*messages): + if _do_debug: + messages = itertools.chain([sys._getframe(1).f_code.co_name, ':'], + messages) + _debug_file.write(' '.join(str(msg) for msg in messages) + '\n') + + +def run_unittest_in_module(modulename): + try: + gdb.lookup_type('PyModuleObject') + except RuntimeError: + msg = ("Unable to run tests, Python was not compiled with " + "debugging information. Either compile python with " + "-g or get a debug build (configure with --with-pydebug).") + warnings.warn(msg) + os._exit(1) + else: + m = __import__(modulename, fromlist=['']) + tests = inspect.getmembers(m, inspect.isclass) + + # test_support.run_unittest(tests) + + test_loader = unittest.TestLoader() + suite = unittest.TestSuite( + [test_loader.loadTestsFromTestCase(cls) for name, cls in tests]) + + result = unittest.TextTestRunner(verbosity=1).run(suite) + return result.wasSuccessful() + +def runtests(): + """ + Run the libcython and libpython tests. Ensure that an appropriate status is + returned to the parent test process. + """ + from Cython.Debugger.Tests import test_libpython_in_gdb + + success_libcython = run_unittest_in_module(__name__) + success_libpython = run_unittest_in_module(test_libpython_in_gdb.__name__) + + if not success_libcython or not success_libpython: + sys.exit(2) + +def main(version, trace_code=False): + global inferior_python_version + + inferior_python_version = version + + if trace_code: + tracer = trace.Trace(count=False, trace=True, outfile=sys.stderr, + ignoredirs=[sys.prefix, sys.exec_prefix]) + tracer.runfunc(runtests) + else: + runtests() diff --git a/contrib/tools/cython/Cython/Debugger/Tests/test_libpython_in_gdb.py b/contrib/tools/cython/Cython/Debugger/Tests/test_libpython_in_gdb.py new file mode 100644 index 0000000000..6f34cee47b --- /dev/null +++ b/contrib/tools/cython/Cython/Debugger/Tests/test_libpython_in_gdb.py @@ -0,0 +1,115 @@ +# -*- coding: UTF-8 -*- + +""" +Test libpython.py. This is already partly tested by test_libcython_in_gdb and +Lib/test/test_gdb.py in the Python source. These tests are run in gdb and +called from test_libcython_in_gdb.main() +""" + +import os +import sys + +import gdb + +from Cython.Debugger import libcython +from Cython.Debugger import libpython + +from . import test_libcython_in_gdb +from .test_libcython_in_gdb import _debug, inferior_python_version + + +class TestPrettyPrinters(test_libcython_in_gdb.DebugTestCase): + """ + Test whether types of Python objects are correctly inferred and that + the right libpython.PySomeTypeObjectPtr classes are instantiated. + + Also test whether values are appropriately formatted (don't be too + laborious as Lib/test/test_gdb.py already covers this extensively). + + Don't take care of decreffing newly allocated objects as a new + interpreter is started for every test anyway. + """ + + def setUp(self): + super(TestPrettyPrinters, self).setUp() + self.break_and_run('b = c = d = 0') + + def get_pyobject(self, code): + value = gdb.parse_and_eval(code) + assert libpython.pointervalue(value) != 0 + return value + + def pyobject_fromcode(self, code, gdbvar=None): + if gdbvar is not None: + d = {'varname':gdbvar, 'code':code} + gdb.execute('set $%(varname)s = %(code)s' % d) + code = '$' + gdbvar + + return libpython.PyObjectPtr.from_pyobject_ptr(self.get_pyobject(code)) + + def get_repr(self, pyobject): + return pyobject.get_truncated_repr(libpython.MAX_OUTPUT_LEN) + + def alloc_bytestring(self, string, gdbvar=None): + if inferior_python_version < (3, 0): + funcname = 'PyString_FromStringAndSize' + else: + funcname = 'PyBytes_FromStringAndSize' + + assert b'"' not in string + + # ensure double quotes + code = '(PyObject *) %s("%s", %d)' % (funcname, string.decode('iso8859-1'), len(string)) + return self.pyobject_fromcode(code, gdbvar=gdbvar) + + def alloc_unicodestring(self, string, gdbvar=None): + postfix = libpython.get_inferior_unicode_postfix() + funcname = 'PyUnicode%s_DecodeUnicodeEscape' % (postfix,) + + data = string.encode("unicode_escape").decode('iso8859-1') + return self.pyobject_fromcode( + '(PyObject *) %s("%s", %d, "strict")' % ( + funcname, data.replace('"', r'\"').replace('\\', r'\\'), len(data)), + gdbvar=gdbvar) + + def test_bytestring(self): + bytestring = self.alloc_bytestring(b"spam") + + if inferior_python_version < (3, 0): + bytestring_class = libpython.PyStringObjectPtr + expected = repr(b"spam") + else: + bytestring_class = libpython.PyBytesObjectPtr + expected = "b'spam'" + + self.assertEqual(type(bytestring), bytestring_class) + self.assertEqual(self.get_repr(bytestring), expected) + + def test_unicode(self): + unicode_string = self.alloc_unicodestring(u"spam ἄλφα") + + expected = u"'spam ἄλφα'" + if inferior_python_version < (3, 0): + expected = 'u' + expected + + self.assertEqual(type(unicode_string), libpython.PyUnicodeObjectPtr) + self.assertEqual(self.get_repr(unicode_string), expected) + + def test_int(self): + if inferior_python_version < (3, 0): + intval = self.pyobject_fromcode('PyInt_FromLong(100)') + self.assertEqual(type(intval), libpython.PyIntObjectPtr) + self.assertEqual(self.get_repr(intval), '100') + + def test_long(self): + longval = self.pyobject_fromcode('PyLong_FromLong(200)', + gdbvar='longval') + assert gdb.parse_and_eval('$longval->ob_type == &PyLong_Type') + + self.assertEqual(type(longval), libpython.PyLongObjectPtr) + self.assertEqual(self.get_repr(longval), '200') + + def test_frame_type(self): + frame = self.pyobject_fromcode('PyEval_GetFrame()') + + self.assertEqual(type(frame), libpython.PyFrameObjectPtr) diff --git a/contrib/tools/cython/Cython/Debugger/__init__.py b/contrib/tools/cython/Cython/Debugger/__init__.py new file mode 100644 index 0000000000..fa81adaff6 --- /dev/null +++ b/contrib/tools/cython/Cython/Debugger/__init__.py @@ -0,0 +1 @@ +# empty file diff --git a/contrib/tools/cython/Cython/Debugger/libcython.py b/contrib/tools/cython/Cython/Debugger/libcython.py new file mode 100644 index 0000000000..23153789b6 --- /dev/null +++ b/contrib/tools/cython/Cython/Debugger/libcython.py @@ -0,0 +1,1434 @@ +""" +GDB extension that adds Cython support. +""" + +from __future__ import print_function + +try: + input = raw_input +except NameError: + pass + +import sys +import textwrap +import traceback +import functools +import itertools +import collections + +import gdb + +try: # python 2 + UNICODE = unicode + BYTES = str +except NameError: # python 3 + UNICODE = str + BYTES = bytes + +try: + from lxml import etree + have_lxml = True +except ImportError: + have_lxml = False + try: + # Python 2.5 + from xml.etree import cElementTree as etree + except ImportError: + try: + # Python 2.5 + from xml.etree import ElementTree as etree + except ImportError: + try: + # normal cElementTree install + import cElementTree as etree + except ImportError: + # normal ElementTree install + import elementtree.ElementTree as etree + +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 + +# C or Python type +CObject = 'CObject' +PythonObject = 'PythonObject' + +_data_types = dict(CObject=CObject, PythonObject=PythonObject) +_filesystemencoding = sys.getfilesystemencoding() or 'UTF-8' + + +# decorators + +def dont_suppress_errors(function): + "*sigh*, readline" + @functools.wraps(function) + def wrapper(*args, **kwargs): + try: + return function(*args, **kwargs) + except Exception: + traceback.print_exc() + raise + + return wrapper + + +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 + + +# Classes that represent the debug information +# Don't rename the parameters of these classes, they come directly from the XML + +class CythonModule(object): + def __init__(self, module_name, filename, c_filename): + self.name = module_name + self.filename = filename + self.c_filename = c_filename + self.globals = {} + # {cython_lineno: min(c_linenos)} + self.lineno_cy2c = {} + # {c_lineno: cython_lineno} + self.lineno_c2cy = {} + self.functions = {} + + +class CythonVariable(object): + + 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) + + +class CythonFunction(CythonVariable): + def __init__(self, + module, + name, + cname, + pf_cname, + qualified_name, + lineno, + type=CObject, + is_initmodule_function="False"): + super(CythonFunction, self).__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() + + +# General purpose classes + +class CythonBase(object): + + @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 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), 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 + lineno = self.get_cython_lineno(frame) + 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): + # check for direct C function call from a Cython function + 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. + """ + # do this to prevent the require_cython_frame decorator from + # raising GdbError when calling self.cy.cy_cvalue.invoke() + 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(): + # print this python function as a C function + 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 = [] # [(arg, f(arg)) for arg in cyfunc.arguments] + 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): + # Seriously? Why is the address not an 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(' ' + source_desc.get_source(lineno)) + 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.items(): + 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() + + if '->' in cyvar.cname: + # Closed over free variable + 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(object): + 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: + # to provide "correct" colouring, the entire code needs to be + # lexed. However, this makes a lot of things terribly slow, so + # we decide not to. Besides, it's unlikely to matter. + + 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 IOError: + raise exc + + +# Errors + +class CyGDBError(gdb.GdbError): + """ + Base class for Cython-command related errors + """ + + def __init__(self, *args): + args = args or (self.msg,) + super(CyGDBError, self).__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') + + +# Parameters + +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(CythonParameter, self).__init__(name, command_class, + parameter_class) + if default is not None: + self.value = default + + def __bool__(self): + return bool(self.value) + + __nonzero__ = __bool__ # Python 2 + + + +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(object): + """ + 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() + + +# Commands + +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): + # keep the signature 2.5 compatible (i.e. do not use f(*a, k=v) + super(CythonCommand, self).__init__(name, command_class, + completer_class, prefix=True) + + commands = dict( + # GDB commands + 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(), + + # GDB functions + 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 + + # Cython module namespace + self.cython_namespace = {} + + # maps (unique) qualified function names (e.g. + # cythonmodule.ClassName.method_name) to the CythonFunction object + self.functions_by_qualified_name = {} + + # unique cnames of Cython functions + self.functions_by_cname = {} + + # map function names like method_name to a list of all such + # CythonFunction objects + 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 + + 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) + + # update the global function mappings + 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'): + cython_lineno = int(marker.attrib['cython_lineno']) + c_linenos = list(map(int, marker.attrib['c_linenos'].split())) + cython_module.lineno_cy2c[cython_lineno] = min(c_linenos) + for c_lineno in c_linenos: + cython_module.lineno_c2cy[c_lineno] = cython_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 lineno in cython_module.lineno_cy2c: + c_lineno = cython_module.lineno_cy2c[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: + # multiple functions, let the user pick one + 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) + + 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) + + @dont_suppress_errors + def complete(self, text, word): + # Filter init-module functions (breakpoints can be set using + # modulename:linenumber). + 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]: + # complete unqualified + seen = set(text[:-len(word)].split()) + return [n for n in all_names + if n.startswith(word) and n not in seen] + + # complete qualified name + lastword = words[-1] + compl = [n for n in qnames if n.startswith(lastword)] + + if len(lastword) > len(word): + # readline sees something (e.g. a '.') as a word boundary, so don't + # "recomplete" this prefix + 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): + # Take care of the Python and Cython levels. We need to care for both + # as we can't simply dispatch to 'py-step', since that would work for + # stepping through Python code, but it would not step back into Cython- + # related code. The C level should be dispatched to the 'step' command. + if self.is_cython_function(frame): + return self.get_cython_lineno(frame) + return super(CythonInfo, self).lineno(frame) + + def get_source_line(self, frame): + try: + line = super(CythonInfo, self).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(CythonInfo, self).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 + + 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 = 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 = CythonExecutionControlCommand.cont + + +class CyFinish(CythonExecutionControlCommand): + """ + Execute until the function returns. + """ + name = 'cy finish' + + invoke = CythonExecutionControlCommand.finish + + +class CyUp(CythonCommand): + """ + Go up a Cython, Python or relevant C frame. + """ + name = 'cy up' + _command = 'up' + + 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' + + 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 + + @require_running_program + def invoke(self, args, from_tty): + # get the first frame + 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 + + # @dispatch_on_frame(c_command='list') + 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 + + def invoke(self, name, from_tty, max_name_length=None): + if 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, max_name_length) + 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 + + @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 + + @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(object): + """ + 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: + # PyDict_SetItem doesn't steal our reference + 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(): + # get the dict of Cython globals and construct a dict in the + # inferior with Cython locals + 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 + + def invoke(self, expr, from_tty): + expr, input_type = self.readcode(expr) + executor = libpython.PythonCodeExecutor() + executor.xdecref(self.evalcode(expr, executor.Py_single_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 + + @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)) + + +# Functions + +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") + """ + + @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. + """ + + @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(CyCValue, self).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. + """ + + @require_cython_frame + def invoke(self): + return self.get_cython_lineno() + + +class CyEval(gdb.Function, CythonBase, EvaluateOrExecuteCodeMixin): + """ + Evaluate Python code in the nearest Python or Cython frame and return + """ + + @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() diff --git a/contrib/tools/cython/Cython/Debugger/libpython.py b/contrib/tools/cython/Cython/Debugger/libpython.py new file mode 100644 index 0000000000..fea626dd73 --- /dev/null +++ b/contrib/tools/cython/Cython/Debugger/libpython.py @@ -0,0 +1,2760 @@ +#!/usr/bin/python + +# NOTE: this file is taken from the Python source distribution +# It can be found under Tools/gdb/libpython.py. It is shipped with Cython +# because it's not installed as a python module, and because changes are only +# merged into new python versions (v3.2+). + +''' +From gdb 7 onwards, gdb's build can be configured --with-python, allowing gdb +to be extended with Python code e.g. for library-specific data visualizations, +such as for the C++ STL types. Documentation on this API can be seen at: +http://sourceware.org/gdb/current/onlinedocs/gdb/Python-API.html + + +This python module deals with the case when the process being debugged (the +"inferior process" in gdb parlance) is itself python, or more specifically, +linked against libpython. In this situation, almost every item of data is a +(PyObject*), and having the debugger merely print their addresses is not very +enlightening. + +This module embeds knowledge about the implementation details of libpython so +that we can emit useful visualizations e.g. a string, a list, a dict, a frame +giving file/line information and the state of local variables + +In particular, given a gdb.Value corresponding to a PyObject* in the inferior +process, we can generate a "proxy value" within the gdb process. For example, +given a PyObject* in the inferior process that is in fact a PyListObject* +holding three PyObject* that turn out to be PyBytesObject* instances, we can +generate a proxy value within the gdb process that is a list of bytes +instances: + [b"foo", b"bar", b"baz"] + +Doing so can be expensive for complicated graphs of objects, and could take +some time, so we also have a "write_repr" method that writes a representation +of the data to a file-like object. This allows us to stop the traversal by +having the file-like object raise an exception if it gets too much data. + +With both "proxyval" and "write_repr" we keep track of the set of all addresses +visited so far in the traversal, to avoid infinite recursion due to cycles in +the graph of object references. + +We try to defer gdb.lookup_type() invocations for python types until as late as +possible: for a dynamically linked python binary, when the process starts in +the debugger, the libpython.so hasn't been dynamically loaded yet, so none of +the type names are known to the debugger + +The module also extends gdb with some python-specific commands. +''' + +# NOTE: some gdbs are linked with Python 3, so this file should be dual-syntax +# compatible (2.6+ and 3.0+). See #19308. + +from __future__ import print_function +import gdb +import os +import locale +import sys + +if sys.version_info[0] >= 3: + unichr = chr + xrange = range + long = int + +# Look up the gdb.Type for some standard types: +# Those need to be refreshed as types (pointer sizes) may change when +# gdb loads different executables + +def _type_char_ptr(): + return gdb.lookup_type('char').pointer() # char* + + +def _type_unsigned_char_ptr(): + return gdb.lookup_type('unsigned char').pointer() # unsigned char* + + +def _type_unsigned_short_ptr(): + return gdb.lookup_type('unsigned short').pointer() + + +def _type_unsigned_int_ptr(): + return gdb.lookup_type('unsigned int').pointer() + + +def _sizeof_void_p(): + return gdb.lookup_type('void').pointer().sizeof + + +# value computed later, see PyUnicodeObjectPtr.proxy() +_is_pep393 = None + +Py_TPFLAGS_HEAPTYPE = (1 << 9) +Py_TPFLAGS_LONG_SUBCLASS = (1 << 24) +Py_TPFLAGS_LIST_SUBCLASS = (1 << 25) +Py_TPFLAGS_TUPLE_SUBCLASS = (1 << 26) +Py_TPFLAGS_BYTES_SUBCLASS = (1 << 27) +Py_TPFLAGS_UNICODE_SUBCLASS = (1 << 28) +Py_TPFLAGS_DICT_SUBCLASS = (1 << 29) +Py_TPFLAGS_BASE_EXC_SUBCLASS = (1 << 30) +Py_TPFLAGS_TYPE_SUBCLASS = (1 << 31) + + +MAX_OUTPUT_LEN=1024 + +hexdigits = "0123456789abcdef" + +ENCODING = locale.getpreferredencoding() + +EVALFRAME = '_PyEval_EvalFrameDefault' + +class NullPyObjectPtr(RuntimeError): + pass + + +def safety_limit(val): + # Given an integer value from the process being debugged, limit it to some + # safety threshold so that arbitrary breakage within said process doesn't + # break the gdb process too much (e.g. sizes of iterations, sizes of lists) + return min(val, 1000) + + +def safe_range(val): + # As per range, but don't trust the value too much: cap it to a safety + # threshold in case the data was corrupted + return xrange(safety_limit(int(val))) + +if sys.version_info[0] >= 3: + def write_unicode(file, text): + file.write(text) +else: + def write_unicode(file, text): + # Write a byte or unicode string to file. Unicode strings are encoded to + # ENCODING encoding with 'backslashreplace' error handler to avoid + # UnicodeEncodeError. + if isinstance(text, unicode): + text = text.encode(ENCODING, 'backslashreplace') + file.write(text) + +try: + os_fsencode = os.fsencode +except AttributeError: + def os_fsencode(filename): + if not isinstance(filename, unicode): + return filename + encoding = sys.getfilesystemencoding() + if encoding == 'mbcs': + # mbcs doesn't support surrogateescape + return filename.encode(encoding) + encoded = [] + for char in filename: + # surrogateescape error handler + if 0xDC80 <= ord(char) <= 0xDCFF: + byte = chr(ord(char) - 0xDC00) + else: + byte = char.encode(encoding) + encoded.append(byte) + return ''.join(encoded) + +class StringTruncated(RuntimeError): + pass + +class TruncatedStringIO(object): + '''Similar to io.StringIO, but can truncate the output by raising a + StringTruncated exception''' + def __init__(self, maxlen=None): + self._val = '' + self.maxlen = maxlen + + def write(self, data): + if self.maxlen: + if len(data) + len(self._val) > self.maxlen: + # Truncation: + self._val += data[0:self.maxlen - len(self._val)] + raise StringTruncated() + + self._val += data + + def getvalue(self): + return self._val + +class PyObjectPtr(object): + """ + Class wrapping a gdb.Value that's either a (PyObject*) within the + inferior process, or some subclass pointer e.g. (PyBytesObject*) + + There will be a subclass for every refined PyObject type that we care + about. + + Note that at every stage the underlying pointer could be NULL, point + to corrupt data, etc; this is the debugger, after all. + """ + _typename = 'PyObject' + + def __init__(self, gdbval, cast_to=None): + if cast_to: + self._gdbval = gdbval.cast(cast_to) + else: + self._gdbval = gdbval + + def field(self, name): + ''' + Get the gdb.Value for the given field within the PyObject, coping with + some python 2 versus python 3 differences. + + Various libpython types are defined using the "PyObject_HEAD" and + "PyObject_VAR_HEAD" macros. + + In Python 2, this these are defined so that "ob_type" and (for a var + object) "ob_size" are fields of the type in question. + + In Python 3, this is defined as an embedded PyVarObject type thus: + PyVarObject ob_base; + so that the "ob_size" field is located insize the "ob_base" field, and + the "ob_type" is most easily accessed by casting back to a (PyObject*). + ''' + if self.is_null(): + raise NullPyObjectPtr(self) + + if name == 'ob_type': + pyo_ptr = self._gdbval.cast(PyObjectPtr.get_gdb_type()) + return pyo_ptr.dereference()[name] + + if name == 'ob_size': + pyo_ptr = self._gdbval.cast(PyVarObjectPtr.get_gdb_type()) + return pyo_ptr.dereference()[name] + + # General case: look it up inside the object: + return self._gdbval.dereference()[name] + + def pyop_field(self, name): + ''' + Get a PyObjectPtr for the given PyObject* field within this PyObject, + coping with some python 2 versus python 3 differences. + ''' + return PyObjectPtr.from_pyobject_ptr(self.field(name)) + + def write_field_repr(self, name, out, visited): + ''' + Extract the PyObject* field named "name", and write its representation + to file-like object "out" + ''' + field_obj = self.pyop_field(name) + field_obj.write_repr(out, visited) + + def get_truncated_repr(self, maxlen): + ''' + Get a repr-like string for the data, but truncate it at "maxlen" bytes + (ending the object graph traversal as soon as you do) + ''' + out = TruncatedStringIO(maxlen) + try: + self.write_repr(out, set()) + except StringTruncated: + # Truncation occurred: + return out.getvalue() + '...(truncated)' + + # No truncation occurred: + return out.getvalue() + + def type(self): + return PyTypeObjectPtr(self.field('ob_type')) + + def is_null(self): + return 0 == long(self._gdbval) + + def is_optimized_out(self): + ''' + Is the value of the underlying PyObject* visible to the debugger? + + This can vary with the precise version of the compiler used to build + Python, and the precise version of gdb. + + See e.g. https://bugzilla.redhat.com/show_bug.cgi?id=556975 with + PyEval_EvalFrameEx's "f" + ''' + return self._gdbval.is_optimized_out + + def safe_tp_name(self): + try: + return self.type().field('tp_name').string() + except NullPyObjectPtr: + # NULL tp_name? + return 'unknown' + except RuntimeError: + # Can't even read the object at all? + return 'unknown' + + def proxyval(self, visited): + ''' + Scrape a value from the inferior process, and try to represent it + within the gdb process, whilst (hopefully) avoiding crashes when + the remote data is corrupt. + + Derived classes will override this. + + For example, a PyIntObject* with ob_ival 42 in the inferior process + should result in an int(42) in this process. + + visited: a set of all gdb.Value pyobject pointers already visited + whilst generating this value (to guard against infinite recursion when + visiting object graphs with loops). Analogous to Py_ReprEnter and + Py_ReprLeave + ''' + + class FakeRepr(object): + """ + Class representing a non-descript PyObject* value in the inferior + process for when we don't have a custom scraper, intended to have + a sane repr(). + """ + + def __init__(self, tp_name, address): + self.tp_name = tp_name + self.address = address + + def __repr__(self): + # For the NULL pointer, we have no way of knowing a type, so + # special-case it as per + # http://bugs.python.org/issue8032#msg100882 + if self.address == 0: + return '0x0' + return '<%s at remote 0x%x>' % (self.tp_name, self.address) + + return FakeRepr(self.safe_tp_name(), + long(self._gdbval)) + + def write_repr(self, out, visited): + ''' + Write a string representation of the value scraped from the inferior + process to "out", a file-like object. + ''' + # Default implementation: generate a proxy value and write its repr + # However, this could involve a lot of work for complicated objects, + # so for derived classes we specialize this + return out.write(repr(self.proxyval(visited))) + + @classmethod + def subclass_from_type(cls, t): + ''' + Given a PyTypeObjectPtr instance wrapping a gdb.Value that's a + (PyTypeObject*), determine the corresponding subclass of PyObjectPtr + to use + + Ideally, we would look up the symbols for the global types, but that + isn't working yet: + (gdb) python print gdb.lookup_symbol('PyList_Type')[0].value + Traceback (most recent call last): + File "<string>", line 1, in <module> + NotImplementedError: Symbol type not yet supported in Python scripts. + Error while executing Python code. + + For now, we use tp_flags, after doing some string comparisons on the + tp_name for some special-cases that don't seem to be visible through + flags + ''' + try: + tp_name = t.field('tp_name').string() + tp_flags = int(t.field('tp_flags')) + except RuntimeError: + # Handle any kind of error e.g. NULL ptrs by simply using the base + # class + return cls + + #print('tp_flags = 0x%08x' % tp_flags) + #print('tp_name = %r' % tp_name) + + name_map = {'bool': PyBoolObjectPtr, + 'classobj': PyClassObjectPtr, + 'NoneType': PyNoneStructPtr, + 'frame': PyFrameObjectPtr, + 'set' : PySetObjectPtr, + 'frozenset' : PySetObjectPtr, + 'builtin_function_or_method' : PyCFunctionObjectPtr, + 'method-wrapper': wrapperobject, + } + if tp_name in name_map: + return name_map[tp_name] + + if tp_flags & Py_TPFLAGS_HEAPTYPE: + return HeapTypeObjectPtr + + if tp_flags & Py_TPFLAGS_LONG_SUBCLASS: + return PyLongObjectPtr + if tp_flags & Py_TPFLAGS_LIST_SUBCLASS: + return PyListObjectPtr + if tp_flags & Py_TPFLAGS_TUPLE_SUBCLASS: + return PyTupleObjectPtr + if tp_flags & Py_TPFLAGS_BYTES_SUBCLASS: + return PyBytesObjectPtr + if tp_flags & Py_TPFLAGS_UNICODE_SUBCLASS: + return PyUnicodeObjectPtr + if tp_flags & Py_TPFLAGS_DICT_SUBCLASS: + return PyDictObjectPtr + if tp_flags & Py_TPFLAGS_BASE_EXC_SUBCLASS: + return PyBaseExceptionObjectPtr + #if tp_flags & Py_TPFLAGS_TYPE_SUBCLASS: + # return PyTypeObjectPtr + + # Use the base class: + return cls + + @classmethod + def from_pyobject_ptr(cls, gdbval): + ''' + Try to locate the appropriate derived class dynamically, and cast + the pointer accordingly. + ''' + try: + p = PyObjectPtr(gdbval) + cls = cls.subclass_from_type(p.type()) + return cls(gdbval, cast_to=cls.get_gdb_type()) + except RuntimeError: + # Handle any kind of error e.g. NULL ptrs by simply using the base + # class + pass + return cls(gdbval) + + @classmethod + def get_gdb_type(cls): + return gdb.lookup_type(cls._typename).pointer() + + def as_address(self): + return long(self._gdbval) + +class PyVarObjectPtr(PyObjectPtr): + _typename = 'PyVarObject' + +class ProxyAlreadyVisited(object): + ''' + Placeholder proxy to use when protecting against infinite recursion due to + loops in the object graph. + + Analogous to the values emitted by the users of Py_ReprEnter and Py_ReprLeave + ''' + def __init__(self, rep): + self._rep = rep + + def __repr__(self): + return self._rep + + +def _write_instance_repr(out, visited, name, pyop_attrdict, address): + '''Shared code for use by all classes: + write a representation to file-like object "out"''' + out.write('<') + out.write(name) + + # Write dictionary of instance attributes: + if isinstance(pyop_attrdict, PyDictObjectPtr): + out.write('(') + first = True + for pyop_arg, pyop_val in pyop_attrdict.iteritems(): + if not first: + out.write(', ') + first = False + out.write(pyop_arg.proxyval(visited)) + out.write('=') + pyop_val.write_repr(out, visited) + out.write(')') + out.write(' at remote 0x%x>' % address) + + +class InstanceProxy(object): + + def __init__(self, cl_name, attrdict, address): + self.cl_name = cl_name + self.attrdict = attrdict + self.address = address + + def __repr__(self): + if isinstance(self.attrdict, dict): + kwargs = ', '.join(["%s=%r" % (arg, val) + for arg, val in self.attrdict.iteritems()]) + return '<%s(%s) at remote 0x%x>' % (self.cl_name, + kwargs, self.address) + else: + return '<%s at remote 0x%x>' % (self.cl_name, + self.address) + +def _PyObject_VAR_SIZE(typeobj, nitems): + if _PyObject_VAR_SIZE._type_size_t is None: + _PyObject_VAR_SIZE._type_size_t = gdb.lookup_type('size_t') + + return ( ( typeobj.field('tp_basicsize') + + nitems * typeobj.field('tp_itemsize') + + (_sizeof_void_p() - 1) + ) & ~(_sizeof_void_p() - 1) + ).cast(_PyObject_VAR_SIZE._type_size_t) +_PyObject_VAR_SIZE._type_size_t = None + +class HeapTypeObjectPtr(PyObjectPtr): + _typename = 'PyObject' + + def get_attr_dict(self): + ''' + Get the PyDictObject ptr representing the attribute dictionary + (or None if there's a problem) + ''' + try: + typeobj = self.type() + dictoffset = int_from_int(typeobj.field('tp_dictoffset')) + if dictoffset != 0: + if dictoffset < 0: + type_PyVarObject_ptr = gdb.lookup_type('PyVarObject').pointer() + tsize = int_from_int(self._gdbval.cast(type_PyVarObject_ptr)['ob_size']) + if tsize < 0: + tsize = -tsize + size = _PyObject_VAR_SIZE(typeobj, tsize) + dictoffset += size + assert dictoffset > 0 + assert dictoffset % _sizeof_void_p() == 0 + + dictptr = self._gdbval.cast(_type_char_ptr()) + dictoffset + PyObjectPtrPtr = PyObjectPtr.get_gdb_type().pointer() + dictptr = dictptr.cast(PyObjectPtrPtr) + return PyObjectPtr.from_pyobject_ptr(dictptr.dereference()) + except RuntimeError: + # Corrupt data somewhere; fail safe + pass + + # Not found, or some kind of error: + return None + + def proxyval(self, visited): + ''' + Support for classes. + + Currently we just locate the dictionary using a transliteration to + python of _PyObject_GetDictPtr, ignoring descriptors + ''' + # Guard against infinite loops: + if self.as_address() in visited: + return ProxyAlreadyVisited('<...>') + visited.add(self.as_address()) + + pyop_attr_dict = self.get_attr_dict() + if pyop_attr_dict: + attr_dict = pyop_attr_dict.proxyval(visited) + else: + attr_dict = {} + tp_name = self.safe_tp_name() + + # Class: + return InstanceProxy(tp_name, attr_dict, long(self._gdbval)) + + def write_repr(self, out, visited): + # Guard against infinite loops: + if self.as_address() in visited: + out.write('<...>') + return + visited.add(self.as_address()) + + pyop_attrdict = self.get_attr_dict() + _write_instance_repr(out, visited, + self.safe_tp_name(), pyop_attrdict, self.as_address()) + +class ProxyException(Exception): + def __init__(self, tp_name, args): + self.tp_name = tp_name + self.args = args + + def __repr__(self): + return '%s%r' % (self.tp_name, self.args) + +class PyBaseExceptionObjectPtr(PyObjectPtr): + """ + Class wrapping a gdb.Value that's a PyBaseExceptionObject* i.e. an exception + within the process being debugged. + """ + _typename = 'PyBaseExceptionObject' + + def proxyval(self, visited): + # Guard against infinite loops: + if self.as_address() in visited: + return ProxyAlreadyVisited('(...)') + visited.add(self.as_address()) + arg_proxy = self.pyop_field('args').proxyval(visited) + return ProxyException(self.safe_tp_name(), + arg_proxy) + + def write_repr(self, out, visited): + # Guard against infinite loops: + if self.as_address() in visited: + out.write('(...)') + return + visited.add(self.as_address()) + + out.write(self.safe_tp_name()) + self.write_field_repr('args', out, visited) + +class PyClassObjectPtr(PyObjectPtr): + """ + Class wrapping a gdb.Value that's a PyClassObject* i.e. a <classobj> + instance within the process being debugged. + """ + _typename = 'PyClassObject' + + +class BuiltInFunctionProxy(object): + def __init__(self, ml_name): + self.ml_name = ml_name + + def __repr__(self): + return "<built-in function %s>" % self.ml_name + +class BuiltInMethodProxy(object): + def __init__(self, ml_name, pyop_m_self): + self.ml_name = ml_name + self.pyop_m_self = pyop_m_self + + def __repr__(self): + return ('<built-in method %s of %s object at remote 0x%x>' + % (self.ml_name, + self.pyop_m_self.safe_tp_name(), + self.pyop_m_self.as_address()) + ) + +class PyCFunctionObjectPtr(PyObjectPtr): + """ + Class wrapping a gdb.Value that's a PyCFunctionObject* + (see Include/methodobject.h and Objects/methodobject.c) + """ + _typename = 'PyCFunctionObject' + + def proxyval(self, visited): + m_ml = self.field('m_ml') # m_ml is a (PyMethodDef*) + ml_name = m_ml['ml_name'].string() + + pyop_m_self = self.pyop_field('m_self') + if pyop_m_self.is_null(): + return BuiltInFunctionProxy(ml_name) + else: + return BuiltInMethodProxy(ml_name, pyop_m_self) + + +class PyCodeObjectPtr(PyObjectPtr): + """ + Class wrapping a gdb.Value that's a PyCodeObject* i.e. a <code> instance + within the process being debugged. + """ + _typename = 'PyCodeObject' + + def addr2line(self, addrq): + ''' + Get the line number for a given bytecode offset + + Analogous to PyCode_Addr2Line; translated from pseudocode in + Objects/lnotab_notes.txt + ''' + co_lnotab = self.pyop_field('co_lnotab').proxyval(set()) + + # Initialize lineno to co_firstlineno as per PyCode_Addr2Line + # not 0, as lnotab_notes.txt has it: + lineno = int_from_int(self.field('co_firstlineno')) + + addr = 0 + for addr_incr, line_incr in zip(co_lnotab[::2], co_lnotab[1::2]): + addr += ord(addr_incr) + if addr > addrq: + return lineno + lineno += ord(line_incr) + return lineno + + +class PyDictObjectPtr(PyObjectPtr): + """ + Class wrapping a gdb.Value that's a PyDictObject* i.e. a dict instance + within the process being debugged. + """ + _typename = 'PyDictObject' + + def iteritems(self): + ''' + Yields a sequence of (PyObjectPtr key, PyObjectPtr value) pairs, + analogous to dict.iteritems() + ''' + keys = self.field('ma_keys') + values = self.field('ma_values') + entries, nentries = self._get_entries(keys) + for i in safe_range(nentries): + ep = entries[i] + if long(values): + pyop_value = PyObjectPtr.from_pyobject_ptr(values[i]) + else: + pyop_value = PyObjectPtr.from_pyobject_ptr(ep['me_value']) + if not pyop_value.is_null(): + pyop_key = PyObjectPtr.from_pyobject_ptr(ep['me_key']) + yield (pyop_key, pyop_value) + + def proxyval(self, visited): + # Guard against infinite loops: + if self.as_address() in visited: + return ProxyAlreadyVisited('{...}') + visited.add(self.as_address()) + + result = {} + for pyop_key, pyop_value in self.iteritems(): + proxy_key = pyop_key.proxyval(visited) + proxy_value = pyop_value.proxyval(visited) + result[proxy_key] = proxy_value + return result + + def write_repr(self, out, visited): + # Guard against infinite loops: + if self.as_address() in visited: + out.write('{...}') + return + visited.add(self.as_address()) + + out.write('{') + first = True + for pyop_key, pyop_value in self.iteritems(): + if not first: + out.write(', ') + first = False + pyop_key.write_repr(out, visited) + out.write(': ') + pyop_value.write_repr(out, visited) + out.write('}') + + def _get_entries(self, keys): + dk_nentries = int(keys['dk_nentries']) + dk_size = int(keys['dk_size']) + try: + # <= Python 3.5 + return keys['dk_entries'], dk_size + except RuntimeError: + # >= Python 3.6 + pass + + if dk_size <= 0xFF: + offset = dk_size + elif dk_size <= 0xFFFF: + offset = 2 * dk_size + elif dk_size <= 0xFFFFFFFF: + offset = 4 * dk_size + else: + offset = 8 * dk_size + + ent_addr = keys['dk_indices']['as_1'].address + ent_addr = ent_addr.cast(_type_unsigned_char_ptr()) + offset + ent_ptr_t = gdb.lookup_type('PyDictKeyEntry').pointer() + ent_addr = ent_addr.cast(ent_ptr_t) + + return ent_addr, dk_nentries + + +class PyListObjectPtr(PyObjectPtr): + _typename = 'PyListObject' + + def __getitem__(self, i): + # Get the gdb.Value for the (PyObject*) with the given index: + field_ob_item = self.field('ob_item') + return field_ob_item[i] + + def proxyval(self, visited): + # Guard against infinite loops: + if self.as_address() in visited: + return ProxyAlreadyVisited('[...]') + visited.add(self.as_address()) + + result = [PyObjectPtr.from_pyobject_ptr(self[i]).proxyval(visited) + for i in safe_range(int_from_int(self.field('ob_size')))] + return result + + def write_repr(self, out, visited): + # Guard against infinite loops: + if self.as_address() in visited: + out.write('[...]') + return + visited.add(self.as_address()) + + out.write('[') + for i in safe_range(int_from_int(self.field('ob_size'))): + if i > 0: + out.write(', ') + element = PyObjectPtr.from_pyobject_ptr(self[i]) + element.write_repr(out, visited) + out.write(']') + +class PyLongObjectPtr(PyObjectPtr): + _typename = 'PyLongObject' + + def proxyval(self, visited): + ''' + Python's Include/longobjrep.h has this declaration: + struct _longobject { + PyObject_VAR_HEAD + digit ob_digit[1]; + }; + + with this description: + The absolute value of a number is equal to + SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i) + Negative numbers are represented with ob_size < 0; + zero is represented by ob_size == 0. + + where SHIFT can be either: + #define PyLong_SHIFT 30 + #define PyLong_SHIFT 15 + ''' + ob_size = long(self.field('ob_size')) + if ob_size == 0: + return 0 + + ob_digit = self.field('ob_digit') + + if gdb.lookup_type('digit').sizeof == 2: + SHIFT = 15 + else: + SHIFT = 30 + + digits = [long(ob_digit[i]) * 2**(SHIFT*i) + for i in safe_range(abs(ob_size))] + result = sum(digits) + if ob_size < 0: + result = -result + return result + + def write_repr(self, out, visited): + # Write this out as a Python 3 int literal, i.e. without the "L" suffix + proxy = self.proxyval(visited) + out.write("%s" % proxy) + + +class PyBoolObjectPtr(PyLongObjectPtr): + """ + Class wrapping a gdb.Value that's a PyBoolObject* i.e. one of the two + <bool> instances (Py_True/Py_False) within the process being debugged. + """ + def proxyval(self, visited): + if PyLongObjectPtr.proxyval(self, visited): + return True + else: + return False + +class PyNoneStructPtr(PyObjectPtr): + """ + Class wrapping a gdb.Value that's a PyObject* pointing to the + singleton (we hope) _Py_NoneStruct with ob_type PyNone_Type + """ + _typename = 'PyObject' + + def proxyval(self, visited): + return None + + +class PyFrameObjectPtr(PyObjectPtr): + _typename = 'PyFrameObject' + + def __init__(self, gdbval, cast_to=None): + PyObjectPtr.__init__(self, gdbval, cast_to) + + if not self.is_optimized_out(): + self.co = PyCodeObjectPtr.from_pyobject_ptr(self.field('f_code')) + self.co_name = self.co.pyop_field('co_name') + self.co_filename = self.co.pyop_field('co_filename') + + self.f_lineno = int_from_int(self.field('f_lineno')) + self.f_lasti = int_from_int(self.field('f_lasti')) + self.co_nlocals = int_from_int(self.co.field('co_nlocals')) + self.co_varnames = PyTupleObjectPtr.from_pyobject_ptr(self.co.field('co_varnames')) + + def iter_locals(self): + ''' + Yield a sequence of (name,value) pairs of PyObjectPtr instances, for + the local variables of this frame + ''' + if self.is_optimized_out(): + return + + f_localsplus = self.field('f_localsplus') + for i in safe_range(self.co_nlocals): + pyop_value = PyObjectPtr.from_pyobject_ptr(f_localsplus[i]) + if not pyop_value.is_null(): + pyop_name = PyObjectPtr.from_pyobject_ptr(self.co_varnames[i]) + yield (pyop_name, pyop_value) + + def iter_globals(self): + ''' + Yield a sequence of (name,value) pairs of PyObjectPtr instances, for + the global variables of this frame + ''' + if self.is_optimized_out(): + return () + + pyop_globals = self.pyop_field('f_globals') + return pyop_globals.iteritems() + + def iter_builtins(self): + ''' + Yield a sequence of (name,value) pairs of PyObjectPtr instances, for + the builtin variables + ''' + if self.is_optimized_out(): + return () + + pyop_builtins = self.pyop_field('f_builtins') + return pyop_builtins.iteritems() + + def get_var_by_name(self, name): + ''' + Look for the named local variable, returning a (PyObjectPtr, scope) pair + where scope is a string 'local', 'global', 'builtin' + + If not found, return (None, None) + ''' + for pyop_name, pyop_value in self.iter_locals(): + if name == pyop_name.proxyval(set()): + return pyop_value, 'local' + for pyop_name, pyop_value in self.iter_globals(): + if name == pyop_name.proxyval(set()): + return pyop_value, 'global' + for pyop_name, pyop_value in self.iter_builtins(): + if name == pyop_name.proxyval(set()): + return pyop_value, 'builtin' + return None, None + + def filename(self): + '''Get the path of the current Python source file, as a string''' + if self.is_optimized_out(): + return '(frame information optimized out)' + return self.co_filename.proxyval(set()) + + def current_line_num(self): + '''Get current line number as an integer (1-based) + + Translated from PyFrame_GetLineNumber and PyCode_Addr2Line + + See Objects/lnotab_notes.txt + ''' + if self.is_optimized_out(): + return None + f_trace = self.field('f_trace') + if long(f_trace) != 0: + # we have a non-NULL f_trace: + return self.f_lineno + else: + #try: + return self.co.addr2line(self.f_lasti) + #except ValueError: + # return self.f_lineno + + def current_line(self): + '''Get the text of the current source line as a string, with a trailing + newline character''' + if self.is_optimized_out(): + return '(frame information optimized out)' + filename = self.filename() + try: + f = open(os_fsencode(filename), 'r') + except IOError: + return None + with f: + all_lines = f.readlines() + # Convert from 1-based current_line_num to 0-based list offset: + return all_lines[self.current_line_num()-1] + + def write_repr(self, out, visited): + if self.is_optimized_out(): + out.write('(frame information optimized out)') + return + out.write('Frame 0x%x, for file %s, line %i, in %s (' + % (self.as_address(), + self.co_filename.proxyval(visited), + self.current_line_num(), + self.co_name.proxyval(visited))) + first = True + for pyop_name, pyop_value in self.iter_locals(): + if not first: + out.write(', ') + first = False + + out.write(pyop_name.proxyval(visited)) + out.write('=') + pyop_value.write_repr(out, visited) + + out.write(')') + + def print_traceback(self): + if self.is_optimized_out(): + sys.stdout.write(' (frame information optimized out)\n') + return + visited = set() + sys.stdout.write(' File "%s", line %i, in %s\n' + % (self.co_filename.proxyval(visited), + self.current_line_num(), + self.co_name.proxyval(visited))) + +class PySetObjectPtr(PyObjectPtr): + _typename = 'PySetObject' + + @classmethod + def _dummy_key(self): + return gdb.lookup_global_symbol('_PySet_Dummy').value() + + def __iter__(self): + dummy_ptr = self._dummy_key() + table = self.field('table') + for i in safe_range(self.field('mask') + 1): + setentry = table[i] + key = setentry['key'] + if key != 0 and key != dummy_ptr: + yield PyObjectPtr.from_pyobject_ptr(key) + + def proxyval(self, visited): + # Guard against infinite loops: + if self.as_address() in visited: + return ProxyAlreadyVisited('%s(...)' % self.safe_tp_name()) + visited.add(self.as_address()) + + members = (key.proxyval(visited) for key in self) + if self.safe_tp_name() == 'frozenset': + return frozenset(members) + else: + return set(members) + + def write_repr(self, out, visited): + # Emulate Python 3's set_repr + tp_name = self.safe_tp_name() + + # Guard against infinite loops: + if self.as_address() in visited: + out.write('(...)') + return + visited.add(self.as_address()) + + # Python 3's set_repr special-cases the empty set: + if not self.field('used'): + out.write(tp_name) + out.write('()') + return + + # Python 3 uses {} for set literals: + if tp_name != 'set': + out.write(tp_name) + out.write('(') + + out.write('{') + first = True + for key in self: + if not first: + out.write(', ') + first = False + key.write_repr(out, visited) + out.write('}') + + if tp_name != 'set': + out.write(')') + + +class PyBytesObjectPtr(PyObjectPtr): + _typename = 'PyBytesObject' + + def __str__(self): + field_ob_size = self.field('ob_size') + field_ob_sval = self.field('ob_sval') + char_ptr = field_ob_sval.address.cast(_type_unsigned_char_ptr()) + return ''.join([chr(char_ptr[i]) for i in safe_range(field_ob_size)]) + + def proxyval(self, visited): + return str(self) + + def write_repr(self, out, visited): + # Write this out as a Python 3 bytes literal, i.e. with a "b" prefix + + # Get a PyStringObject* within the Python 2 gdb process: + proxy = self.proxyval(visited) + + # Transliteration of Python 3's Objects/bytesobject.c:PyBytes_Repr + # to Python 2 code: + quote = "'" + if "'" in proxy and not '"' in proxy: + quote = '"' + out.write('b') + out.write(quote) + for byte in proxy: + if byte == quote or byte == '\\': + out.write('\\') + out.write(byte) + elif byte == '\t': + out.write('\\t') + elif byte == '\n': + out.write('\\n') + elif byte == '\r': + out.write('\\r') + elif byte < ' ' or ord(byte) >= 0x7f: + out.write('\\x') + out.write(hexdigits[(ord(byte) & 0xf0) >> 4]) + out.write(hexdigits[ord(byte) & 0xf]) + else: + out.write(byte) + out.write(quote) + + +class PyStringObjectPtr(PyBytesObjectPtr): + _typename = 'PyStringObject' + + +class PyTupleObjectPtr(PyObjectPtr): + _typename = 'PyTupleObject' + + def __getitem__(self, i): + # Get the gdb.Value for the (PyObject*) with the given index: + field_ob_item = self.field('ob_item') + return field_ob_item[i] + + def proxyval(self, visited): + # Guard against infinite loops: + if self.as_address() in visited: + return ProxyAlreadyVisited('(...)') + visited.add(self.as_address()) + + result = tuple(PyObjectPtr.from_pyobject_ptr(self[i]).proxyval(visited) + for i in safe_range(int_from_int(self.field('ob_size')))) + return result + + def write_repr(self, out, visited): + # Guard against infinite loops: + if self.as_address() in visited: + out.write('(...)') + return + visited.add(self.as_address()) + + out.write('(') + for i in safe_range(int_from_int(self.field('ob_size'))): + if i > 0: + out.write(', ') + element = PyObjectPtr.from_pyobject_ptr(self[i]) + element.write_repr(out, visited) + if self.field('ob_size') == 1: + out.write(',)') + else: + out.write(')') + +class PyTypeObjectPtr(PyObjectPtr): + _typename = 'PyTypeObject' + + +def _unichr_is_printable(char): + # Logic adapted from Python 3's Tools/unicode/makeunicodedata.py + if char == u" ": + return True + import unicodedata + return unicodedata.category(char) not in ("C", "Z") + +if sys.maxunicode >= 0x10000: + _unichr = unichr +else: + # Needed for proper surrogate support if sizeof(Py_UNICODE) is 2 in gdb + def _unichr(x): + if x < 0x10000: + return unichr(x) + x -= 0x10000 + ch1 = 0xD800 | (x >> 10) + ch2 = 0xDC00 | (x & 0x3FF) + return unichr(ch1) + unichr(ch2) + + +class PyUnicodeObjectPtr(PyObjectPtr): + _typename = 'PyUnicodeObject' + + def char_width(self): + _type_Py_UNICODE = gdb.lookup_type('Py_UNICODE') + return _type_Py_UNICODE.sizeof + + def proxyval(self, visited): + global _is_pep393 + if _is_pep393 is None: + fields = gdb.lookup_type('PyUnicodeObject').target().fields() + _is_pep393 = 'data' in [f.name for f in fields] + if _is_pep393: + # Python 3.3 and newer + may_have_surrogates = False + compact = self.field('_base') + ascii = compact['_base'] + state = ascii['state'] + is_compact_ascii = (int(state['ascii']) and int(state['compact'])) + if not int(state['ready']): + # string is not ready + field_length = long(compact['wstr_length']) + may_have_surrogates = True + field_str = ascii['wstr'] + else: + field_length = long(ascii['length']) + if is_compact_ascii: + field_str = ascii.address + 1 + elif int(state['compact']): + field_str = compact.address + 1 + else: + field_str = self.field('data')['any'] + repr_kind = int(state['kind']) + if repr_kind == 1: + field_str = field_str.cast(_type_unsigned_char_ptr()) + elif repr_kind == 2: + field_str = field_str.cast(_type_unsigned_short_ptr()) + elif repr_kind == 4: + field_str = field_str.cast(_type_unsigned_int_ptr()) + else: + # Python 3.2 and earlier + field_length = long(self.field('length')) + field_str = self.field('str') + may_have_surrogates = self.char_width() == 2 + + # Gather a list of ints from the Py_UNICODE array; these are either + # UCS-1, UCS-2 or UCS-4 code points: + if not may_have_surrogates: + Py_UNICODEs = [int(field_str[i]) for i in safe_range(field_length)] + else: + # A more elaborate routine if sizeof(Py_UNICODE) is 2 in the + # inferior process: we must join surrogate pairs. + Py_UNICODEs = [] + i = 0 + limit = safety_limit(field_length) + while i < limit: + ucs = int(field_str[i]) + i += 1 + if ucs < 0xD800 or ucs >= 0xDC00 or i == field_length: + Py_UNICODEs.append(ucs) + continue + # This could be a surrogate pair. + ucs2 = int(field_str[i]) + if ucs2 < 0xDC00 or ucs2 > 0xDFFF: + continue + code = (ucs & 0x03FF) << 10 + code |= ucs2 & 0x03FF + code += 0x00010000 + Py_UNICODEs.append(code) + i += 1 + + # Convert the int code points to unicode characters, and generate a + # local unicode instance. + # This splits surrogate pairs if sizeof(Py_UNICODE) is 2 here (in gdb). + result = u''.join([ + (_unichr(ucs) if ucs <= 0x10ffff else '\ufffd') + for ucs in Py_UNICODEs]) + return result + + def write_repr(self, out, visited): + # Write this out as a Python 3 str literal, i.e. without a "u" prefix + + # Get a PyUnicodeObject* within the Python 2 gdb process: + proxy = self.proxyval(visited) + + # Transliteration of Python 3's Object/unicodeobject.c:unicode_repr + # to Python 2: + if "'" in proxy and '"' not in proxy: + quote = '"' + else: + quote = "'" + out.write(quote) + + i = 0 + while i < len(proxy): + ch = proxy[i] + i += 1 + + # Escape quotes and backslashes + if ch == quote or ch == '\\': + out.write('\\') + out.write(ch) + + # Map special whitespace to '\t', \n', '\r' + elif ch == '\t': + out.write('\\t') + elif ch == '\n': + out.write('\\n') + elif ch == '\r': + out.write('\\r') + + # Map non-printable US ASCII to '\xhh' */ + elif ch < ' ' or ch == 0x7F: + out.write('\\x') + out.write(hexdigits[(ord(ch) >> 4) & 0x000F]) + out.write(hexdigits[ord(ch) & 0x000F]) + + # Copy ASCII characters as-is + elif ord(ch) < 0x7F: + out.write(ch) + + # Non-ASCII characters + else: + ucs = ch + ch2 = None + if sys.maxunicode < 0x10000: + # If sizeof(Py_UNICODE) is 2 here (in gdb), join + # surrogate pairs before calling _unichr_is_printable. + if (i < len(proxy) + and 0xD800 <= ord(ch) < 0xDC00 \ + and 0xDC00 <= ord(proxy[i]) <= 0xDFFF): + ch2 = proxy[i] + ucs = ch + ch2 + i += 1 + + # Unfortuately, Python 2's unicode type doesn't seem + # to expose the "isprintable" method + printable = _unichr_is_printable(ucs) + if printable: + try: + ucs.encode(ENCODING) + except UnicodeEncodeError: + printable = False + + # Map Unicode whitespace and control characters + # (categories Z* and C* except ASCII space) + if not printable: + if ch2 is not None: + # Match Python 3's representation of non-printable + # wide characters. + code = (ord(ch) & 0x03FF) << 10 + code |= ord(ch2) & 0x03FF + code += 0x00010000 + else: + code = ord(ucs) + + # Map 8-bit characters to '\\xhh' + if code <= 0xff: + out.write('\\x') + out.write(hexdigits[(code >> 4) & 0x000F]) + out.write(hexdigits[code & 0x000F]) + # Map 21-bit characters to '\U00xxxxxx' + elif code >= 0x10000: + out.write('\\U') + out.write(hexdigits[(code >> 28) & 0x0000000F]) + out.write(hexdigits[(code >> 24) & 0x0000000F]) + out.write(hexdigits[(code >> 20) & 0x0000000F]) + out.write(hexdigits[(code >> 16) & 0x0000000F]) + out.write(hexdigits[(code >> 12) & 0x0000000F]) + out.write(hexdigits[(code >> 8) & 0x0000000F]) + out.write(hexdigits[(code >> 4) & 0x0000000F]) + out.write(hexdigits[code & 0x0000000F]) + # Map 16-bit characters to '\uxxxx' + else: + out.write('\\u') + out.write(hexdigits[(code >> 12) & 0x000F]) + out.write(hexdigits[(code >> 8) & 0x000F]) + out.write(hexdigits[(code >> 4) & 0x000F]) + out.write(hexdigits[code & 0x000F]) + else: + # Copy characters as-is + out.write(ch) + if ch2 is not None: + out.write(ch2) + + out.write(quote) + + +class wrapperobject(PyObjectPtr): + _typename = 'wrapperobject' + + def safe_name(self): + try: + name = self.field('descr')['d_base']['name'].string() + return repr(name) + except (NullPyObjectPtr, RuntimeError): + return '<unknown name>' + + def safe_tp_name(self): + try: + return self.field('self')['ob_type']['tp_name'].string() + except (NullPyObjectPtr, RuntimeError): + return '<unknown tp_name>' + + def safe_self_addresss(self): + try: + address = long(self.field('self')) + return '%#x' % address + except (NullPyObjectPtr, RuntimeError): + return '<failed to get self address>' + + def proxyval(self, visited): + name = self.safe_name() + tp_name = self.safe_tp_name() + self_address = self.safe_self_addresss() + return ("<method-wrapper %s of %s object at %s>" + % (name, tp_name, self_address)) + + def write_repr(self, out, visited): + proxy = self.proxyval(visited) + out.write(proxy) + + +def int_from_int(gdbval): + return int(str(gdbval)) + + +def stringify(val): + # TODO: repr() puts everything on one line; pformat can be nicer, but + # can lead to v.long results; this function isolates the choice + if True: + return repr(val) + else: + from pprint import pformat + return pformat(val) + + +class PyObjectPtrPrinter: + "Prints a (PyObject*)" + + def __init__ (self, gdbval): + self.gdbval = gdbval + + def to_string (self): + pyop = PyObjectPtr.from_pyobject_ptr(self.gdbval) + if True: + return pyop.get_truncated_repr(MAX_OUTPUT_LEN) + else: + # Generate full proxy value then stringify it. + # Doing so could be expensive + proxyval = pyop.proxyval(set()) + return stringify(proxyval) + +def pretty_printer_lookup(gdbval): + type = gdbval.type.unqualified() + if type.code != gdb.TYPE_CODE_PTR: + return None + + type = type.target().unqualified() + t = str(type) + if t in ("PyObject", "PyFrameObject", "PyUnicodeObject", "wrapperobject"): + return PyObjectPtrPrinter(gdbval) + +""" +During development, I've been manually invoking the code in this way: +(gdb) python + +import sys +sys.path.append('/home/david/coding/python-gdb') +import libpython +end + +then reloading it after each edit like this: +(gdb) python reload(libpython) + +The following code should ensure that the prettyprinter is registered +if the code is autoloaded by gdb when visiting libpython.so, provided +that this python file is installed to the same path as the library (or its +.debug file) plus a "-gdb.py" suffix, e.g: + /usr/lib/libpython2.6.so.1.0-gdb.py + /usr/lib/debug/usr/lib/libpython2.6.so.1.0.debug-gdb.py +""" +def register (obj): + if obj is None: + obj = gdb + + # Wire up the pretty-printer + obj.pretty_printers.append(pretty_printer_lookup) + +register (gdb.current_objfile ()) + + + +# Unfortunately, the exact API exposed by the gdb module varies somewhat +# from build to build +# See http://bugs.python.org/issue8279?#msg102276 + +class Frame(object): + ''' + Wrapper for gdb.Frame, adding various methods + ''' + def __init__(self, gdbframe): + self._gdbframe = gdbframe + + def older(self): + older = self._gdbframe.older() + if older: + return Frame(older) + else: + return None + + def newer(self): + newer = self._gdbframe.newer() + if newer: + return Frame(newer) + else: + return None + + def select(self): + '''If supported, select this frame and return True; return False if unsupported + + Not all builds have a gdb.Frame.select method; seems to be present on Fedora 12 + onwards, but absent on Ubuntu buildbot''' + if not hasattr(self._gdbframe, 'select'): + print ('Unable to select frame: ' + 'this build of gdb does not expose a gdb.Frame.select method') + return False + self._gdbframe.select() + return True + + def get_index(self): + '''Calculate index of frame, starting at 0 for the newest frame within + this thread''' + index = 0 + # Go down until you reach the newest frame: + iter_frame = self + while iter_frame.newer(): + index += 1 + iter_frame = iter_frame.newer() + return index + + # We divide frames into: + # - "python frames": + # - "bytecode frames" i.e. PyEval_EvalFrameEx + # - "other python frames": things that are of interest from a python + # POV, but aren't bytecode (e.g. GC, GIL) + # - everything else + + def is_python_frame(self): + '''Is this a _PyEval_EvalFrameDefault frame, or some other important + frame? (see is_other_python_frame for what "important" means in this + context)''' + if self.is_evalframe(): + return True + if self.is_other_python_frame(): + return True + return False + + def is_evalframe(self): + '''Is this a _PyEval_EvalFrameDefault frame?''' + if self._gdbframe.name() == EVALFRAME: + ''' + I believe we also need to filter on the inline + struct frame_id.inline_depth, only regarding frames with + an inline depth of 0 as actually being this function + + So we reject those with type gdb.INLINE_FRAME + ''' + if self._gdbframe.type() == gdb.NORMAL_FRAME: + # We have a _PyEval_EvalFrameDefault frame: + return True + + return False + + def is_other_python_frame(self): + '''Is this frame worth displaying in python backtraces? + Examples: + - waiting on the GIL + - garbage-collecting + - within a CFunction + If it is, return a descriptive string + For other frames, return False + ''' + if self.is_waiting_for_gil(): + return 'Waiting for the GIL' + + if self.is_gc_collect(): + return 'Garbage-collecting' + + # Detect invocations of PyCFunction instances: + frame = self._gdbframe + caller = frame.name() + if not caller: + return False + + if caller in ('_PyCFunction_FastCallDict', + '_PyCFunction_FastCallKeywords'): + arg_name = 'func' + # Within that frame: + # "func" is the local containing the PyObject* of the + # PyCFunctionObject instance + # "f" is the same value, but cast to (PyCFunctionObject*) + # "self" is the (PyObject*) of the 'self' + try: + # Use the prettyprinter for the func: + func = frame.read_var(arg_name) + return str(func) + except RuntimeError: + return 'PyCFunction invocation (unable to read %s)' % arg_name + + if caller == 'wrapper_call': + try: + func = frame.read_var('wp') + return str(func) + except RuntimeError: + return '<wrapper_call invocation>' + + # This frame isn't worth reporting: + return False + + def is_waiting_for_gil(self): + '''Is this frame waiting on the GIL?''' + # This assumes the _POSIX_THREADS version of Python/ceval_gil.h: + name = self._gdbframe.name() + if name: + return 'pthread_cond_timedwait' in name + + def is_gc_collect(self): + '''Is this frame "collect" within the garbage-collector?''' + return self._gdbframe.name() == 'collect' + + def get_pyop(self): + try: + f = self._gdbframe.read_var('f') + frame = PyFrameObjectPtr.from_pyobject_ptr(f) + if not frame.is_optimized_out(): + return frame + # gdb is unable to get the "f" argument of PyEval_EvalFrameEx() + # because it was "optimized out". Try to get "f" from the frame + # of the caller, PyEval_EvalCodeEx(). + orig_frame = frame + caller = self._gdbframe.older() + if caller: + f = caller.read_var('f') + frame = PyFrameObjectPtr.from_pyobject_ptr(f) + if not frame.is_optimized_out(): + return frame + return orig_frame + except ValueError: + return None + + @classmethod + def get_selected_frame(cls): + _gdbframe = gdb.selected_frame() + if _gdbframe: + return Frame(_gdbframe) + return None + + @classmethod + def get_selected_python_frame(cls): + '''Try to obtain the Frame for the python-related code in the selected + frame, or None''' + try: + frame = cls.get_selected_frame() + except gdb.error: + # No frame: Python didn't start yet + return None + + while frame: + if frame.is_python_frame(): + return frame + frame = frame.older() + + # Not found: + return None + + @classmethod + def get_selected_bytecode_frame(cls): + '''Try to obtain the Frame for the python bytecode interpreter in the + selected GDB frame, or None''' + frame = cls.get_selected_frame() + + while frame: + if frame.is_evalframe(): + return frame + frame = frame.older() + + # Not found: + return None + + def print_summary(self): + if self.is_evalframe(): + pyop = self.get_pyop() + if pyop: + line = pyop.get_truncated_repr(MAX_OUTPUT_LEN) + write_unicode(sys.stdout, '#%i %s\n' % (self.get_index(), line)) + if not pyop.is_optimized_out(): + line = pyop.current_line() + if line is not None: + sys.stdout.write(' %s\n' % line.strip()) + else: + sys.stdout.write('#%i (unable to read python frame information)\n' % self.get_index()) + else: + info = self.is_other_python_frame() + if info: + sys.stdout.write('#%i %s\n' % (self.get_index(), info)) + else: + sys.stdout.write('#%i\n' % self.get_index()) + + def print_traceback(self): + if self.is_evalframe(): + pyop = self.get_pyop() + if pyop: + pyop.print_traceback() + if not pyop.is_optimized_out(): + line = pyop.current_line() + if line is not None: + sys.stdout.write(' %s\n' % line.strip()) + else: + sys.stdout.write(' (unable to read python frame information)\n') + else: + info = self.is_other_python_frame() + if info: + sys.stdout.write(' %s\n' % info) + else: + sys.stdout.write(' (not a python frame)\n') + +class PyList(gdb.Command): + '''List the current Python source code, if any + + Use + py-list START + to list at a different line number within the python source. + + Use + py-list START, END + to list a specific range of lines within the python source. + ''' + + def __init__(self): + gdb.Command.__init__ (self, + "py-list", + gdb.COMMAND_FILES, + gdb.COMPLETE_NONE) + + + def invoke(self, args, from_tty): + import re + + start = None + end = None + + m = re.match(r'\s*(\d+)\s*', args) + if m: + start = int(m.group(0)) + end = start + 10 + + m = re.match(r'\s*(\d+)\s*,\s*(\d+)\s*', args) + if m: + start, end = map(int, m.groups()) + + # py-list requires an actual PyEval_EvalFrameEx frame: + frame = Frame.get_selected_bytecode_frame() + if not frame: + print('Unable to locate gdb frame for python bytecode interpreter') + return + + pyop = frame.get_pyop() + if not pyop or pyop.is_optimized_out(): + print('Unable to read information on python frame') + return + + filename = pyop.filename() + lineno = pyop.current_line_num() + + if start is None: + start = lineno - 5 + end = lineno + 5 + + if start<1: + start = 1 + + try: + f = open(os_fsencode(filename), 'r') + except IOError as err: + sys.stdout.write('Unable to open %s: %s\n' + % (filename, err)) + return + with f: + all_lines = f.readlines() + # start and end are 1-based, all_lines is 0-based; + # so [start-1:end] as a python slice gives us [start, end] as a + # closed interval + for i, line in enumerate(all_lines[start-1:end]): + linestr = str(i+start) + # Highlight current line: + if i + start == lineno: + linestr = '>' + linestr + sys.stdout.write('%4s %s' % (linestr, line)) + + +# ...and register the command: +PyList() + +def move_in_stack(move_up): + '''Move up or down the stack (for the py-up/py-down command)''' + frame = Frame.get_selected_python_frame() + if not frame: + print('Unable to locate python frame') + return + + while frame: + if move_up: + iter_frame = frame.older() + else: + iter_frame = frame.newer() + + if not iter_frame: + break + + if iter_frame.is_python_frame(): + # Result: + if iter_frame.select(): + iter_frame.print_summary() + return + + frame = iter_frame + + if move_up: + print('Unable to find an older python frame') + else: + print('Unable to find a newer python frame') + +class PyUp(gdb.Command): + 'Select and print the python stack frame that called this one (if any)' + def __init__(self): + gdb.Command.__init__ (self, + "py-up", + gdb.COMMAND_STACK, + gdb.COMPLETE_NONE) + + + def invoke(self, args, from_tty): + move_in_stack(move_up=True) + +class PyDown(gdb.Command): + 'Select and print the python stack frame called by this one (if any)' + def __init__(self): + gdb.Command.__init__ (self, + "py-down", + gdb.COMMAND_STACK, + gdb.COMPLETE_NONE) + + + def invoke(self, args, from_tty): + move_in_stack(move_up=False) + +# Not all builds of gdb have gdb.Frame.select +if hasattr(gdb.Frame, 'select'): + PyUp() + PyDown() + +class PyBacktraceFull(gdb.Command): + 'Display the current python frame and all the frames within its call stack (if any)' + def __init__(self): + gdb.Command.__init__ (self, + "py-bt-full", + gdb.COMMAND_STACK, + gdb.COMPLETE_NONE) + + + def invoke(self, args, from_tty): + frame = Frame.get_selected_python_frame() + if not frame: + print('Unable to locate python frame') + return + + while frame: + if frame.is_python_frame(): + frame.print_summary() + frame = frame.older() + +PyBacktraceFull() + +class PyBacktrace(gdb.Command): + 'Display the current python frame and all the frames within its call stack (if any)' + def __init__(self): + gdb.Command.__init__ (self, + "py-bt", + gdb.COMMAND_STACK, + gdb.COMPLETE_NONE) + + + def invoke(self, args, from_tty): + frame = Frame.get_selected_python_frame() + if not frame: + print('Unable to locate python frame') + return + + sys.stdout.write('Traceback (most recent call first):\n') + while frame: + if frame.is_python_frame(): + frame.print_traceback() + frame = frame.older() + +PyBacktrace() + +class PyPrint(gdb.Command): + 'Look up the given python variable name, and print it' + def __init__(self): + gdb.Command.__init__ (self, + "py-print", + gdb.COMMAND_DATA, + gdb.COMPLETE_NONE) + + + def invoke(self, args, from_tty): + name = str(args) + + frame = Frame.get_selected_python_frame() + if not frame: + print('Unable to locate python frame') + return + + pyop_frame = frame.get_pyop() + if not pyop_frame: + print('Unable to read information on python frame') + return + + pyop_var, scope = pyop_frame.get_var_by_name(name) + + if pyop_var: + print('%s %r = %s' + % (scope, + name, + pyop_var.get_truncated_repr(MAX_OUTPUT_LEN))) + else: + print('%r not found' % name) + +PyPrint() + +class PyLocals(gdb.Command): + 'Look up the given python variable name, and print it' + def __init__(self, command="py-locals"): + gdb.Command.__init__ (self, + command, + gdb.COMMAND_DATA, + gdb.COMPLETE_NONE) + + + def invoke(self, args, from_tty): + name = str(args) + + frame = Frame.get_selected_python_frame() + if not frame: + print('Unable to locate python frame') + return + + pyop_frame = frame.get_pyop() + if not pyop_frame: + print('Unable to read information on python frame') + return + + namespace = self.get_namespace(pyop_frame) + namespace = [(name.proxyval(set()), val) for name, val in namespace] + + if namespace: + name, val = max(namespace, key=lambda item: len(item[0])) + max_name_length = len(name) + + for name, pyop_value in namespace: + value = pyop_value.get_truncated_repr(MAX_OUTPUT_LEN) + print('%-*s = %s' % (max_name_length, name, value)) + + def get_namespace(self, pyop_frame): + return pyop_frame.iter_locals() + +PyLocals() + + +################################################################## +## added, not in CPython +################################################################## + +import re +import warnings +import tempfile +import textwrap +import itertools + +class PyGlobals(PyLocals): + 'List all the globals in the currently select Python frame' + + def get_namespace(self, pyop_frame): + return pyop_frame.iter_globals() + + +PyGlobals("py-globals") + + +class PyNameEquals(gdb.Function): + + def _get_pycurframe_attr(self, attr): + frame = Frame(gdb.selected_frame()) + if frame.is_evalframeex(): + pyframe = frame.get_pyop() + if pyframe is None: + warnings.warn("Use a Python debug build, Python breakpoints " + "won't work otherwise.") + return None + + return getattr(pyframe, attr).proxyval(set()) + + return None + + def invoke(self, funcname): + attr = self._get_pycurframe_attr('co_name') + return attr is not None and attr == funcname.string() + +PyNameEquals("pyname_equals") + + +class PyModEquals(PyNameEquals): + + def invoke(self, modname): + attr = self._get_pycurframe_attr('co_filename') + if attr is not None: + filename, ext = os.path.splitext(os.path.basename(attr)) + return filename == modname.string() + return False + +PyModEquals("pymod_equals") + + +class PyBreak(gdb.Command): + """ + Set a Python breakpoint. Examples: + + Break on any function or method named 'func' in module 'modname' + + py-break modname.func + + Break on any function or method named 'func' + + py-break func + """ + + def invoke(self, funcname, from_tty): + if '.' in funcname: + modname, dot, funcname = funcname.rpartition('.') + cond = '$pyname_equals("%s") && $pymod_equals("%s")' % (funcname, + modname) + else: + cond = '$pyname_equals("%s")' % funcname + + gdb.execute('break PyEval_EvalFrameEx if ' + cond) + +PyBreak("py-break", gdb.COMMAND_RUNNING, gdb.COMPLETE_NONE) + + +class _LoggingState(object): + """ + State that helps to provide a reentrant gdb.execute() function. + """ + + def __init__(self): + f = tempfile.NamedTemporaryFile('r+') + self.file = f + self.filename = f.name + self.fd = f.fileno() + _execute("set logging file %s" % self.filename) + self.file_position_stack = [] + + def __enter__(self): + if not self.file_position_stack: + _execute("set logging redirect on") + _execute("set logging on") + _execute("set pagination off") + + self.file_position_stack.append(os.fstat(self.fd).st_size) + return self + + def getoutput(self): + gdb.flush() + self.file.seek(self.file_position_stack[-1]) + result = self.file.read() + return result + + def __exit__(self, exc_type, exc_val, tb): + startpos = self.file_position_stack.pop() + self.file.seek(startpos) + self.file.truncate() + if not self.file_position_stack: + _execute("set logging off") + _execute("set logging redirect off") + _execute("set pagination on") + + +def execute(command, from_tty=False, to_string=False): + """ + Replace gdb.execute() with this function and have it accept a 'to_string' + argument (new in 7.2). Have it properly capture stderr also. Ensure + reentrancy. + """ + if to_string: + with _logging_state as state: + _execute(command, from_tty) + return state.getoutput() + else: + _execute(command, from_tty) + + +_execute = gdb.execute +gdb.execute = execute +_logging_state = _LoggingState() + + +def get_selected_inferior(): + """ + Return the selected inferior in gdb. + """ + # Woooh, another bug in gdb! Is there an end in sight? + # http://sourceware.org/bugzilla/show_bug.cgi?id=12212 + return gdb.inferiors()[0] + + selected_thread = gdb.selected_thread() + + for inferior in gdb.inferiors(): + for thread in inferior.threads(): + if thread == selected_thread: + return inferior + + +def source_gdb_script(script_contents, to_string=False): + """ + Source a gdb script with script_contents passed as a string. This is useful + to provide defines for py-step and py-next to make them repeatable (this is + not possible with gdb.execute()). See + http://sourceware.org/bugzilla/show_bug.cgi?id=12216 + """ + fd, filename = tempfile.mkstemp() + f = os.fdopen(fd, 'w') + f.write(script_contents) + f.close() + gdb.execute("source %s" % filename, to_string=to_string) + os.remove(filename) + + +def register_defines(): + source_gdb_script(textwrap.dedent("""\ + define py-step + -py-step + end + + define py-next + -py-next + end + + document py-step + %s + end + + document py-next + %s + end + """) % (PyStep.__doc__, PyNext.__doc__)) + + +def stackdepth(frame): + "Tells the stackdepth of a gdb frame." + depth = 0 + while frame: + frame = frame.older() + depth += 1 + + return depth + + +class ExecutionControlCommandBase(gdb.Command): + """ + Superclass for language specific execution control. Language specific + features should be implemented by lang_info using the LanguageInfo + interface. 'name' is the name of the command. + """ + + def __init__(self, name, lang_info): + super(ExecutionControlCommandBase, self).__init__( + name, gdb.COMMAND_RUNNING, gdb.COMPLETE_NONE) + self.lang_info = lang_info + + def install_breakpoints(self): + all_locations = itertools.chain( + self.lang_info.static_break_functions(), + self.lang_info.runtime_break_functions()) + + for location in all_locations: + result = gdb.execute('break %s' % location, to_string=True) + yield re.search(r'Breakpoint (\d+)', result).group(1) + + def delete_breakpoints(self, breakpoint_list): + for bp in breakpoint_list: + gdb.execute("delete %s" % bp) + + def filter_output(self, result): + reflags = re.MULTILINE + + output_on_halt = [ + (r'^Program received signal .*', reflags|re.DOTALL), + (r'.*[Ww]arning.*', 0), + (r'^Program exited .*', reflags), + ] + + output_always = [ + # output when halting on a watchpoint + (r'^(Old|New) value = .*', reflags), + # output from the 'display' command + (r'^\d+: \w+ = .*', reflags), + ] + + def filter_output(regexes): + output = [] + for regex, flags in regexes: + for match in re.finditer(regex, result, flags): + output.append(match.group(0)) + + return '\n'.join(output) + + # Filter the return value output of the 'finish' command + match_finish = re.search(r'^Value returned is \$\d+ = (.*)', result, + re.MULTILINE) + if match_finish: + finish_output = 'Value returned: %s\n' % match_finish.group(1) + else: + finish_output = '' + + return (filter_output(output_on_halt), + finish_output + filter_output(output_always)) + + def stopped(self): + return get_selected_inferior().pid == 0 + + def finish_executing(self, result): + """ + After doing some kind of code running in the inferior, print the line + of source code or the result of the last executed gdb command (passed + in as the `result` argument). + """ + output_on_halt, output_always = self.filter_output(result) + + if self.stopped(): + print(output_always) + print(output_on_halt) + else: + frame = gdb.selected_frame() + source_line = self.lang_info.get_source_line(frame) + if self.lang_info.is_relevant_function(frame): + raised_exception = self.lang_info.exc_info(frame) + if raised_exception: + print(raised_exception) + + if source_line: + if output_always.rstrip(): + print(output_always.rstrip()) + print(source_line) + else: + print(result) + + def _finish(self): + """ + Execute until the function returns (or until something else makes it + stop) + """ + if gdb.selected_frame().older() is not None: + return gdb.execute('finish', to_string=True) + else: + # outermost frame, continue + return gdb.execute('cont', to_string=True) + + def _finish_frame(self): + """ + Execute until the function returns to a relevant caller. + """ + while True: + result = self._finish() + + try: + frame = gdb.selected_frame() + except RuntimeError: + break + + hitbp = re.search(r'Breakpoint (\d+)', result) + is_relevant = self.lang_info.is_relevant_function(frame) + if hitbp or is_relevant or self.stopped(): + break + + return result + + def finish(self, *args): + "Implements the finish command." + result = self._finish_frame() + self.finish_executing(result) + + def step(self, stepinto, stepover_command='next'): + """ + Do a single step or step-over. Returns the result of the last gdb + command that made execution stop. + + This implementation, for stepping, sets (conditional) breakpoints for + all functions that are deemed relevant. It then does a step over until + either something halts execution, or until the next line is reached. + + If, however, stepover_command is given, it should be a string gdb + command that continues execution in some way. The idea is that the + caller has set a (conditional) breakpoint or watchpoint that can work + more efficiently than the step-over loop. For Python this means setting + a watchpoint for f->f_lasti, which means we can then subsequently + "finish" frames. + We want f->f_lasti instead of f->f_lineno, because the latter only + works properly with local trace functions, see + PyFrameObjectPtr.current_line_num and PyFrameObjectPtr.addr2line. + """ + if stepinto: + breakpoint_list = list(self.install_breakpoints()) + + beginframe = gdb.selected_frame() + + if self.lang_info.is_relevant_function(beginframe): + # If we start in a relevant frame, initialize stuff properly. If + # we don't start in a relevant frame, the loop will halt + # immediately. So don't call self.lang_info.lineno() as it may + # raise for irrelevant frames. + beginline = self.lang_info.lineno(beginframe) + + if not stepinto: + depth = stackdepth(beginframe) + + newframe = beginframe + + while True: + if self.lang_info.is_relevant_function(newframe): + result = gdb.execute(stepover_command, to_string=True) + else: + result = self._finish_frame() + + if self.stopped(): + break + + newframe = gdb.selected_frame() + is_relevant_function = self.lang_info.is_relevant_function(newframe) + try: + framename = newframe.name() + except RuntimeError: + framename = None + + m = re.search(r'Breakpoint (\d+)', result) + if m: + if is_relevant_function and m.group(1) in breakpoint_list: + # although we hit a breakpoint, we still need to check + # that the function, in case hit by a runtime breakpoint, + # is in the right context + break + + if newframe != beginframe: + # new function + + if not stepinto: + # see if we returned to the caller + newdepth = stackdepth(newframe) + is_relevant_function = (newdepth < depth and + is_relevant_function) + + if is_relevant_function: + break + else: + # newframe equals beginframe, check for a difference in the + # line number + lineno = self.lang_info.lineno(newframe) + if lineno and lineno != beginline: + break + + if stepinto: + self.delete_breakpoints(breakpoint_list) + + self.finish_executing(result) + + def run(self, args, from_tty): + self.finish_executing(gdb.execute('run ' + args, to_string=True)) + + def cont(self, *args): + self.finish_executing(gdb.execute('cont', to_string=True)) + + +class LanguageInfo(object): + """ + This class defines the interface that ExecutionControlCommandBase needs to + provide language-specific execution control. + + Classes that implement this interface should implement: + + lineno(frame) + Tells the current line number (only called for a relevant frame). + If lineno is a false value it is not checked for a difference. + + is_relevant_function(frame) + tells whether we care about frame 'frame' + + get_source_line(frame) + get the line of source code for the current line (only called for a + relevant frame). If the source code cannot be retrieved this + function should return None + + exc_info(frame) -- optional + tells whether an exception was raised, if so, it should return a + string representation of the exception value, None otherwise. + + static_break_functions() + returns an iterable of function names that are considered relevant + and should halt step-into execution. This is needed to provide a + performing step-into + + runtime_break_functions() -- optional + list of functions that we should break into depending on the + context + """ + + def exc_info(self, frame): + "See this class' docstring." + + def runtime_break_functions(self): + """ + Implement this if the list of step-into functions depends on the + context. + """ + return () + + +class PythonInfo(LanguageInfo): + + def pyframe(self, frame): + pyframe = Frame(frame).get_pyop() + if pyframe: + return pyframe + else: + raise gdb.RuntimeError( + "Unable to find the Python frame, run your code with a debug " + "build (configure with --with-pydebug or compile with -g).") + + def lineno(self, frame): + return self.pyframe(frame).current_line_num() + + def is_relevant_function(self, frame): + return Frame(frame).is_evalframeex() + + def get_source_line(self, frame): + try: + pyframe = self.pyframe(frame) + return '%4d %s' % (pyframe.current_line_num(), + pyframe.current_line().rstrip()) + except IOError: + return None + + def exc_info(self, frame): + try: + tstate = frame.read_var('tstate').dereference() + if gdb.parse_and_eval('tstate->frame == f'): + # tstate local variable initialized, check for an exception + inf_type = tstate['curexc_type'] + inf_value = tstate['curexc_value'] + + if inf_type: + return 'An exception was raised: %s' % (inf_value,) + except (ValueError, RuntimeError): + # Could not read the variable tstate or it's memory, it's ok + pass + + def static_break_functions(self): + yield 'PyEval_EvalFrameEx' + + +class PythonStepperMixin(object): + """ + Make this a mixin so CyStep can also inherit from this and use a + CythonCodeStepper at the same time. + """ + + def python_step(self, stepinto): + """ + Set a watchpoint on the Python bytecode instruction pointer and try + to finish the frame + """ + output = gdb.execute('watch f->f_lasti', to_string=True) + watchpoint = int(re.search(r'[Ww]atchpoint (\d+):', output).group(1)) + self.step(stepinto=stepinto, stepover_command='finish') + gdb.execute('delete %s' % watchpoint) + + +class PyStep(ExecutionControlCommandBase, PythonStepperMixin): + "Step through Python code." + + stepinto = True + + def invoke(self, args, from_tty): + self.python_step(stepinto=self.stepinto) + + +class PyNext(PyStep): + "Step-over Python code." + + stepinto = False + + +class PyFinish(ExecutionControlCommandBase): + "Execute until function returns to a caller." + + invoke = ExecutionControlCommandBase.finish + + +class PyRun(ExecutionControlCommandBase): + "Run the program." + + invoke = ExecutionControlCommandBase.run + + +class PyCont(ExecutionControlCommandBase): + + invoke = ExecutionControlCommandBase.cont + + +def _pointervalue(gdbval): + """ + Return the value of the pointer as a Python int. + + gdbval.type must be a pointer type + """ + # don't convert with int() as it will raise a RuntimeError + if gdbval.address is not None: + return int(gdbval.address) + else: + # the address attribute is None sometimes, in which case we can + # still convert the pointer to an int + return int(gdbval) + + +def pointervalue(gdbval): + pointer = _pointervalue(gdbval) + try: + if pointer < 0: + raise gdb.GdbError("Negative pointer value, presumably a bug " + "in gdb, aborting.") + except RuntimeError: + # work around yet another bug in gdb where you get random behaviour + # and tracebacks + pass + + return pointer + + +def get_inferior_unicode_postfix(): + try: + gdb.parse_and_eval('PyUnicode_FromEncodedObject') + except RuntimeError: + try: + gdb.parse_and_eval('PyUnicodeUCS2_FromEncodedObject') + except RuntimeError: + return 'UCS4' + else: + return 'UCS2' + else: + return '' + + +class PythonCodeExecutor(object): + + Py_single_input = 256 + Py_file_input = 257 + Py_eval_input = 258 + + def malloc(self, size): + chunk = (gdb.parse_and_eval("(void *) malloc((size_t) %d)" % size)) + + pointer = pointervalue(chunk) + if pointer == 0: + raise gdb.GdbError("No memory could be allocated in the inferior.") + + return pointer + + def alloc_string(self, string): + pointer = self.malloc(len(string)) + get_selected_inferior().write_memory(pointer, string) + + return pointer + + def alloc_pystring(self, string): + stringp = self.alloc_string(string) + PyString_FromStringAndSize = 'PyString_FromStringAndSize' + + try: + gdb.parse_and_eval(PyString_FromStringAndSize) + except RuntimeError: + # Python 3 + PyString_FromStringAndSize = ('PyUnicode%s_FromStringAndSize' % + (get_inferior_unicode_postfix(),)) + + try: + result = gdb.parse_and_eval( + '(PyObject *) %s((char *) %d, (size_t) %d)' % ( + PyString_FromStringAndSize, stringp, len(string))) + finally: + self.free(stringp) + + pointer = pointervalue(result) + if pointer == 0: + raise gdb.GdbError("Unable to allocate Python string in " + "the inferior.") + + return pointer + + def free(self, pointer): + gdb.parse_and_eval("free((void *) %d)" % pointer) + + def incref(self, pointer): + "Increment the reference count of a Python object in the inferior." + gdb.parse_and_eval('Py_IncRef((PyObject *) %d)' % pointer) + + def xdecref(self, pointer): + "Decrement the reference count of a Python object in the inferior." + # Py_DecRef is like Py_XDECREF, but a function. So we don't have + # to check for NULL. This should also decref all our allocated + # Python strings. + gdb.parse_and_eval('Py_DecRef((PyObject *) %d)' % pointer) + + def evalcode(self, code, input_type, global_dict=None, local_dict=None): + """ + Evaluate python code `code` given as a string in the inferior and + return the result as a gdb.Value. Returns a new reference in the + inferior. + + Of course, executing any code in the inferior may be dangerous and may + leave the debuggee in an unsafe state or terminate it altogether. + """ + if '\0' in code: + raise gdb.GdbError("String contains NUL byte.") + + code += '\0' + + pointer = self.alloc_string(code) + + globalsp = pointervalue(global_dict) + localsp = pointervalue(local_dict) + + if globalsp == 0 or localsp == 0: + raise gdb.GdbError("Unable to obtain or create locals or globals.") + + code = """ + PyRun_String( + (char *) %(code)d, + (int) %(start)d, + (PyObject *) %(globals)s, + (PyObject *) %(locals)d) + """ % dict(code=pointer, start=input_type, + globals=globalsp, locals=localsp) + + with FetchAndRestoreError(): + try: + pyobject_return_value = gdb.parse_and_eval(code) + finally: + self.free(pointer) + + return pyobject_return_value + + +class FetchAndRestoreError(PythonCodeExecutor): + """ + Context manager that fetches the error indicator in the inferior and + restores it on exit. + """ + + def __init__(self): + self.sizeof_PyObjectPtr = gdb.lookup_type('PyObject').pointer().sizeof + self.pointer = self.malloc(self.sizeof_PyObjectPtr * 3) + + type = self.pointer + value = self.pointer + self.sizeof_PyObjectPtr + traceback = self.pointer + self.sizeof_PyObjectPtr * 2 + + self.errstate = type, value, traceback + + def __enter__(self): + gdb.parse_and_eval("PyErr_Fetch(%d, %d, %d)" % self.errstate) + + def __exit__(self, *args): + if gdb.parse_and_eval("(int) PyErr_Occurred()"): + gdb.parse_and_eval("PyErr_Print()") + + pyerr_restore = ("PyErr_Restore(" + "(PyObject *) *%d," + "(PyObject *) *%d," + "(PyObject *) *%d)") + + try: + gdb.parse_and_eval(pyerr_restore % self.errstate) + finally: + self.free(self.pointer) + + +class FixGdbCommand(gdb.Command): + + def __init__(self, command, actual_command): + super(FixGdbCommand, self).__init__(command, gdb.COMMAND_DATA, + gdb.COMPLETE_NONE) + self.actual_command = actual_command + + def fix_gdb(self): + """ + It seems that invoking either 'cy exec' and 'py-exec' work perfectly + fine, but after this gdb's python API is entirely broken. + Maybe some uncleared exception value is still set? + sys.exc_clear() didn't help. A demonstration: + + (gdb) cy exec 'hello' + 'hello' + (gdb) python gdb.execute('cont') + RuntimeError: Cannot convert value to int. + Error while executing Python code. + (gdb) python gdb.execute('cont') + [15148 refs] + + Program exited normally. + """ + warnings.filterwarnings('ignore', r'.*', RuntimeWarning, + re.escape(__name__)) + try: + int(gdb.parse_and_eval("(void *) 0")) == 0 + except RuntimeError: + pass + # warnings.resetwarnings() + + def invoke(self, args, from_tty): + self.fix_gdb() + try: + gdb.execute('%s %s' % (self.actual_command, args)) + except RuntimeError as e: + raise gdb.GdbError(str(e)) + self.fix_gdb() + + +def _evalcode_python(executor, code, input_type): + """ + Execute Python code in the most recent stack frame. + """ + global_dict = gdb.parse_and_eval('PyEval_GetGlobals()') + local_dict = gdb.parse_and_eval('PyEval_GetLocals()') + + if (pointervalue(global_dict) == 0 or pointervalue(local_dict) == 0): + raise gdb.GdbError("Unable to find the locals or globals of the " + "most recent Python function (relative to the " + "selected frame).") + + return executor.evalcode(code, input_type, global_dict, local_dict) + + +class PyExec(gdb.Command): + + def readcode(self, expr): + if expr: + return expr, PythonCodeExecutor.Py_single_input + else: + lines = [] + while True: + try: + line = input('>') + except EOFError: + break + else: + if line.rstrip() == 'end': + break + + lines.append(line) + + return '\n'.join(lines), PythonCodeExecutor.Py_file_input + + def invoke(self, expr, from_tty): + expr, input_type = self.readcode(expr) + executor = PythonCodeExecutor() + executor.xdecref(_evalcode_python(executor, input_type, global_dict, local_dict)) + + +gdb.execute('set breakpoint pending on') + +if hasattr(gdb, 'GdbError'): + # Wrap py-step and py-next in gdb defines to make them repeatable. + py_step = PyStep('-py-step', PythonInfo()) + py_next = PyNext('-py-next', PythonInfo()) + register_defines() + py_finish = PyFinish('py-finish', PythonInfo()) + py_run = PyRun('py-run', PythonInfo()) + py_cont = PyCont('py-cont', PythonInfo()) + + py_exec = FixGdbCommand('py-exec', '-py-exec') + _py_exec = PyExec("-py-exec", gdb.COMMAND_DATA, gdb.COMPLETE_NONE) +else: + warnings.warn("Use gdb 7.2 or higher to use the py-exec command.") |