Home | History | Annotate | Download | only in process_dumps
      1 #!/usr/bin/env python
      2 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """A tool to collect crash signatures for Chrome builds on Linux."""
      7 
      8 import fnmatch
      9 import optparse
     10 import os
     11 import shutil
     12 import subprocess
     13 import struct
     14 import sys
     15 import tempfile
     16 
     17 
     18 def VerifySymbolAndCopyToTempDir(symbol_file, temp_dir, sym_module_name):
     19   """Verify the symbol file looks correct and copy it to the right place
     20   in temp_dir.
     21 
     22   Args:
     23     symbol_file: the path to the symbol file.
     24     temp_dir: the base of the temp directory where the symbol file will reside.
     25   Returns:
     26     True on success.
     27   """
     28   symbol = open(symbol_file)
     29   signature_line = symbol.readline().strip().split()
     30   symbol.close()
     31   # signature_line should look like:
     32   # MODULE Linux x86 28D8A79A426807B5462CBA24F56746750 chrome
     33   if (len(signature_line) == 5 and signature_line[0] == 'MODULE' and
     34       signature_line[1] == 'Linux' and signature_line[4] == sym_module_name and
     35       len(signature_line[3]) == 33):
     36     dest = os.path.join(temp_dir, signature_line[4], signature_line[3])
     37     os.makedirs(dest)
     38     dest_file = os.path.join(dest, '%s.sym' % signature_line[4])
     39     shutil.copyfile(symbol_file, dest_file)
     40     return True
     41   return False
     42 
     43 
     44 def GetCommandOutput(command):
     45   """Runs the command list, returning its output.
     46 
     47   Prints the given command (which should be a list of one or more strings),
     48   then runs it and returns its output (stdout and stderr) as a string.
     49 
     50   If the command exits with an error, raises OSError.
     51 
     52   From chromium_utils.
     53   """
     54   proc = subprocess.Popen(command, stdout=subprocess.PIPE,
     55                           stderr=subprocess.STDOUT, bufsize=1)
     56   output = proc.communicate()[0]
     57   if proc.returncode:
     58     raise OSError('%s: %s' % (subprocess.list2cmdline(command), output))
     59   return output
     60 
     61 
     62 def GetCrashDumpDir():
     63   """Returns the default crash dump directory used by Chromium."""
     64   config_home = os.environ.get('XDG_CONFIG_HOME')
     65   if not config_home:
     66     home = os.path.expanduser('~')
     67     if not home:
     68       return ''
     69     config_home = os.path.join(home, '.config')
     70   return os.path.join(config_home, 'chromium', 'Crash Reports')
     71 
     72 
     73 def GetStackTrace(processor_bin, symbol_path, dump_file):
     74   """Gets and prints the stack trace from a crash dump file.
     75 
     76   Args:
     77     processor_bin: the path to the processor.
     78     symbol_path: root dir for the symbols.
     79     dump_file: the path to the dump file.
     80   Returns:
     81     A string representing the stack trace.
     82   """
     83   # Run processor to analyze crash dump.
     84   cmd = [processor_bin, '-m', dump_file, symbol_path]
     85 
     86   try:
     87     output = GetCommandOutput(cmd)
     88   except OSError:
     89     return 'Cannot get stack trace.'
     90 
     91   # Retrieve stack trace from processor output. Processor output looks like:
     92   # ----------------
     93   # Debug output
     94   # ...
     95   # Debug output
     96   # Module ...
     97   # ...
     98   # Module ...
     99   #
    100   # N|...     <--+
    101   # ...          |--- crashed thread stack trace
    102   # N|...     <--+
    103   # M|...
    104   # ...
    105   # ----------------
    106   # where each line of the stack trace looks like:
    107   # ThreadNumber|FrameNumber|ExeName|Function|SourceFile|LineNo|Offset
    108 
    109   stack_trace_frames = []
    110   idx = output.find('\nModule')
    111   if idx >= 0:
    112     output = output[idx+1:]
    113     idx = output.find('\n\n')
    114     if idx >= 0:
    115       output = output[idx+2:].splitlines()
    116       if output:
    117         first_line = output[0].split('|')
    118         if first_line:
    119           crashed_thread = first_line[0]
    120           for line in output:
    121             line_split = line.split('|')
    122             if not line_split:
    123               break
    124             if line_split[0] != crashed_thread:
    125               break
    126             stack_trace_frames.append(line_split)
    127   if not stack_trace_frames:
    128     return 'Cannot get stack trace.'
    129   stack_trace = []
    130   for frame in stack_trace_frames:
    131     if len(frame) != 7:
    132       continue
    133     (exe, func, source, line, offset) = frame[2:]
    134     if not exe or not source or not line or not offset:
    135       continue
    136     idx = func.find('(')
    137     if idx >= 0:
    138       func = func[:idx]
    139     if not func:
    140       continue
    141     frame_output = '%s!%s+%s [%s @ %s]' % (exe, func, offset, source, line)
    142     stack_trace.append(frame_output)
    143   return '\n'.join(stack_trace)
    144 
    145 
    146 def LocateFiles(pattern, root=os.curdir):
    147   """Yields files matching pattern found in root and its subdirectories.
    148 
    149   An exception is thrown if root doesn't exist.
    150 
    151   From chromium_utils."""
    152   root = os.path.expanduser(root)
    153   for path, dirs, files in os.walk(os.path.abspath(root)):
    154     for filename in fnmatch.filter(files, pattern):
    155       yield os.path.join(path, filename)
    156 
    157 
    158 def ProcessDump(dump_file, temp_dir):
    159   """Extracts the part of the dump file that minidump_stackwalk can read.
    160 
    161   Args:
    162     dump_file: the dump file that needs to be processed.
    163     temp_dir: the temp directory to put the dump file in.
    164   Returns:
    165     path of the processed dump file.
    166   """
    167   dump = open(dump_file, 'rb')
    168   dump_data = dump.read()
    169   dump.close()
    170   idx = dump_data.find('MDMP')
    171   if idx < 0:
    172     return ''
    173 
    174   dump_data = dump_data[idx:]
    175   if not dump_data:
    176     return ''
    177   (dump_fd, dump_name) = tempfile.mkstemp(suffix='chromedump', dir=temp_dir)
    178   os.write(dump_fd, dump_data)
    179   os.close(dump_fd)
    180   return dump_name
    181 
    182 
    183 def main_linux(options, args):
    184   # minidump_stackwalk is part of Google Breakpad. You may need to checkout
    185   # the code and build your own copy. http://google-breakpad.googlecode.com/
    186   LINUX_PROCESSOR = 'minidump_stackwalk'
    187   processor_bin = None
    188   if options.processor_dir:
    189     bin = os.path.join(os.path.expanduser(options.processor_dir),
    190                        LINUX_PROCESSOR)
    191     if os.access(bin, os.X_OK):
    192       processor_bin = bin
    193   else:
    194     for path in os.environ['PATH'].split(':'):
    195       bin = os.path.join(path, LINUX_PROCESSOR)
    196       if os.access(bin, os.X_OK):
    197         processor_bin = bin
    198         break
    199   if not processor_bin:
    200     print 'Cannot find minidump_stackwalk.'
    201     return 1
    202 
    203   if options.symbol_filename:
    204     symbol_file = options.symbol_filename
    205   else:
    206     if options.architecture:
    207       bits = options.architecture
    208     else:
    209       bits = struct.calcsize('P') * 8
    210     if bits == 32:
    211       symbol_file = 'chrome.breakpad.ia32'
    212     elif bits == 64:
    213       symbol_file = 'chrome.breakpad.x64'
    214     else:
    215       print 'Unknown architecture'
    216       return 1
    217 
    218   symbol_dir = options.symbol_dir
    219   if not options.symbol_dir:
    220     symbol_dir = os.curdir
    221   symbol_dir = os.path.abspath(os.path.expanduser(symbol_dir))
    222   symbol_file = os.path.join(symbol_dir, symbol_file)
    223   if not os.path.exists(symbol_file):
    224     print 'Cannot find symbols.'
    225     return 1
    226   symbol_time = os.path.getmtime(symbol_file)
    227 
    228   dump_files = []
    229   if options.dump_file:
    230     dump_files.append(options.dump_file)
    231   else:
    232     dump_dir = options.dump_dir
    233     if not dump_dir:
    234       dump_dir = GetCrashDumpDir()
    235     if not dump_dir:
    236       print 'Cannot find dump files.'
    237       return 1
    238     for dump_file in LocateFiles(pattern='*.dmp', root=dump_dir):
    239       file_time = os.path.getmtime(dump_file)
    240       if file_time < symbol_time:
    241         # Ignore dumps older than symbol file.
    242         continue
    243       dump_files.append(dump_file)
    244 
    245   temp_dir = tempfile.mkdtemp(suffix='chromedump')
    246   if not VerifySymbolAndCopyToTempDir(symbol_file, temp_dir,
    247                                       options.sym_module_name):
    248     print 'Cannot parse symbols.'
    249     shutil.rmtree(temp_dir)
    250     return 1
    251 
    252   dump_count = 0
    253   for dump_file in dump_files:
    254     processed_dump_file = ProcessDump(dump_file, temp_dir)
    255     if not processed_dump_file:
    256       continue
    257     print '-------------------------'
    258     print GetStackTrace(processor_bin, temp_dir, processed_dump_file)
    259     print
    260     os.remove(processed_dump_file)
    261     dump_count += 1
    262 
    263   shutil.rmtree(temp_dir)
    264   print '%s dumps found' % dump_count
    265   return 0
    266 
    267 
    268 def main():
    269   if not sys.platform.startswith('linux'):
    270     return 1
    271   parser = optparse.OptionParser()
    272   parser.add_option('', '--processor-dir', type='string', default='',
    273                     help='The directory where the processor is installed. '
    274                          'The processor is used to get stack trace from dumps. '
    275                           'Searches $PATH by default')
    276   parser.add_option('', '--dump-file', type='string', default='',
    277                     help='The path of the dump file to be processed. '
    278                          'Overwrites dump-path.')
    279   parser.add_option('', '--dump-dir', type='string', default='',
    280                     help='The directory where dump files are stored. '
    281                          'Searches this directory if dump-file is not '
    282                          'specified. Default is the Chromium crash directory.')
    283   parser.add_option('', '--symbol-dir', default='',
    284                     help='The directory with the symbols file. [Required]')
    285   parser.add_option('', '--symbol-filename', default='',
    286                     help='The name of the symbols file to use.  '
    287                          'This argument overrides --architecture.')
    288   parser.add_option('', '--architecture', type='int', default=None,
    289                     help='Override automatic x86/x86-64 detection. '
    290                          'Valid values are 32 and 64')
    291   parser.add_option('', '--sym-module-name', type='string', default='chrome',
    292                     help='The module name for the symbol file.  '
    293                          'Default: chrome')
    294 
    295   (options, args) = parser.parse_args()
    296   return main_linux(options, args)
    297 
    298 
    299 if '__main__' == __name__:
    300   sys.exit(main())
    301