1#!/usr/bin/python
2# Copyright (c) 2012 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
6import os
7import subprocess
8import sys
9import tempfile
10import time
11
12script_dir = os.path.dirname(__file__)
13sys.path.append(os.path.join(script_dir,
14                             '../../tools/browser_tester'))
15
16import browser_tester
17import browsertester.browserlauncher
18
19# This script extends browser_tester to check for the presence of
20# Breakpad crash dumps.
21
22
23# This reads a file of lines containing 'key:value' pairs.
24# The file contains entries like the following:
25#   plat:Win32
26#   prod:Chromium
27#   ptype:nacl-loader
28#   rept:crash svc
29def ReadDumpTxtFile(filename):
30  dump_info = {}
31  fh = open(filename, 'r')
32  for line in fh:
33    if ':' in line:
34      key, value = line.rstrip().split(':', 1)
35      dump_info[key] = value
36  fh.close()
37  return dump_info
38
39
40def StartCrashService(browser_path, dumps_dir, windows_pipe_name,
41                      cleanup_funcs, crash_service_exe,
42                      skip_if_missing=False):
43  # Find crash_service.exe relative to chrome.exe.  This is a bit icky.
44  browser_dir = os.path.dirname(browser_path)
45  crash_service_path = os.path.join(browser_dir, crash_service_exe)
46  if skip_if_missing and not os.path.exists(crash_service_path):
47    return
48  proc = subprocess.Popen([crash_service_path,
49                           '--v=1',  # Verbose output for debugging failures
50                           '--dumps-dir=%s' % dumps_dir,
51                           '--pipe-name=%s' % windows_pipe_name])
52
53  def Cleanup():
54    # Note that if the process has already exited, this will raise
55    # an 'Access is denied' WindowsError exception, but
56    # crash_service.exe is not supposed to do this and such
57    # behaviour should make the test fail.
58    proc.terminate()
59    status = proc.wait()
60    sys.stdout.write('crash_dump_tester: %s exited with status %s\n'
61                     % (crash_service_exe, status))
62
63  cleanup_funcs.append(Cleanup)
64
65
66def ListPathsInDir(dir_path):
67  if os.path.exists(dir_path):
68    return [os.path.join(dir_path, name)
69            for name in os.listdir(dir_path)]
70  else:
71    return []
72
73
74def GetDumpFiles(dumps_dirs):
75  all_files = [filename
76               for dumps_dir in dumps_dirs
77               for filename in ListPathsInDir(dumps_dir)]
78  sys.stdout.write('crash_dump_tester: Found %i files\n' % len(all_files))
79  for dump_file in all_files:
80    sys.stdout.write('  %s (size %i)\n'
81                     % (dump_file, os.stat(dump_file).st_size))
82  return [dump_file for dump_file in all_files
83          if dump_file.endswith('.dmp')]
84
85
86def Main(cleanup_funcs):
87  parser = browser_tester.BuildArgParser()
88  parser.add_option('--expected_crash_dumps', dest='expected_crash_dumps',
89                    type=int, default=0,
90                    help='The number of crash dumps that we should expect')
91  parser.add_option('--expected_process_type_for_crash',
92                    dest='expected_process_type_for_crash',
93                    type=str, default='nacl-loader',
94                    help='The type of Chromium process that we expect the '
95                    'crash dump to be for')
96  # Ideally we would just query the OS here to find out whether we are
97  # running x86-32 or x86-64 Windows, but Python's win32api module
98  # does not contain a wrapper for GetNativeSystemInfo(), which is
99  # what NaCl uses to check this, or for IsWow64Process(), which is
100  # what Chromium uses.  Instead, we just rely on the build system to
101  # tell us.
102  parser.add_option('--win64', dest='win64', action='store_true',
103                    help='Pass this if we are running tests for x86-64 Windows')
104  options, args = parser.parse_args()
105
106  temp_dir = tempfile.mkdtemp(prefix='nacl_crash_dump_tester_')
107  def CleanUpTempDir():
108    browsertester.browserlauncher.RemoveDirectory(temp_dir)
109  cleanup_funcs.append(CleanUpTempDir)
110
111  # To get a guaranteed unique pipe name, use the base name of the
112  # directory we just created.
113  windows_pipe_name = r'\\.\pipe\%s_crash_service' % os.path.basename(temp_dir)
114
115  # This environment variable enables Breakpad crash dumping in
116  # non-official builds of Chromium.
117  os.environ['CHROME_HEADLESS'] = '1'
118  if sys.platform == 'win32':
119    dumps_dir = temp_dir
120    # Override the default (global) Windows pipe name that Chromium will
121    # use for out-of-process crash reporting.
122    os.environ['CHROME_BREAKPAD_PIPE_NAME'] = windows_pipe_name
123    # Launch the x86-32 crash service so that we can handle crashes in
124    # the browser process.
125    StartCrashService(options.browser_path, dumps_dir, windows_pipe_name,
126                      cleanup_funcs, 'crash_service.exe')
127    if options.win64:
128      # Launch the x86-64 crash service so that we can handle crashes
129      # in the NaCl loader process (nacl64.exe).
130      # Skip if missing, since in win64 builds crash_service.exe is 64-bit
131      # and crash_service64.exe does not exist.
132      StartCrashService(options.browser_path, dumps_dir, windows_pipe_name,
133                        cleanup_funcs, 'crash_service64.exe',
134                        skip_if_missing=True)
135    # We add a delay because there is probably a race condition:
136    # crash_service.exe might not have finished doing
137    # CreateNamedPipe() before NaCl does a crash dump and tries to
138    # connect to that pipe.
139    # TODO(mseaborn): We could change crash_service.exe to report when
140    # it has successfully created the named pipe.
141    time.sleep(1)
142  elif sys.platform == 'darwin':
143    dumps_dir = temp_dir
144    os.environ['BREAKPAD_DUMP_LOCATION'] = dumps_dir
145  elif sys.platform.startswith('linux'):
146    # The "--user-data-dir" option is not effective for the Breakpad
147    # setup in Linux Chromium, because Breakpad is initialized before
148    # "--user-data-dir" is read.  So we set HOME to redirect the crash
149    # dumps to a temporary directory.
150    home_dir = temp_dir
151    os.environ['HOME'] = home_dir
152    options.enable_crash_reporter = True
153
154  result = browser_tester.Run(options.url, options)
155
156  # Find crash dump results.
157  if sys.platform.startswith('linux'):
158    # Look in "~/.config/*/Crash Reports".  This will find crash
159    # reports under ~/.config/chromium or ~/.config/google-chrome, or
160    # under other subdirectories in case the branding is changed.
161    dumps_dirs = [os.path.join(path, 'Crash Reports')
162                  for path in ListPathsInDir(os.path.join(home_dir, '.config'))]
163  else:
164    dumps_dirs = [dumps_dir]
165  dmp_files = GetDumpFiles(dumps_dirs)
166
167  failed = False
168  msg = ('crash_dump_tester: ERROR: Got %i crash dumps but expected %i\n' %
169         (len(dmp_files), options.expected_crash_dumps))
170  if len(dmp_files) != options.expected_crash_dumps:
171    sys.stdout.write(msg)
172    failed = True
173
174  for dump_file in dmp_files:
175    # Sanity check: Make sure dumping did not fail after opening the file.
176    msg = 'crash_dump_tester: ERROR: Dump file is empty\n'
177    if os.stat(dump_file).st_size == 0:
178      sys.stdout.write(msg)
179      failed = True
180
181    # On Windows, the crash dumps should come in pairs of a .dmp and
182    # .txt file.
183    if sys.platform == 'win32':
184      second_file = dump_file[:-4] + '.txt'
185      msg = ('crash_dump_tester: ERROR: File %r is missing a corresponding '
186             '%r file\n' % (dump_file, second_file))
187      if not os.path.exists(second_file):
188        sys.stdout.write(msg)
189        failed = True
190        continue
191      # Check that the crash dump comes from the NaCl process.
192      dump_info = ReadDumpTxtFile(second_file)
193      if 'ptype' in dump_info:
194        msg = ('crash_dump_tester: ERROR: Unexpected ptype value: %r != %r\n'
195               % (dump_info['ptype'], options.expected_process_type_for_crash))
196        if dump_info['ptype'] != options.expected_process_type_for_crash:
197          sys.stdout.write(msg)
198          failed = True
199      else:
200        sys.stdout.write('crash_dump_tester: ERROR: Missing ptype field\n')
201        failed = True
202    # TODO(mseaborn): Ideally we would also check that a backtrace
203    # containing an expected function name can be extracted from the
204    # crash dump.
205
206  if failed:
207    sys.stdout.write('crash_dump_tester: FAILED\n')
208    result = 1
209  else:
210    sys.stdout.write('crash_dump_tester: PASSED\n')
211
212  return result
213
214
215def MainWrapper():
216  cleanup_funcs = []
217  try:
218    return Main(cleanup_funcs)
219  finally:
220    for func in cleanup_funcs:
221      func()
222
223
224if __name__ == '__main__':
225  sys.exit(MainWrapper())
226