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