#!/usr/bin/python # Copyright (c) 2012 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import os.path import re import shutil import sys import tempfile import time import urlparse import browserprocess class LaunchFailure(Exception): pass def GetPlatform(): if sys.platform == 'darwin': platform = 'mac' elif sys.platform.startswith('linux'): platform = 'linux' elif sys.platform in ('cygwin', 'win32'): platform = 'windows' else: raise LaunchFailure('Unknown platform: %s' % sys.platform) return platform PLATFORM = GetPlatform() def SelectRunCommand(): # The subprocess module added support for .kill in Python 2.6 assert (sys.version_info[0] >= 3 or (sys.version_info[0] == 2 and sys.version_info[1] >= 6)) if PLATFORM == 'linux': return browserprocess.RunCommandInProcessGroup else: return browserprocess.RunCommandWithSubprocess RunCommand = SelectRunCommand() def RemoveDirectory(path): retry = 5 sleep_time = 0.25 while True: try: shutil.rmtree(path) except Exception: # Windows processes sometime hang onto files too long if retry > 0: retry -= 1 time.sleep(sleep_time) sleep_time *= 2 else: # No luck - don't mask the error raise else: # succeeded break # In Windows, subprocess seems to have an issue with file names that # contain spaces. def EscapeSpaces(path): if PLATFORM == 'windows' and ' ' in path: return '"%s"' % path return path def MakeEnv(options): env = dict(os.environ) # Enable PPAPI Dev interfaces for testing. env['NACL_ENABLE_PPAPI_DEV'] = str(options.enable_ppapi_dev) if options.debug: env['PPAPI_BROWSER_DEBUG'] = '1' env['NACL_PLUGIN_DEBUG'] = '1' env['NACL_PPAPI_PROXY_DEBUG'] = '1' # env['NACL_SRPC_DEBUG'] = '1' return env class BrowserLauncher(object): WAIT_TIME = 20 WAIT_STEPS = 80 SLEEP_TIME = float(WAIT_TIME) / WAIT_STEPS def __init__(self, options): self.options = options self.profile = None self.binary = None self.tool_log_dir = None def KnownPath(self): raise NotImplementedError def BinaryName(self): raise NotImplementedError def CreateProfile(self): raise NotImplementedError def MakeCmd(self, url, host, port): raise NotImplementedError def CreateToolLogDir(self): self.tool_log_dir = tempfile.mkdtemp(prefix='vglogs_') return self.tool_log_dir def FindBinary(self): if self.options.browser_path: return self.options.browser_path else: path = self.KnownPath() if path is None or not os.path.exists(path): raise LaunchFailure('Cannot find the browser directory') binary = os.path.join(path, self.BinaryName()) if not os.path.exists(binary): raise LaunchFailure('Cannot find the browser binary') return binary def WaitForProcessDeath(self): self.browser_process.Wait(self.WAIT_STEPS, self.SLEEP_TIME) def Cleanup(self): self.browser_process.Kill() RemoveDirectory(self.profile) if self.tool_log_dir is not None: RemoveDirectory(self.tool_log_dir) def MakeProfileDirectory(self): self.profile = tempfile.mkdtemp(prefix='browserprofile_') return self.profile def SetStandardStream(self, env, var_name, redirect_file, is_output): if redirect_file is None: return file_prefix = 'file:' dev_prefix = 'dev:' debug_warning = 'DEBUG_ONLY:' # logic must match src/trusted/service_runtime/nacl_resource.* # resource specification notation. file: is the default # interpretation, so we must have an exhaustive list of # alternative schemes accepted. if we remove the file-is-default # interpretation, replace with # is_file = redirect_file.startswith(file_prefix) # and remove the list of non-file schemes. is_file = (not (redirect_file.startswith(dev_prefix) or redirect_file.startswith(debug_warning + dev_prefix))) if is_file: if redirect_file.startswith(file_prefix): bare_file = redirect_file[len(file_prefix)] else: bare_file = redirect_file # why always abspath? does chrome chdir or might it in the # future? this means we do not test/use the relative path case. redirect_file = file_prefix + os.path.abspath(bare_file) else: bare_file = None # ensure error if used without checking is_file env[var_name] = redirect_file if is_output: # sel_ldr appends program output to the file so we need to clear it # in order to get the stable result. if is_file: if os.path.exists(bare_file): os.remove(bare_file) parent_dir = os.path.dirname(bare_file) # parent directory may not exist. if not os.path.exists(parent_dir): os.makedirs(parent_dir) def Launch(self, cmd, env): browser_path = cmd[0] if not os.path.exists(browser_path): raise LaunchFailure('Browser does not exist %r'% browser_path) if not os.access(browser_path, os.X_OK): raise LaunchFailure('Browser cannot be executed %r (Is this binary on an ' 'NFS volume?)' % browser_path) if self.options.sel_ldr: env['NACL_SEL_LDR'] = self.options.sel_ldr if self.options.sel_ldr_bootstrap: env['NACL_SEL_LDR_BOOTSTRAP'] = self.options.sel_ldr_bootstrap if self.options.irt_library: env['NACL_IRT_LIBRARY'] = self.options.irt_library self.SetStandardStream(env, 'NACL_EXE_STDIN', self.options.nacl_exe_stdin, False) self.SetStandardStream(env, 'NACL_EXE_STDOUT', self.options.nacl_exe_stdout, True) self.SetStandardStream(env, 'NACL_EXE_STDERR', self.options.nacl_exe_stderr, True) print 'ENV:', ' '.join(['='.join(pair) for pair in env.iteritems()]) print 'LAUNCHING: %s' % ' '.join(cmd) sys.stdout.flush() self.browser_process = RunCommand(cmd, env=env) def IsRunning(self): return self.browser_process.IsRunning() def GetReturnCode(self): return self.browser_process.GetReturnCode() def Run(self, url, host, port): self.binary = EscapeSpaces(self.FindBinary()) self.profile = self.CreateProfile() if self.options.tool is not None: self.tool_log_dir = self.CreateToolLogDir() cmd = self.MakeCmd(url, host, port) self.Launch(cmd, MakeEnv(self.options)) def EnsureDirectory(path): if not os.path.exists(path): os.makedirs(path) def EnsureDirectoryForFile(path): EnsureDirectory(os.path.dirname(path)) class ChromeLauncher(BrowserLauncher): def KnownPath(self): if PLATFORM == 'linux': # TODO(ncbray): look in path? return '/opt/google/chrome' elif PLATFORM == 'mac': return '/Applications/Google Chrome.app/Contents/MacOS' else: homedir = os.path.expanduser('~') path = os.path.join(homedir, r'AppData\Local\Google\Chrome\Application') return path def BinaryName(self): if PLATFORM == 'mac': return 'Google Chrome' elif PLATFORM == 'windows': return 'chrome.exe' else: return 'chrome' def MakeEmptyJSONFile(self, path): EnsureDirectoryForFile(path) f = open(path, 'w') f.write('{}') f.close() def CreateProfile(self): profile = self.MakeProfileDirectory() # Squelch warnings by creating bogus files. self.MakeEmptyJSONFile(os.path.join(profile, 'Default', 'Preferences')) self.MakeEmptyJSONFile(os.path.join(profile, 'Local State')) return profile def NetLogName(self): return os.path.join(self.profile, 'netlog.json') def MakeCmd(self, url, host, port): cmd = [self.binary, # --enable-logging enables stderr output from Chromium subprocesses # on Windows (see # https://code.google.com/p/chromium/issues/detail?id=171836) '--enable-logging', '--disable-web-resources', '--disable-preconnect', # This is speculative, sync should not occur with a clean profile. '--disable-sync', # This prevents Chrome from making "hidden" network requests at # startup. These requests could be a source of non-determinism, # and they also add noise to the netlogs. '--dns-prefetch-disable', '--no-first-run', '--no-default-browser-check', '--log-level=1', '--safebrowsing-disable-auto-update', '--disable-default-apps', # Suppress metrics reporting. This prevents misconfigured bots, # people testing at their desktop, etc from poisoning the UMA data. '--metrics-recording-only', # Chrome explicitly blacklists some ports as "unsafe" because # certain protocols use them. Chrome gives an error like this: # Error 312 (net::ERR_UNSAFE_PORT): Unknown error # Unfortunately, the browser tester can randomly choose a # blacklisted port. To work around this, the tester whitelists # whatever port it is using. '--explicitly-allowed-ports=%d' % port, '--user-data-dir=%s' % self.profile] # Log network requests to assist debugging. cmd.append('--log-net-log=%s' % self.NetLogName()) if PLATFORM == 'linux': # Explicitly run with mesa on linux. The test infrastructure doesn't have # sufficient native GL contextes to run these tests. cmd.append('--use-gl=osmesa') if self.options.ppapi_plugin is None: cmd.append('--enable-nacl') disable_sandbox = False # Chrome process can't access file within sandbox disable_sandbox |= self.options.nacl_exe_stdin is not None disable_sandbox |= self.options.nacl_exe_stdout is not None disable_sandbox |= self.options.nacl_exe_stderr is not None if disable_sandbox: cmd.append('--no-sandbox') else: cmd.append('--register-pepper-plugins=%s;%s' % (self.options.ppapi_plugin, self.options.ppapi_plugin_mimetype)) cmd.append('--no-sandbox') if self.options.browser_extensions: cmd.append('--load-extension=%s' % ','.join(self.options.browser_extensions)) cmd.append('--enable-experimental-extension-apis') if self.options.enable_crash_reporter: cmd.append('--enable-crash-reporter-for-testing') if self.options.tool == 'memcheck': cmd = ['src/third_party/valgrind/memcheck.sh', '-v', '--xml=yes', '--leak-check=no', '--gen-suppressions=all', '--num-callers=30', '--trace-children=yes', '--nacl-file=%s' % (self.options.files[0],), '--suppressions=' + '../tools/valgrind/memcheck/suppressions.txt', '--xml-file=%s/xml.%%p' % (self.tool_log_dir,), '--log-file=%s/log.%%p' % (self.tool_log_dir,)] + cmd elif self.options.tool == 'tsan': cmd = ['src/third_party/valgrind/tsan.sh', '-v', '--num-callers=30', '--trace-children=yes', '--nacl-file=%s' % (self.options.files[0],), '--ignore=../tools/valgrind/tsan/ignores.txt', '--suppressions=../tools/valgrind/tsan/suppressions.txt', '--log-file=%s/log.%%p' % (self.tool_log_dir,)] + cmd elif self.options.tool != None: raise LaunchFailure('Invalid tool name "%s"' % (self.options.tool,)) if self.options.enable_sockets: cmd.append('--allow-nacl-socket-api=%s' % host) cmd.extend(self.options.browser_flags) cmd.append(url) return cmd