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
6import os.path
7import re
8import shutil
9import sys
10import tempfile
11import time
12import urlparse
13
14import browserprocess
15
16class LaunchFailure(Exception):
17  pass
18
19
20def 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
32PLATFORM = GetPlatform()
33
34
35def 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
45RunCommand = SelectRunCommand()
46
47def 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.
70def EscapeSpaces(path):
71  if PLATFORM == 'windows' and ' ' in path:
72    return '"%s"' % path
73  return path
74
75
76def 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
86class 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
216def EnsureDirectory(path):
217  if not os.path.exists(path):
218    os.makedirs(path)
219
220
221def EnsureDirectoryForFile(path):
222  EnsureDirectory(os.path.dirname(path))
223
224
225class 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