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['PPAPI_BROWSER_DEBUG'] = '1'
82    env['NACL_PLUGIN_DEBUG'] = '1'
83    env['NACL_PPAPI_PROXY_DEBUG'] = '1'
84    # env['NACL_SRPC_DEBUG'] = '1'
85  return env
86
87
88class BrowserLauncher(object):
89
90  WAIT_TIME = 20
91  WAIT_STEPS = 80
92  SLEEP_TIME = float(WAIT_TIME) / WAIT_STEPS
93
94  def __init__(self, options):
95    self.options = options
96    self.profile = None
97    self.binary = None
98    self.tool_log_dir = None
99
100  def KnownPath(self):
101    raise NotImplementedError
102
103  def BinaryName(self):
104    raise NotImplementedError
105
106  def CreateProfile(self):
107    raise NotImplementedError
108
109  def MakeCmd(self, url, host, port):
110    raise NotImplementedError
111
112  def CreateToolLogDir(self):
113    self.tool_log_dir = tempfile.mkdtemp(prefix='vglogs_')
114    return self.tool_log_dir
115
116  def FindBinary(self):
117    if self.options.browser_path:
118      return self.options.browser_path
119    else:
120      path = self.KnownPath()
121      if path is None or not os.path.exists(path):
122        raise LaunchFailure('Cannot find the browser directory')
123      binary = os.path.join(path, self.BinaryName())
124      if not os.path.exists(binary):
125        raise LaunchFailure('Cannot find the browser binary')
126      return binary
127
128  def WaitForProcessDeath(self):
129    self.browser_process.Wait(self.WAIT_STEPS, self.SLEEP_TIME)
130
131  def Cleanup(self):
132    self.browser_process.Kill()
133
134    RemoveDirectory(self.profile)
135    if self.tool_log_dir is not None:
136      RemoveDirectory(self.tool_log_dir)
137
138  def MakeProfileDirectory(self):
139    self.profile = tempfile.mkdtemp(prefix='browserprofile_')
140    return self.profile
141
142  def SetStandardStream(self, env, var_name, redirect_file, is_output):
143    if redirect_file is None:
144      return
145    file_prefix = 'file:'
146    dev_prefix = 'dev:'
147    debug_warning = 'DEBUG_ONLY:'
148    # logic must match src/trusted/service_runtime/nacl_resource.*
149    # resource specification notation.  file: is the default
150    # interpretation, so we must have an exhaustive list of
151    # alternative schemes accepted.  if we remove the file-is-default
152    # interpretation, replace with
153    #   is_file = redirect_file.startswith(file_prefix)
154    # and remove the list of non-file schemes.
155    is_file = (not (redirect_file.startswith(dev_prefix) or
156                    redirect_file.startswith(debug_warning + dev_prefix)))
157    if is_file:
158      if redirect_file.startswith(file_prefix):
159        bare_file = redirect_file[len(file_prefix)]
160      else:
161        bare_file = redirect_file
162      # why always abspath?  does chrome chdir or might it in the
163      # future?  this means we do not test/use the relative path case.
164      redirect_file = file_prefix + os.path.abspath(bare_file)
165    else:
166      bare_file = None  # ensure error if used without checking is_file
167    env[var_name] = redirect_file
168    if is_output:
169      # sel_ldr appends program output to the file so we need to clear it
170      # in order to get the stable result.
171      if is_file:
172        if os.path.exists(bare_file):
173          os.remove(bare_file)
174        parent_dir = os.path.dirname(bare_file)
175        # parent directory may not exist.
176        if not os.path.exists(parent_dir):
177          os.makedirs(parent_dir)
178
179  def Launch(self, cmd, env):
180    browser_path = cmd[0]
181    if not os.path.exists(browser_path):
182      raise LaunchFailure('Browser does not exist %r'% browser_path)
183    if not os.access(browser_path, os.X_OK):
184      raise LaunchFailure('Browser cannot be executed %r (Is this binary on an '
185                          'NFS volume?)' % browser_path)
186    if self.options.sel_ldr:
187      env['NACL_SEL_LDR'] = self.options.sel_ldr
188    if self.options.sel_ldr_bootstrap:
189      env['NACL_SEL_LDR_BOOTSTRAP'] = self.options.sel_ldr_bootstrap
190    if self.options.irt_library:
191      env['NACL_IRT_LIBRARY'] = self.options.irt_library
192    self.SetStandardStream(env, 'NACL_EXE_STDIN',
193                           self.options.nacl_exe_stdin, False)
194    self.SetStandardStream(env, 'NACL_EXE_STDOUT',
195                           self.options.nacl_exe_stdout, True)
196    self.SetStandardStream(env, 'NACL_EXE_STDERR',
197                           self.options.nacl_exe_stderr, True)
198    print 'ENV:', ' '.join(['='.join(pair) for pair in env.iteritems()])
199    print 'LAUNCHING: %s' % ' '.join(cmd)
200    sys.stdout.flush()
201    self.browser_process = RunCommand(cmd, env=env)
202
203  def IsRunning(self):
204    return self.browser_process.IsRunning()
205
206  def GetReturnCode(self):
207    return self.browser_process.GetReturnCode()
208
209  def Run(self, url, host, port):
210    self.binary = EscapeSpaces(self.FindBinary())
211    self.profile = self.CreateProfile()
212    if self.options.tool is not None:
213      self.tool_log_dir = self.CreateToolLogDir()
214    cmd = self.MakeCmd(url, host, port)
215    self.Launch(cmd, MakeEnv(self.options))
216
217
218def EnsureDirectory(path):
219  if not os.path.exists(path):
220    os.makedirs(path)
221
222
223def EnsureDirectoryForFile(path):
224  EnsureDirectory(os.path.dirname(path))
225
226
227class ChromeLauncher(BrowserLauncher):
228
229  def KnownPath(self):
230    if PLATFORM == 'linux':
231      # TODO(ncbray): look in path?
232      return '/opt/google/chrome'
233    elif PLATFORM == 'mac':
234      return '/Applications/Google Chrome.app/Contents/MacOS'
235    else:
236      homedir = os.path.expanduser('~')
237      path = os.path.join(homedir, r'AppData\Local\Google\Chrome\Application')
238      return path
239
240  def BinaryName(self):
241    if PLATFORM == 'mac':
242      return 'Google Chrome'
243    elif PLATFORM == 'windows':
244      return 'chrome.exe'
245    else:
246      return 'chrome'
247
248  def MakeEmptyJSONFile(self, path):
249    EnsureDirectoryForFile(path)
250    f = open(path, 'w')
251    f.write('{}')
252    f.close()
253
254  def CreateProfile(self):
255    profile = self.MakeProfileDirectory()
256
257    # Squelch warnings by creating bogus files.
258    self.MakeEmptyJSONFile(os.path.join(profile, 'Default', 'Preferences'))
259    self.MakeEmptyJSONFile(os.path.join(profile, 'Local State'))
260
261    return profile
262
263  def NetLogName(self):
264    return os.path.join(self.profile, 'netlog.json')
265
266  def MakeCmd(self, url, host, port):
267    cmd = [self.binary,
268            # --enable-logging enables stderr output from Chromium subprocesses
269            # on Windows (see
270            # https://code.google.com/p/chromium/issues/detail?id=171836)
271            '--enable-logging',
272            '--disable-web-resources',
273            '--disable-preconnect',
274            # This is speculative, sync should not occur with a clean profile.
275            '--disable-sync',
276            # This prevents Chrome from making "hidden" network requests at
277            # startup.  These requests could be a source of non-determinism,
278            # and they also add noise to the netlogs.
279            '--dns-prefetch-disable',
280            '--no-first-run',
281            '--no-default-browser-check',
282            '--log-level=1',
283            '--safebrowsing-disable-auto-update',
284            '--disable-default-apps',
285            # Suppress metrics reporting.  This prevents misconfigured bots,
286            # people testing at their desktop, etc from poisoning the UMA data.
287            '--metrics-recording-only',
288            # Chrome explicitly blacklists some ports as "unsafe" because
289            # certain protocols use them.  Chrome gives an error like this:
290            # Error 312 (net::ERR_UNSAFE_PORT): Unknown error
291            # Unfortunately, the browser tester can randomly choose a
292            # blacklisted port.  To work around this, the tester whitelists
293            # whatever port it is using.
294            '--explicitly-allowed-ports=%d' % port,
295            '--user-data-dir=%s' % self.profile]
296    # Log network requests to assist debugging.
297    cmd.append('--log-net-log=%s' % self.NetLogName())
298    if PLATFORM == 'linux':
299      # Explicitly run with mesa on linux. The test infrastructure doesn't have
300      # sufficient native GL contextes to run these tests.
301      cmd.append('--use-gl=osmesa')
302    if self.options.ppapi_plugin is None:
303      cmd.append('--enable-nacl')
304      disable_sandbox = False
305      # Chrome process can't access file within sandbox
306      disable_sandbox |= self.options.nacl_exe_stdin is not None
307      disable_sandbox |= self.options.nacl_exe_stdout is not None
308      disable_sandbox |= self.options.nacl_exe_stderr is not None
309      if disable_sandbox:
310        cmd.append('--no-sandbox')
311    else:
312      cmd.append('--register-pepper-plugins=%s;%s'
313                 % (self.options.ppapi_plugin,
314                    self.options.ppapi_plugin_mimetype))
315      cmd.append('--no-sandbox')
316    if self.options.browser_extensions:
317      cmd.append('--load-extension=%s' %
318                 ','.join(self.options.browser_extensions))
319      cmd.append('--enable-experimental-extension-apis')
320    if self.options.enable_crash_reporter:
321      cmd.append('--enable-crash-reporter-for-testing')
322    if self.options.tool == 'memcheck':
323      cmd = ['src/third_party/valgrind/memcheck.sh',
324             '-v',
325             '--xml=yes',
326             '--leak-check=no',
327             '--gen-suppressions=all',
328             '--num-callers=30',
329             '--trace-children=yes',
330             '--nacl-file=%s' % (self.options.files[0],),
331             '--suppressions=' +
332             '../tools/valgrind/memcheck/suppressions.txt',
333             '--xml-file=%s/xml.%%p' % (self.tool_log_dir,),
334             '--log-file=%s/log.%%p' % (self.tool_log_dir,)] + cmd
335    elif self.options.tool == 'tsan':
336      cmd = ['src/third_party/valgrind/tsan.sh',
337             '-v',
338             '--num-callers=30',
339             '--trace-children=yes',
340             '--nacl-file=%s' % (self.options.files[0],),
341             '--ignore=../tools/valgrind/tsan/ignores.txt',
342             '--suppressions=../tools/valgrind/tsan/suppressions.txt',
343             '--log-file=%s/log.%%p' % (self.tool_log_dir,)] + cmd
344    elif self.options.tool != None:
345      raise LaunchFailure('Invalid tool name "%s"' % (self.options.tool,))
346    if self.options.enable_sockets:
347      cmd.append('--allow-nacl-socket-api=%s' % host)
348    cmd.extend(self.options.browser_flags)
349    cmd.append(url)
350    return cmd
351