#!/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.

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.iteritems()])
    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',
            '--disable-web-resources',
            '--disable-preconnect',
            # This is speculative, sync should not occur with a clean profile.
            '--disable-sync',
            # This prevents Chrome from making "hidden" network requests at
            # startup.  These requests could be a source of non-determinism,
            # and they also add noise to the netlogs.
            '--dns-prefetch-disable',
            '--no-first-run',
            '--no-default-browser-check',
            '--log-level=1',
            '--safebrowsing-disable-auto-update',
            '--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 mesa on linux. The test infrastructure doesn't have
      # sufficient native GL contextes to run these tests.
      cmd.append('--use-gl=osmesa')
    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
