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

"""Functions specific to build slaves, shared by several buildbot scripts.
"""

import datetime
import glob
import os
import re
import shutil
import sys
import tempfile
import time

from common import chromium_utils
from slave.bootstrap import ImportMasterConfigs # pylint: disable=W0611
from common.chromium_utils import GetActiveMaster # pylint: disable=W0611

# These codes used to distinguish true errors from script warnings.
ERROR_EXIT_CODE = 1
WARNING_EXIT_CODE = 88


# Local errors.
class PageHeapError(Exception):
  pass


# Cache the path to gflags.exe.
_gflags_exe = None


def SubversionExe():
  # TODO(pamg): move this into platform_utils to support Mac and Linux.
  if chromium_utils.IsWindows():
    return 'svn.bat'  # Find it in the user's path.
  elif chromium_utils.IsLinux() or chromium_utils.IsMac():
    return 'svn'  # Find it in the user's path.
  else:
    raise NotImplementedError(
        'Platform "%s" is not currently supported.' % sys.platform)


def GitExe():
  return 'git.bat' if chromium_utils.IsWindows() else 'git'


def SubversionCat(wc_dir):
  """Output the content of specified files or URLs in SVN.
  """
  try:
    return chromium_utils.GetCommandOutput([SubversionExe(), 'cat',
                                            wc_dir])
  except chromium_utils.ExternalError:
    return None


class NotGitWorkingCopy(Exception): pass
class NotSVNWorkingCopy(Exception): pass
class NotAnyWorkingCopy(Exception): pass
class InvalidSVNRevision(Exception): pass


def ScrapeSVNInfoRevision(wc_dir, regexp):
  """Runs 'svn info' on a working copy and applies the supplied regex and
  returns the matched group as an int.
  regexp can be either a compiled regex or a string regex.
  throws NotSVNWorkingCopy if wc_dir is not in a working copy.
  throws InvalidSVNRevision if matched group is not alphanumeric.
  """
  if isinstance(regexp, (str, unicode)):
    regexp = re.compile(regexp)
  retval, svn_info = chromium_utils.GetStatusOutput([SubversionExe(), 'info',
                                                    wc_dir])
  if retval or 'is not a working copy' in svn_info:
    raise NotSVNWorkingCopy(wc_dir)
  match = regexp.search(svn_info)
  if not match or not match.groups():
    raise InvalidSVNRevision(
        '%s did not match in svn info %s.' % (regexp.pattern, svn_info))
  text = match.group(1)
  if text.isalnum():
    return int(text)
  else:
    raise InvalidSVNRevision(text)


def SubversionRevision(wc_dir):
  """Finds the last svn revision of a working copy and returns it as an int."""
  return ScrapeSVNInfoRevision(wc_dir, r'(?s).*Revision: (\d+).*')


def SubversionLastChangedRevision(wc_dir_or_file):
  """Finds the last changed svn revision of a fs path returns it as an int."""
  return ScrapeSVNInfoRevision(wc_dir_or_file,
                               r'(?s).*Last Changed Rev: (\d+).*')


def GitHash(wc_dir):
  """Finds the current commit hash of the wc_dir."""
  retval, text = chromium_utils.GetStatusOutput(
      [GitExe(), 'rev-parse', 'HEAD'], cwd=wc_dir)
  if retval or 'fatal: Not a git repository' in text:
    raise NotGitWorkingCopy(wc_dir)
  return text.strip()


def GetHashOrRevision(wc_dir):
  """Gets the svn revision or git hash of wc_dir as a string. Throws
  NotAnyWorkingCopy if neither are appropriate."""
  try:
    return str(SubversionRevision(wc_dir))
  except NotSVNWorkingCopy:
    pass
  try:
    return GitHash(wc_dir)
  except NotGitWorkingCopy:
    pass
  raise NotAnyWorkingCopy(wc_dir)


def GitOrSubversion(wc_dir):
  """Returns the VCS for the given directory.

  Returns:
    'svn' if the directory is a valid svn repo
    'git' if the directory is a valid git repo root
    None otherwise
  """
  ret, out = chromium_utils.GetStatusOutput([SubversionExe(), 'info', wc_dir])
  if not ret and 'is not a working copy' not in out:
    return 'svn'

  ret, out = chromium_utils.GetStatusOutput(
      [GitExe(), 'rev-parse', '--is-inside-work-tree'], cwd=wc_dir)
  if not ret and 'fatal: Not a git repository' not in out:
    return 'git'

  return None


def GetBuildRevisions(src_dir, webkit_dir=None, revision_dir=None):
  """Parses build revisions out of the provided directories.

  Args:
    src_dir: The source directory to be used to check the revision in.
    webkit_dir: Optional WebKit directory, relative to src_dir.
    revision_dir: If provided, this dir will be used for the build revision
      instead of the mandatory src_dir.

  Returns a tuple of the build revision and (optional) WebKit revision.
  NOTICE: These revisions are strings, since they can be both Subversion numbers
  and Git hashes.
  """
  abs_src_dir = os.path.abspath(src_dir)
  webkit_revision = None
  if webkit_dir:
    webkit_dir = os.path.join(abs_src_dir, webkit_dir)
    webkit_revision = GetHashOrRevision(webkit_dir)

  if revision_dir:
    revision_dir = os.path.join(abs_src_dir, revision_dir)
    build_revision = GetHashOrRevision(revision_dir)
  else:
    build_revision = GetHashOrRevision(src_dir)
  return (build_revision, webkit_revision)


