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.path 7import re 8import shutil 9import sys 10import tempfile 11import time 12import urlparse 13 14import browserprocess 15 16class LaunchFailure(Exception): 17 pass 18 19 20def GetPlatform(): 21 if sys.platform == 'darwin': 22 platform = 'mac' 23 elif sys.platform.startswith('linux'): 24 platform = 'linux' 25 elif sys.platform in ('cygwin', 'win32'): 26 platform = 'windows' 27 else: 28 raise LaunchFailure('Unknown platform: %s' % sys.platform) 29 return platform 30 31 32PLATFORM = GetPlatform() 33 34 35def SelectRunCommand(): 36 # The subprocess module added support for .kill in Python 2.6 37 assert (sys.version_info[0] >= 3 or (sys.version_info[0] == 2 and 38 sys.version_info[1] >= 6)) 39 if PLATFORM == 'linux': 40 return browserprocess.RunCommandInProcessGroup 41 else: 42 return browserprocess.RunCommandWithSubprocess 43 44 45RunCommand = SelectRunCommand() 46 47def RemoveDirectory(path): 48 retry = 5 49 sleep_time = 0.25 50 while True: 51 try: 52 shutil.rmtree(path) 53 except Exception: 54 # Windows processes sometime hang onto files too long 55 if retry > 0: 56 retry -= 1 57 time.sleep(sleep_time) 58 sleep_time *= 2 59 else: 60 # No luck - don't mask the error 61 raise 62 else: 63 # succeeded 64 break 65 66 67 68# In Windows, subprocess seems to have an issue with file names that 69# contain spaces. 70def EscapeSpaces(path): 71 if PLATFORM == 'windows' and ' ' in path: 72 return '"%s"' % path 73 return path 74 75 76def MakeEnv(options): 77 env = dict(os.environ) 78 # Enable PPAPI Dev interfaces for testing. 79 env['NACL_ENABLE_PPAPI_DEV'] = str(options.enable_ppapi_dev) 80 if options.debug: 81 env['PPAPI_BROWSER_DEBUG'] = '1' 82 env['NACL_PLUGIN_DEBUG'] = '1' 83 env['NACL_PPAPI_PROXY_DEBUG'] = '1' 84 # env['NACL_SRPC_DEBUG'] = '1' 85 return env 86 87 88class BrowserLauncher(object): 89 90 WAIT_TIME = 20 91 WAIT_STEPS = 80 92 SLEEP_TIME = float(WAIT_TIME) / WAIT_STEPS 93 94 def __init__(self, options): 95 self.options = options 96 self.profile = None 97 self.binary = None 98 self.tool_log_dir = None 99 100 def KnownPath(self): 101 raise NotImplementedError 102 103 def BinaryName(self): 104 raise NotImplementedError 105 106 def CreateProfile(self): 107 raise NotImplementedError 108 109 def MakeCmd(self, url, host, port): 110 raise NotImplementedError 111 112 def CreateToolLogDir(self): 113 self.tool_log_dir = tempfile.mkdtemp(prefix='vglogs_') 114 return self.tool_log_dir 115 116 def FindBinary(self): 117 if self.options.browser_path: 118 return self.options.browser_path 119 else: 120 path = self.KnownPath() 121 if path is None or not os.path.exists(path): 122 raise LaunchFailure('Cannot find the browser directory') 123 binary = os.path.join(path, self.BinaryName()) 124 if not os.path.exists(binary): 125 raise LaunchFailure('Cannot find the browser binary') 126 return binary 127 128 def WaitForProcessDeath(self): 129 self.browser_process.Wait(self.WAIT_STEPS, self.SLEEP_TIME) 130 131 def Cleanup(self): 132 self.browser_process.Kill() 133 134 RemoveDirectory(self.profile) 135 if self.tool_log_dir is not None: 136 RemoveDirectory(self.tool_log_dir) 137 138 def MakeProfileDirectory(self): 139 self.profile = tempfile.mkdtemp(prefix='browserprofile_') 140 return self.profile 141 142 def SetStandardStream(self, env, var_name, redirect_file, is_output): 143 if redirect_file is None: 144 return 145 file_prefix = 'file:' 146 dev_prefix = 'dev:' 147 debug_warning = 'DEBUG_ONLY:' 148 # logic must match src/trusted/service_runtime/nacl_resource.* 149 # resource specification notation. file: is the default 150 # interpretation, so we must have an exhaustive list of 151 # alternative schemes accepted. if we remove the file-is-default 152 # interpretation, replace with 153 # is_file = redirect_file.startswith(file_prefix) 154 # and remove the list of non-file schemes. 155 is_file = (not (redirect_file.startswith(dev_prefix) or 156 redirect_file.startswith(debug_warning + dev_prefix))) 157 if is_file: 158 if redirect_file.startswith(file_prefix): 159 bare_file = redirect_file[len(file_prefix)] 160 else: 161 bare_file = redirect_file 162 # why always abspath? does chrome chdir or might it in the 163 # future? this means we do not test/use the relative path case. 164 redirect_file = file_prefix + os.path.abspath(bare_file) 165 else: 166 bare_file = None # ensure error if used without checking is_file 167 env[var_name] = redirect_file 168 if is_output: 169 # sel_ldr appends program output to the file so we need to clear it 170 # in order to get the stable result. 171 if is_file: 172 if os.path.exists(bare_file): 173 os.remove(bare_file) 174 parent_dir = os.path.dirname(bare_file) 175 # parent directory may not exist. 176 if not os.path.exists(parent_dir): 177 os.makedirs(parent_dir) 178 179 def Launch(self, cmd, env): 180 browser_path = cmd[0] 181 if not os.path.exists(browser_path): 182 raise LaunchFailure('Browser does not exist %r'% browser_path) 183 if not os.access(browser_path, os.X_OK): 184 raise LaunchFailure('Browser cannot be executed %r (Is this binary on an ' 185 'NFS volume?)' % browser_path) 186 if self.options.sel_ldr: 187 env['NACL_SEL_LDR'] = self.options.sel_ldr 188 if self.options.sel_ldr_bootstrap: 189 env['NACL_SEL_LDR_BOOTSTRAP'] = self.options.sel_ldr_bootstrap 190 if self.options.irt_library: 191 env['NACL_IRT_LIBRARY'] = self.options.irt_library 192 self.SetStandardStream(env, 'NACL_EXE_STDIN', 193 self.options.nacl_exe_stdin, False) 194 self.SetStandardStream(env, 'NACL_EXE_STDOUT', 195 self.options.nacl_exe_stdout, True) 196 self.SetStandardStream(env, 'NACL_EXE_STDERR', 197 self.options.nacl_exe_stderr, True) 198 print 'ENV:', ' '.join(['='.join(pair) for pair in env.iteritems()]) 199 print 'LAUNCHING: %s' % ' '.join(cmd) 200 sys.stdout.flush() 201 self.browser_process = RunCommand(cmd, env=env) 202 203 def IsRunning(self): 204 return self.browser_process.IsRunning() 205 206 def GetReturnCode(self): 207 return self.browser_process.GetReturnCode() 208 209 def Run(self, url, host, port): 210 self.binary = EscapeSpaces(self.FindBinary()) 211 self.profile = self.CreateProfile() 212 if self.options.tool is not None: 213 self.tool_log_dir = self.CreateToolLogDir() 214 cmd = self.MakeCmd(url, host, port) 215 self.Launch(cmd, MakeEnv(self.options)) 216 217 218def EnsureDirectory(path): 219 if not os.path.exists(path): 220 os.makedirs(path) 221 222 223def EnsureDirectoryForFile(path): 224 EnsureDirectory(os.path.dirname(path)) 225 226 227class ChromeLauncher(BrowserLauncher): 228 229 def KnownPath(self): 230 if PLATFORM == 'linux': 231 # TODO(ncbray): look in path? 232 return '/opt/google/chrome' 233 elif PLATFORM == 'mac': 234 return '/Applications/Google Chrome.app/Contents/MacOS' 235 else: 236 homedir = os.path.expanduser('~') 237 path = os.path.join(homedir, r'AppData\Local\Google\Chrome\Application') 238 return path 239 240 def BinaryName(self): 241 if PLATFORM == 'mac': 242 return 'Google Chrome' 243 elif PLATFORM == 'windows': 244 return 'chrome.exe' 245 else: 246 return 'chrome' 247 248 def MakeEmptyJSONFile(self, path): 249 EnsureDirectoryForFile(path) 250 f = open(path, 'w') 251 f.write('{}') 252 f.close() 253 254 def CreateProfile(self): 255 profile = self.MakeProfileDirectory() 256 257 # Squelch warnings by creating bogus files. 258 self.MakeEmptyJSONFile(os.path.join(profile, 'Default', 'Preferences')) 259 self.MakeEmptyJSONFile(os.path.join(profile, 'Local State')) 260 261 return profile 262 263 def NetLogName(self): 264 return os.path.join(self.profile, 'netlog.json') 265 266 def MakeCmd(self, url, host, port): 267 cmd = [self.binary, 268 # --enable-logging enables stderr output from Chromium subprocesses 269 # on Windows (see 270 # https://code.google.com/p/chromium/issues/detail?id=171836) 271 '--enable-logging', 272 '--disable-web-resources', 273 '--disable-preconnect', 274 # This is speculative, sync should not occur with a clean profile. 275 '--disable-sync', 276 # This prevents Chrome from making "hidden" network requests at 277 # startup. These requests could be a source of non-determinism, 278 # and they also add noise to the netlogs. 279 '--dns-prefetch-disable', 280 '--no-first-run', 281 '--no-default-browser-check', 282 '--log-level=1', 283 '--safebrowsing-disable-auto-update', 284 '--disable-default-apps', 285 # Suppress metrics reporting. This prevents misconfigured bots, 286 # people testing at their desktop, etc from poisoning the UMA data. 287 '--metrics-recording-only', 288 # Chrome explicitly blacklists some ports as "unsafe" because 289 # certain protocols use them. Chrome gives an error like this: 290 # Error 312 (net::ERR_UNSAFE_PORT): Unknown error 291 # Unfortunately, the browser tester can randomly choose a 292 # blacklisted port. To work around this, the tester whitelists 293 # whatever port it is using. 294 '--explicitly-allowed-ports=%d' % port, 295 '--user-data-dir=%s' % self.profile] 296 # Log network requests to assist debugging. 297 cmd.append('--log-net-log=%s' % self.NetLogName()) 298 if PLATFORM == 'linux': 299 # Explicitly run with mesa on linux. The test infrastructure doesn't have 300 # sufficient native GL contextes to run these tests. 301 cmd.append('--use-gl=osmesa') 302 if self.options.ppapi_plugin is None: 303 cmd.append('--enable-nacl') 304 disable_sandbox = False 305 # Chrome process can't access file within sandbox 306 disable_sandbox |= self.options.nacl_exe_stdin is not None 307 disable_sandbox |= self.options.nacl_exe_stdout is not None 308 disable_sandbox |= self.options.nacl_exe_stderr is not None 309 if disable_sandbox: 310 cmd.append('--no-sandbox') 311 else: 312 cmd.append('--register-pepper-plugins=%s;%s' 313 % (self.options.ppapi_plugin, 314 self.options.ppapi_plugin_mimetype)) 315 cmd.append('--no-sandbox') 316 if self.options.browser_extensions: 317 cmd.append('--load-extension=%s' % 318 ','.join(self.options.browser_extensions)) 319 cmd.append('--enable-experimental-extension-apis') 320 if self.options.enable_crash_reporter: 321 cmd.append('--enable-crash-reporter-for-testing') 322 if self.options.tool == 'memcheck': 323 cmd = ['src/third_party/valgrind/memcheck.sh', 324 '-v', 325 '--xml=yes', 326 '--leak-check=no', 327 '--gen-suppressions=all', 328 '--num-callers=30', 329 '--trace-children=yes', 330 '--nacl-file=%s' % (self.options.files[0],), 331 '--suppressions=' + 332 '../tools/valgrind/memcheck/suppressions.txt', 333 '--xml-file=%s/xml.%%p' % (self.tool_log_dir,), 334 '--log-file=%s/log.%%p' % (self.tool_log_dir,)] + cmd 335 elif self.options.tool == 'tsan': 336 cmd = ['src/third_party/valgrind/tsan.sh', 337 '-v', 338 '--num-callers=30', 339 '--trace-children=yes', 340 '--nacl-file=%s' % (self.options.files[0],), 341 '--ignore=../tools/valgrind/tsan/ignores.txt', 342 '--suppressions=../tools/valgrind/tsan/suppressions.txt', 343 '--log-file=%s/log.%%p' % (self.tool_log_dir,)] + cmd 344 elif self.options.tool != None: 345 raise LaunchFailure('Invalid tool name "%s"' % (self.options.tool,)) 346 if self.options.enable_sockets: 347 cmd.append('--allow-nacl-socket-api=%s' % host) 348 cmd.extend(self.options.browser_flags) 349 cmd.append(url) 350 return cmd 351