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