xref: /aosp_15_r20/external/angle/build/android/stacktrace/crashpad_stackwalker.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
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