193 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			193 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
| #!/usr/bin/env python
 | |
| 
 | |
| #----------------------------------------------------------------------
 | |
| # Be sure to add the python path that points to the LLDB shared library.
 | |
| #
 | |
| # # To use this in the embedded python interpreter using "lldb" just
 | |
| # import it with the full path using the "command script import"
 | |
| # command
 | |
| #   (lldb) command script import /path/to/clandiag.py
 | |
| #----------------------------------------------------------------------
 | |
| 
 | |
| from __future__ import absolute_import, division, print_function
 | |
| import lldb
 | |
| import argparse
 | |
| import shlex
 | |
| import os
 | |
| import re
 | |
| import subprocess
 | |
| 
 | |
| class MyParser(argparse.ArgumentParser):
 | |
|     def format_help(self):
 | |
|         return '''     Commands for managing clang diagnostic breakpoints
 | |
| 
 | |
| Syntax: clangdiag enable [<warning>|<diag-name>]
 | |
|         clangdiag disable
 | |
|         clangdiag diagtool [<path>|reset]
 | |
| 
 | |
| The following subcommands are supported:
 | |
| 
 | |
|       enable   -- Enable clang diagnostic breakpoints.
 | |
|       disable  -- Disable all clang diagnostic breakpoints.
 | |
|       diagtool -- Return, set, or reset diagtool path.
 | |
| 
 | |
| This command sets breakpoints in clang, and clang based tools, that
 | |
| emit diagnostics.  When a diagnostic is emitted, and clangdiag is
 | |
| enabled, it will use the appropriate diagtool application to determine
 | |
| the name of the DiagID, and set breakpoints in all locations that
 | |
| 'diag::name' appears in the source.  Since the new breakpoints are set
 | |
| after they are encountered, users will need to launch the executable a
 | |
| second time in order to hit the new breakpoints.
 | |
| 
 | |
| For in-tree builds, the diagtool application, used to map DiagID's to
 | |
| names, is found automatically in the same directory as the target
 | |
| executable.  However, out-or-tree builds must use the 'diagtool'
 | |
| subcommand to set the appropriate path for diagtool in the clang debug
 | |
| bin directory.  Since this mapping is created at build-time, it's
 | |
| important for users to use the same version that was generated when
 | |
| clang was compiled, or else the id's won't match.
 | |
| 
 | |
| Notes:
 | |
| - Substrings can be passed for both <warning> and <diag-name>.
 | |
| - If <warning> is passed, only enable the DiagID(s) for that warning.
 | |
| - If <diag-name> is passed, only enable that DiagID.
 | |
| - Rerunning enable clears existing breakpoints.
 | |
| - diagtool is used in breakpoint callbacks, so it can be changed
 | |
|   without the need to rerun enable.
 | |
| - Adding this to your ~.lldbinit file makes clangdiag available at startup:
 | |
|   "command script import /path/to/clangdiag.py"
 | |
| 
 | |
| '''
 | |
| 
 | |
| def create_diag_options():
 | |
|     parser = MyParser(prog='clangdiag')
 | |
|     subparsers = parser.add_subparsers(
 | |
|         title='subcommands',
 | |
|         dest='subcommands',
 | |
|         metavar='')
 | |
|     disable_parser = subparsers.add_parser('disable')
 | |
|     enable_parser = subparsers.add_parser('enable')
 | |
|     enable_parser.add_argument('id', nargs='?')
 | |
|     diagtool_parser = subparsers.add_parser('diagtool')
 | |
|     diagtool_parser.add_argument('path', nargs='?')
 | |
|     return parser
 | |
| 
 | |
| def getDiagtool(target, diagtool = None):
 | |
|     id = target.GetProcess().GetProcessID()
 | |
|     if 'diagtool' not in getDiagtool.__dict__:
 | |
|         getDiagtool.diagtool = {}
 | |
|     if diagtool:
 | |
|         if diagtool == 'reset':
 | |
|             getDiagtool.diagtool[id] = None
 | |
|         elif os.path.exists(diagtool):
 | |
|             getDiagtool.diagtool[id] = diagtool
 | |
|         else:
 | |
|             print('clangdiag: %s not found.' % diagtool)
 | |
|     if not id in getDiagtool.diagtool or not getDiagtool.diagtool[id]:
 | |
|         getDiagtool.diagtool[id] = None
 | |
|         exe = target.GetExecutable()
 | |
|         if not exe.Exists():
 | |
|             print('clangdiag: Target (%s) not set.' % exe.GetFilename())
 | |
|         else:
 | |
|             diagtool = os.path.join(exe.GetDirectory(), 'diagtool')
 | |
|             if os.path.exists(diagtool):
 | |
|                 getDiagtool.diagtool[id] = diagtool
 | |
|             else:
 | |
|                 print('clangdiag: diagtool not found along side %s' % exe)
 | |
| 
 | |
|     return getDiagtool.diagtool[id]
 | |
| 
 | |
| def setDiagBreakpoint(frame, bp_loc, dict):
 | |
