Home | History | Annotate | Download | only in browsertester
      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