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 
     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