|     id = frame.FindVariable("DiagID").GetValue()
 | |
|     if id is None:
 | |
|         print('clangdiag: id is None')
 | |
|         return False
 | |
| 
 | |
|     # Don't need to test this time, since we did that in enable.
 | |
|     target = frame.GetThread().GetProcess().GetTarget()
 | |
|     diagtool = getDiagtool(target)
 | |
|     name = subprocess.check_output([diagtool, "find-diagnostic-id", id]).rstrip();
 | |
|     # Make sure we only consider errors, warnings, and extensions.
 | |
|     # FIXME: Make this configurable?
 | |
|     prefixes = ['err_', 'warn_', 'exp_']
 | |
|     if len([prefix for prefix in prefixes+[''] if name.startswith(prefix)][0]):
 | |
|         bp = target.BreakpointCreateBySourceRegex(name, lldb.SBFileSpec())
 | |
|         bp.AddName("clang::Diagnostic")
 | |
| 
 | |
|     return False
 | |
| 
 | |
| def enable(exe_ctx, args):
 | |
|     # Always disable existing breakpoints
 | |
|     disable(exe_ctx)
 | |
| 
 | |
|     target = exe_ctx.GetTarget()
 | |
|     numOfBreakpoints = target.GetNumBreakpoints()
 | |
| 
 | |
|     if args.id:
 | |
|         # Make sure we only consider errors, warnings, and extensions.
 | |
|         # FIXME: Make this configurable?
 | |
|         prefixes = ['err_', 'warn_', 'exp_']
 | |
|         if len([prefix for prefix in prefixes+[''] if args.id.startswith(prefix)][0]):
 | |
|             bp = target.BreakpointCreateBySourceRegex(args.id, lldb.SBFileSpec())
 | |
|             bp.AddName("clang::Diagnostic")
 | |
|         else:
 | |
|             diagtool = getDiagtool(target)
 | |
|             list = subprocess.check_output([diagtool, "list-warnings"]).rstrip();
 | |
|             for line in list.splitlines(True):
 | |
|                 m = re.search(r' *(.*) .*\[\-W' + re.escape(args.id) + r'.*].*', line)
 | |
|                 # Make sure we only consider warnings.
 | |
|                 if m and m.group(1).startswith('warn_'):
 | |
|                     bp = target.BreakpointCreateBySourceRegex(m.group(1), lldb.SBFileSpec())
 | |
|                     bp.AddName("clang::Diagnostic")
 | |
|     else:
 | |
|         print('Adding callbacks.')
 | |
|         bp = target.BreakpointCreateByName('DiagnosticsEngine::Report')
 | |
|         bp.SetScriptCallbackFunction('clangdiag.setDiagBreakpoint')
 | |
|         bp.AddName("clang::Diagnostic")
 | |
| 
 | |
|     count = target.GetNumBreakpoints() - numOfBreakpoints
 | |
|     print('%i breakpoint%s added.' % (count, "s"[count==1:]))
 | |
| 
 | |
|     return
 | |
| 
 | |
| def disable(exe_ctx):
 | |
|     target = exe_ctx.GetTarget()
 | |
|     # Remove all diag breakpoints.
 | |
|     bkpts = lldb.SBBreakpointList(target)
 | |
|     target.FindBreakpointsByName("clang::Diagnostic", bkpts)
 | |
|     for i in range(bkpts.GetSize()):
 | |
|         target.BreakpointDelete(bkpts.GetBreakpointAtIndex(i).GetID())
 | |
| 
 | |
|     return
 | |
| 
 | |
| def the_diag_command(debugger, command, exe_ctx, result, dict):
 | |
|     # Use the Shell Lexer to properly parse up command options just like a
 | |
|     # shell would
 | |
|     command_args = shlex.split(command)
 | |
|     parser = create_diag_options()
 | |
|     try:
 | |
|         args = parser.parse_args(command_args)
 | |
|     except:
 | |
|         return
 | |
| 
 | |
|     if args.subcommands == 'enable':
 | |
|         enable(exe_ctx, args)
 | |
|     elif args.subcommands == 'disable':
 | |
|         disable(exe_ctx)
 | |
|     else:
 | |
|         diagtool = getDiagtool(exe_ctx.GetTarget(), args.path)
 | |
|         print('diagtool = %s' % diagtool)
 | |
| 
 | |
|     return
 | |
| 
 | |
| def __lldb_init_module(debugger, dict):
 | |
|     # This initializer is being run from LLDB in the embedded command interpreter
 | |
|     # Make the options so we can generate the help text for the new LLDB
 | |
|     # command line command prior to registering it with LLDB below
 | |
|     parser = create_diag_options()
 | |
|     the_diag_command.__doc__ = parser.format_help()
 | |
|     # Add any commands contained in this module to LLDB
 | |
|     debugger.HandleCommand(
 | |
|         'command script add -f clangdiag.the_diag_command clangdiag')
 | |
|     print('The "clangdiag" command has been installed, type "help clangdiag" or "clangdiag --help" for detailed help.')
 |