def GetZipFileNames(
    mastername, buildnumber, parent_buildnumber, build_revision,
    webkit_revision=None, extract=False, use_try_buildnumber=True):
  base_name = 'full-build-%s' % chromium_utils.PlatformName()

  if 'try' in mastername and use_try_buildnumber:
    if extract:
      if not parent_buildnumber:
        raise Exception('missing parent_buildnumber')
      version_suffix = '_%s' % parent_buildnumber
    else:
      version_suffix = '_%s' % buildnumber
  elif webkit_revision:
    version_suffix = '_wk%s_%s' % (webkit_revision, build_revision)
  else:
    version_suffix = '_%s' % build_revision

  return base_name, version_suffix


def SlaveBuildName(chrome_dir):
  """Extracts the build name of this slave (e.g., 'chrome-release') from the
  leaf subdir of its build directory.
  """
  return os.path.basename(SlaveBaseDir(chrome_dir))


def SlaveBaseDir(chrome_dir):
  """Finds the full path to the build slave's base directory (e.g.
  'c:/b/chrome/chrome-release').  This is assumed to be the parent of the
  shallowest 'build' directory in the chrome_dir path.

  Raises chromium_utils.PathNotFound if there is no such directory.
  """
  result = ''
  prev_dir = ''
  curr_dir = chrome_dir
  while prev_dir != curr_dir:
    (parent, leaf) = os.path.split(curr_dir)
    if leaf == 'build':
      # Remember this one and keep looking for something shallower.
      result = parent
    if leaf == 'slave':
      # We are too deep, stop now.
      break
    prev_dir = curr_dir
    curr_dir = parent
  if not result:
    raise chromium_utils.PathNotFound('Unable to find slave base dir above %s' %
                                      chrome_dir)
  return result


def GetStagingDir(start_dir):
  """Creates a chrome_staging dir in the starting directory. and returns its
  full path.
  """
  start_dir = os.path.abspath(start_dir)
  staging_dir = os.path.join(SlaveBaseDir(start_dir), 'chrome_staging')
  chromium_utils.MaybeMakeDirectory(staging_dir)
  return staging_dir


def SetPageHeap(chrome_dir, exe, enable):
  """Enables or disables page-heap checking in the given executable, depending
  on the 'enable' parameter.  gflags_exe should be the full path to gflags.exe.
  """
  global _gflags_exe
  if _gflags_exe is None:
    _gflags_exe = chromium_utils.FindUpward(chrome_dir,
                                            'tools', 'memory', 'gflags.exe')
  command = [_gflags_exe]
  if enable:
    command.extend(['/p', '/enable', exe, '/full'])
  else:
    command.extend(['/p', '/disable', exe])
  result = chromium_utils.RunCommand(command)
  if result:
    description = {True: 'enable', False: 'disable'}
    raise PageHeapError('Unable to %s page heap for %s.' %
                        (description[enable], exe))


def LongSleep(secs):
  """A sleep utility for long durations that avoids appearing hung.

  Sleeps for the specified duration.  Prints output periodically so as not to
  look hung in order to avoid being timed out.  Since this function is meant
  for long durations, it assumes that the caller does not care about losing a
  small amount of precision.

  Args:
    secs: The time to sleep, in seconds.
  """
  secs_per_iteration = 60
  time_slept = 0

  # Make sure we are dealing with an integral duration, since this function is
  # meant for long-lived sleeps we don't mind losing floating point precision.
  secs = int(round(secs))

  remainder = secs % secs_per_iteration
  if remainder > 0:
    time.sleep(remainder)
    time_slept += remainder
    sys.stdout.write('.')
    sys.stdout.flush()

  while time_slept < secs:
    time.sleep(secs_per_iteration)
    time_slept += secs_per_iteration
    sys.stdout.write('.')
    sys.stdout.flush()

  sys.stdout.write('\n')


def RunPythonCommandInBuildDir(build_dir, target, command_line_args,
                               server_dir=None, filter_obj=None):
  if sys.platform == 'win32':
    python_exe = 'python.exe'
  else:
    os.environ['PYTHONPATH'] = (chromium_utils.FindUpward(build_dir, 'tools',
                                                          'python')
                                + ':' +os.environ.get('PYTHONPATH', ''))
    python_exe = 'python'

  command = [python_exe] + command_line_args
  return chromium_utils.RunCommand(command, filter_obj=filter_obj)


class RunCommandCaptureFilter(object):
  lines = []

  def FilterLine(self, in_line):
    self.lines.append(in_line)
    return None

  def FilterDone(self, last_bits):
    self.lines.append(last_bits)
    return None


def GypFlagIsOn(options, flag):
  value = GetGypFlag(options, flag, False)
  # The values we understand as Off are False and a text zero.
  if value is False or value == '0':
    return False