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