crashpad_stackwalker.py 5.9 KB

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