blob: dd94434a15c72e57e9c73c0d7110e1363462eeb6 [file] [log] [blame]
#!/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.
from __future__ import print_function
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['NACL_PLUGIN_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.items()]))
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',
# This prevents Chrome from making "hidden" network requests at
# startup and navigation. These requests could be a source of
# non-determinism, and they also add noise to the netlogs.
'--disable-features=NetworkPrediction',
# This is speculative, sync should not occur with a clean profile.
'--disable-sync',
'--no-first-run',
'--no-default-browser-check',
'--log-level=1',
'--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 SwiftShader on linux. The test infrastructure
# doesn't have sufficient native GL contextes to run these tests.
cmd.append('--use-gl=swiftshader')
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