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