diff options
author | Anton Samokhvalov <pg83@yandex.ru> | 2022-02-10 16:45:15 +0300 |
---|---|---|
committer | Daniil Cherednik <dcherednik@yandex-team.ru> | 2022-02-10 16:45:15 +0300 |
commit | 72cb13b4aff9bc9cf22e49251bc8fd143f82538f (patch) | |
tree | da2c34829458c7d4e74bdfbdf85dff449e9e7fb8 /contrib/tools/cython/Cython/Debugger | |
parent | 778e51ba091dc39e7b7fcab2b9cf4dbedfb6f2b5 (diff) | |
download | ydb-72cb13b4aff9bc9cf22e49251bc8fd143f82538f.tar.gz |
Restoring authorship annotation for Anton Samokhvalov <pg83@yandex.ru>. Commit 1 of 2.
Diffstat (limited to 'contrib/tools/cython/Cython/Debugger')
-rw-r--r-- | contrib/tools/cython/Cython/Debugger/Cygdb.py | 284 | ||||
-rw-r--r-- | contrib/tools/cython/Cython/Debugger/DebugWriter.py | 126 | ||||
-rw-r--r-- | contrib/tools/cython/Cython/Debugger/Tests/TestLibCython.py | 508 | ||||
-rw-r--r-- | contrib/tools/cython/Cython/Debugger/Tests/__init__.py | 2 | ||||
-rw-r--r-- | contrib/tools/cython/Cython/Debugger/Tests/cfuncs.c | 16 | ||||
-rw-r--r-- | contrib/tools/cython/Cython/Debugger/Tests/codefile | 94 | ||||
-rw-r--r-- | contrib/tools/cython/Cython/Debugger/Tests/test_libcython_in_gdb.py | 970 | ||||
-rw-r--r-- | contrib/tools/cython/Cython/Debugger/Tests/test_libpython_in_gdb.py | 208 | ||||
-rw-r--r-- | contrib/tools/cython/Cython/Debugger/__init__.py | 2 | ||||
-rw-r--r-- | contrib/tools/cython/Cython/Debugger/libcython.py | 2792 | ||||
-rw-r--r-- | contrib/tools/cython/Cython/Debugger/libpython.py | 4508 |
11 files changed, 4755 insertions, 4755 deletions
diff --git a/contrib/tools/cython/Cython/Debugger/Cygdb.py b/contrib/tools/cython/Cython/Debugger/Cygdb.py index 45f31ce6f7..a317020ab5 100644 --- a/contrib/tools/cython/Cython/Debugger/Cygdb.py +++ b/contrib/tools/cython/Cython/Debugger/Cygdb.py @@ -1,143 +1,143 @@ -#!/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 +#!/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) + 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) @@ -153,6 +153,6 @@ def main(path_to_debug_info=None, gdb_argv=None, no_import=False): 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) + 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 index 876a3a2169..ebf99a2f64 100644 --- a/contrib/tools/cython/Cython/Debugger/DebugWriter.py +++ b/contrib/tools/cython/Cython/Debugger/DebugWriter.py @@ -1,72 +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: + +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() - + + +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) - + 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) + 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) + 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 index 13560646ff..6a769cc2fc 100644 --- a/contrib/tools/cython/Cython/Debugger/Tests/TestLibCython.py +++ b/contrib/tools/cython/Cython/Debugger/Tests/TestLibCython.py @@ -1,274 +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 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 - +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: + try: p = subprocess.Popen(['gdb', '-nx', '--version'], stdout=subprocess.PIPE) - except OSError: + except OSError: # gdb not found gdb_version = None - else: + else: stdout, _ = p.communicate() - # Based on Lib/test/test_gdb.py + # 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]: + 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) + 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: + 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 - + 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') + + 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() + + 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 index fa81adaff6..4a2889e8e1 100644 --- a/contrib/tools/cython/Cython/Debugger/Tests/__init__.py +++ b/contrib/tools/cython/Cython/Debugger/Tests/__init__.py @@ -1 +1 @@ -# empty file +# empty file diff --git a/contrib/tools/cython/Cython/Debugger/Tests/cfuncs.c b/contrib/tools/cython/Cython/Debugger/Tests/cfuncs.c index ccb42050bf..263589e77f 100644 --- a/contrib/tools/cython/Cython/Debugger/Tests/cfuncs.c +++ b/contrib/tools/cython/Cython/Debugger/Tests/cfuncs.c @@ -1,8 +1,8 @@ -void -some_c_function(void) -{ - int a, b, c; - - a = 1; - b = 2; -} +void +some_c_function(void) +{ + int a, b, c; + + a = 1; + b = 2; +} diff --git a/contrib/tools/cython/Cython/Debugger/Tests/codefile b/contrib/tools/cython/Cython/Debugger/Tests/codefile index 6b4c6b6add..5c2b0c2957 100644 --- a/contrib/tools/cython/Cython/Debugger/Tests/codefile +++ b/contrib/tools/cython/Cython/Debugger/Tests/codefile @@ -1,50 +1,50 @@ -cdef extern from "stdio.h": - int puts(char *s) - +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(): + 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() +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 index bd7608d607..c4d4ace7c0 100644 --- a/contrib/tools/cython/Cython/Debugger/Tests/test_libcython_in_gdb.py +++ b/contrib/tools/cython/Cython/Debugger/Tests/test_libcython_in_gdb.py @@ -1,496 +1,496 @@ -""" -Tests that run inside GDB. - -Note: debug information is already imported by the file generated by -Cython.Debugger.Cygdb.make_command_file() -""" - +""" +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 +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 - + +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) + +# 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): + 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)) - - + 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() +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 index 6f34cee47b..8a2b83419b 100644 --- a/contrib/tools/cython/Cython/Debugger/Tests/test_libpython_in_gdb.py +++ b/contrib/tools/cython/Cython/Debugger/Tests/test_libpython_in_gdb.py @@ -1,115 +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 - +# -*- 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' - + + +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 + + # 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() + 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( + return self.pyobject_fromcode( '(PyObject *) %s("%s", %d, "strict")' % ( funcname, data.replace('"', r'\"').replace('\\', r'\\'), len(data)), - gdbvar=gdbvar) - - def test_bytestring(self): + gdbvar=gdbvar) + + def test_bytestring(self): bytestring = self.alloc_bytestring(b"spam") - - if inferior_python_version < (3, 0): - bytestring_class = libpython.PyStringObjectPtr + + 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 ἄλφα") - + 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) + 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 index fa81adaff6..4a2889e8e1 100644 --- a/contrib/tools/cython/Cython/Debugger/__init__.py +++ b/contrib/tools/cython/Cython/Debugger/__init__.py @@ -1 +1 @@ -# empty file +# empty file diff --git a/contrib/tools/cython/Cython/Debugger/libcython.py b/contrib/tools/cython/Cython/Debugger/libcython.py index 23153789b6..2ddf43922e 100644 --- a/contrib/tools/cython/Cython/Debugger/libcython.py +++ b/contrib/tools/cython/Cython/Debugger/libcython.py @@ -1,23 +1,23 @@ -""" -GDB extension that adds Cython support. -""" - -from __future__ import print_function - +""" +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 - +import sys +import textwrap +import traceback +import functools +import itertools +import collections + +import gdb + try: # python 2 UNICODE = unicode BYTES = str @@ -25,1410 +25,1410 @@ 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: +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() + + 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: + 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): - """ + 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): + """ + + 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']) + 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: + 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): + "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). + 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 + + 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)) + # 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:') + 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:') + 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() - + 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() + 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 index fea626dd73..9285129026 100644 --- a/contrib/tools/cython/Cython/Debugger/libpython.py +++ b/contrib/tools/cython/Cython/Debugger/libpython.py @@ -1,73 +1,73 @@ -#!/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* +#!/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. -''' + +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 os import locale -import sys - +import sys + if sys.version_info[0] >= 3: unichr = chr - xrange = range + xrange = range long = int - -# Look up the gdb.Type for some standard types: + +# 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* @@ -88,41 +88,41 @@ def _sizeof_void_p(): # 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) - - +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() - +hexdigits = "0123456789abcdef" + +ENCODING = locale.getpreferredencoding() + EVALFRAME = '_PyEval_EvalFrameDefault' - -class NullPyObjectPtr(RuntimeError): - pass - - -def safety_limit(val): + +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 + # 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) @@ -134,7 +134,7 @@ else: if isinstance(text, unicode): text = text.encode(ENCODING, 'backslashreplace') file.write(text) - + try: os_fsencode = os.fsencode except AttributeError: @@ -154,525 +154,525 @@ except AttributeError: byte = char.encode(encoding) encoded.append(byte) return ''.join(encoded) - -class StringTruncated(RuntimeError): - pass - -class TruncatedStringIO(object): + +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): - """ + 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): + + 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(), + + 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 - + + 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, + + 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_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_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_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()) + + # 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): + # 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): + +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 + 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): + 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: + 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') + + + 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 + + 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): - ''' + 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() - + + 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()) - + + 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): + +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, + +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) @@ -682,41 +682,41 @@ class PyDictObjectPtr(PyObjectPtr): 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 = {} + 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 + 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('}') - + 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']) @@ -726,7 +726,7 @@ class PyDictObjectPtr(PyObjectPtr): except RuntimeError: # >= Python 3.6 pass - + if dk_size <= 0xFF: offset = dk_size elif dk_size <= 0xFFFF: @@ -735,247 +735,247 @@ class PyDictObjectPtr(PyObjectPtr): 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 - ''' + + +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: + if ob_size == 0: return 0 - - ob_digit = self.field('ob_digit') - - if gdb.lookup_type('digit').sizeof == 2: - SHIFT = 15 - else: - SHIFT = 30 - + + 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): + 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(): + +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') + + 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(): + + 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') + + 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') + + 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() + # 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(')') - + 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') @@ -985,10 +985,10 @@ class PyFrameObjectPtr(PyObjectPtr): % (self.co_filename.proxyval(visited), self.current_line_num(), self.co_name.proxyval(visited))) - -class PySetObjectPtr(PyObjectPtr): - _typename = 'PySetObject' - + +class PySetObjectPtr(PyObjectPtr): + _typename = 'PySetObject' + @classmethod def _dummy_key(self): return gdb.lookup_global_symbol('_PySet_Dummy').value() @@ -1002,168 +1002,168 @@ class PySetObjectPtr(PyObjectPtr): 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()) - + 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 + 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') + 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 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 = '"' + # 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()) - + 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(')') - + 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: + + +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): +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() @@ -1200,160 +1200,160 @@ class PyUnicodeObjectPtr(PyObjectPtr): 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 + + # 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). + 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): + 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) - - + # 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() @@ -1379,124 +1379,124 @@ class wrapperobject(PyObjectPtr): 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() +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 -""" +""" +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) - + 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 - + + + +# 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 @@ -1517,19 +1517,19 @@ class Frame(object): 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: + ''' + 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 - + return True + + return False + def is_other_python_frame(self): '''Is this frame worth displaying in python backtraces? Examples: @@ -1541,10 +1541,10 @@ class Frame(object): ''' 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() @@ -1559,23 +1559,23 @@ class Frame(object): # PyCFunctionObject instance # "f" is the same value, but cast to (PyCFunctionObject*) # "self" is the (PyObject*) of the 'self' - try: + 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: @@ -1587,8 +1587,8 @@ class Frame(object): '''Is this frame "collect" within the garbage-collector?''' return self._gdbframe.name() == 'collect' - def get_pyop(self): - try: + def get_pyop(self): + try: f = self._gdbframe.read_var('f') frame = PyFrameObjectPtr.from_pyobject_ptr(f) if not frame.is_optimized_out(): @@ -1605,17 +1605,17 @@ class Frame(object): 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): + 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: @@ -1636,35 +1636,35 @@ class Frame(object): 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: + frame = cls.get_selected_frame() + + while frame: if frame.is_evalframe(): - return frame - frame = frame.older() - - # Not found: - return None - - def print_summary(self): + 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)) + 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: + 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() @@ -1682,62 +1682,62 @@ class Frame(object): 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()) - + +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: + if not frame: print('Unable to locate gdb frame for python bytecode interpreter') - return - - pyop = frame.get_pyop() + 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 - + 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: @@ -1745,79 +1745,79 @@ class PyList(gdb.Command): % (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() + 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 - + 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() - + # 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): @@ -1825,7 +1825,7 @@ class PyBacktraceFull(gdb.Command): "py-bt-full", gdb.COMMAND_STACK, gdb.COMPLETE_NONE) - + def invoke(self, args, from_tty): frame = Frame.get_selected_python_frame() @@ -1840,101 +1840,101 @@ class PyBacktraceFull(gdb.Command): 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() +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: + 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: + 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' + 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() - + + + 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 @@ -1946,815 +1946,815 @@ 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() - - +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): + + +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): - """ + _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: + + 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 + 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 + + +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): - """ + """ + 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: + 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: + 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.") + 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.") |