1#!/usr/bin/env vpython3 2# 3# Copyright 2019 The Chromium Authors 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7# Fetches Crashpad dumps from a given device, walks and symbolizes the stacks. 8# All the non-trivial operations are performed by generate_breakpad_symbols.py, 9# dump_syms, minidump_dump and minidump_stackwalk. 10 11import argparse 12import logging 13import os 14import posixpath 15import re 16import sys 17import shutil 18import subprocess 19import tempfile 20 21_BUILD_ANDROID_PATH = os.path.abspath( 22 os.path.join(os.path.dirname(__file__), '..')) 23sys.path.append(_BUILD_ANDROID_PATH) 24import devil_chromium 25from devil.android import device_utils 26from devil.utils import timeout_retry 27 28 29def _CreateSymbolsDir(build_path, dynamic_library_names): 30 generator = os.path.normpath( 31 os.path.join(_BUILD_ANDROID_PATH, '..', '..', 'components', 'crash', 32 'content', 'tools', 'generate_breakpad_symbols.py')) 33 syms_dir = os.path.join(build_path, 'crashpad_syms') 34 shutil.rmtree(syms_dir, ignore_errors=True) 35 os.mkdir(syms_dir) 36 for lib in dynamic_library_names: 37 unstripped_library_path = os.path.join(build_path, 'lib.unstripped', lib) 38 if not os.path.exists(unstripped_library_path): 39 continue 40 logging.info('Generating symbols for: %s', unstripped_library_path) 41 cmd = [ 42 generator, 43 '--symbols-dir', 44 syms_dir, 45 '--build-dir', 46 build_path, 47 '--binary', 48 unstripped_library_path, 49 '--platform', 50 'android', 51 ] 52 return_code = subprocess.call(cmd) 53 if return_code != 0: 54 logging.error('Could not extract symbols, command failed: %s', 55 ' '.join(cmd)) 56 return syms_dir 57 58 59def _ChooseLatestCrashpadDump(device, crashpad_dump_path): 60 if not device.PathExists(crashpad_dump_path): 61 logging.warning('Crashpad dump directory does not exist: %s', 62 crashpad_dump_path) 63 return None 64 latest = None 65 latest_timestamp = 0 66 for crashpad_file in device.ListDirectory(crashpad_dump_path): 67 if crashpad_file.endswith('.dmp'): 68 stat = device.StatPath(posixpath.join(crashpad_dump_path, crashpad_file)) 69 current_timestamp = stat['st_mtime'] 70 if current_timestamp > latest_timestamp: 71 latest_timestamp = current_timestamp 72 latest = crashpad_file 73 return latest 74 75 76def _ExtractLibraryNamesFromDump(build_path, dump_path): 77 default_library_name = 'libmonochrome.so' 78 dumper_path = os.path.join(build_path, 'minidump_dump') 79 if not os.access(dumper_path, os.X_OK): 80 logging.warning( 81 'Cannot extract library name from dump because %s is not found, ' 82 'default to: %s', dumper_path, default_library_name) 83 return [default_library_name] 84 p = subprocess.Popen([dumper_path, dump_path], 85 stdout=subprocess.PIPE, 86 stderr=subprocess.PIPE) 87 stdout, stderr = p.communicate() 88 if p.returncode != 0: 89 # Dumper errors often do not affect stack walkability, just a warning. 90 logging.warning('Reading minidump failed with output:\n%s', stderr) 91 92 library_names = [] 93 module_library_line_re = re.compile(r'[(]code_file[)]\s+= ' 94 r'"(?P<library_name>lib[^. ]+.so)"') 95 in_module = False 96 for line in stdout.splitlines(): 97 line = line.lstrip().rstrip('\n') 98 if line == 'MDRawModule': 99 in_module = True 100 continue 101 if line == '': 102 in_module = False 103 continue 104 if in_module: 105 m = module_library_line_re.match(line) 106 if m: 107 library_names.append(m.group('library_name')) 108 if not library_names: 109 logging.warning( 110 'Could not find any library name in the dump, ' 111 'default to: %s', default_library_name) 112 return [default_library_name] 113 return library_names 114 115 116def main(): 117 logging.basicConfig(level=logging.INFO) 118 parser = argparse.ArgumentParser( 119 description='Fetches Crashpad dumps from a given device, ' 120 'walks and symbolizes the stacks.') 121 parser.add_argument('--device', required=True, help='Device serial number') 122 parser.add_argument('--adb-path', help='Path to the "adb" command') 123 parser.add_argument( 124 '--build-path', 125 required=True, 126 help='Build output directory, equivalent to CHROMIUM_OUTPUT_DIR') 127 parser.add_argument( 128 '--chrome-cache-path', 129 required=True, 130 help='Directory on the device where Chrome stores cached files,' 131 ' crashpad stores dumps in a subdirectory of it') 132 args = parser.parse_args() 133 134 stackwalk_path = os.path.join(args.build_path, 'minidump_stackwalk') 135 if not os.path.exists(stackwalk_path): 136 logging.error('Missing minidump_stackwalk executable') 137 return 1 138 139 devil_chromium.Initialize(output_directory=args.build_path, 140 adb_path=args.adb_path) 141 device = device_utils.DeviceUtils(args.device) 142 143 device_crashpad_path = posixpath.join(args.chrome_cache_path, 'Crashpad', 144 'pending') 145 146 def CrashpadDumpExists(): 147 return _ChooseLatestCrashpadDump(device, device_crashpad_path) 148 149 crashpad_file = timeout_retry.WaitFor( 150 CrashpadDumpExists, wait_period=1, max_tries=9) 151 if not crashpad_file: 152 logging.error('Could not locate a crashpad dump') 153 return 1 154 155 dump_dir = tempfile.mkdtemp() 156 symbols_dir = None 157 try: 158 device.PullFile( 159 device_path=posixpath.join(device_crashpad_path, crashpad_file), 160 host_path=dump_dir) 161 dump_full_path = os.path.join(dump_dir, crashpad_file) 162 library_names = _ExtractLibraryNamesFromDump(args.build_path, 163 dump_full_path) 164 symbols_dir = _CreateSymbolsDir(args.build_path, library_names) 165 stackwalk_cmd = [stackwalk_path, dump_full_path, symbols_dir] 166 subprocess.call(stackwalk_cmd) 167 finally: 168 shutil.rmtree(dump_dir, ignore_errors=True) 169 if symbols_dir: 170 shutil.rmtree(symbols_dir, ignore_errors=True) 171 return 0 172 173 174if __name__ == '__main__': 175 sys.exit(main()) 176