#! /usr/bin/env python2
# 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.

"""Setups a local Rietveld instance to test against a live server for
integration tests.

It makes sure Google AppEngine SDK is found, download Rietveld and Django code
if necessary and starts the server on a free inbound TCP port.
"""

import logging
import optparse
import os
import shutil
import socket
import sys
import tempfile
import time

try:
  import subprocess2
except ImportError:
  sys.path.append(
      os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
  import subprocess2

DEPOT_TOOLS=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

class Failure(Exception):
  pass


def test_port(port):
  s = socket.socket()
  try:
    return s.connect_ex(('127.0.0.1', port)) == 0
  finally:
    s.close()


def find_free_port(start_port):
  """Search for a free port starting at specified port."""
  for port in xrange(start_port, (2<<16)):
    if not test_port(port):
      return port
  raise Failure('Having issues finding an available port')


class LocalRietveld(object):
  """Downloads everything needed to run a local instance of Rietveld."""

  def __init__(self, base_dir=None):
    # Paths
    self.base_dir = base_dir
    if not self.base_dir:
      self.base_dir = os.path.dirname(os.path.abspath(__file__))
    self.rietveld = os.path.join(self.base_dir, '_rietveld')
    self.infra = os.path.join(self.base_dir, '_infra')
    self.rietveld_app = os.path.join(
        self.infra, 'infra', 'appengine', 'chromium_rietveld')
    self.dev_app = os.path.join(
        self.infra, 'google_appengine', 'dev_appserver.py')
    self.test_server = None
    self.port = None
    self.tempdir = None

  def install_prerequisites(self):
    if os.path.exists(self.rietveld):
      print "Removing old rietveld dir"
      shutil.rmtree(self.rietveld)

    sdk_path = os.path.join(self.base_dir, 'google_appengine')
    if os.path.exists(sdk_path):
      print "Removing old appengine SDK dir"
      shutil.rmtree(sdk_path)

    previous = os.environ.get('DEPOT_TOOLS_UPDATE')
    os.environ['DEPOT_TOOLS_UPDATE'] = '0'
    try:
      if not os.path.isfile(os.path.join(self.infra, '.gclient')):
        print('Checking out infra...')
        shutil.rmtree(self.infra, ignore_errors=True)
        try:
          os.makedirs(self.infra)
          subprocess2.call(
            [sys.executable, os.path.join(DEPOT_TOOLS, 'fetch.py'),
             '--force', 'infra', '--managed=true'],
            cwd=self.infra)
        except (OSError, subprocess2.CalledProcessError), e:
          raise Failure('Failed to clone infra. \n%s' % e)
      else:
        print('Syncing infra...')
        try:
          subprocess2.call(
            [sys.executable, os.path.join(DEPOT_TOOLS, 'gclient.py'),
             'sync', '--force'],
            cwd=self.infra)
        except (OSError, subprocess2.CalledProcessError), e:
          raise Failure('Failed to sync infra. \n%s' % e)
    finally:
      if previous is None:
        del os.environ['DEPOT_TOOLS_UPDATE']
      else:
        os.environ['DEPOT_TOOLS_UPDATE'] = previous

  def start_server(self, verbose=False):
    self.install_prerequisites()
    assert not self.tempdir
    self.tempdir = tempfile.mkdtemp(prefix='rietveld_test')
    self.port = find_free_port(10000)
    admin_port = find_free_port(self.port + 1)
    if verbose:
      stdout = stderr = None
    else:
      stdout = subprocess2.PIPE
      stderr = subprocess2.STDOUT
    cmd = [
        sys.executable,
        self.dev_app,
        './app.yaml',  # Explicitly specify file to avoid bringing up backends.
        '--port', str(self.port),
        '--admin_port', str(admin_port),
        '--storage', self.tempdir,
        '--clear_search_indexes',
        '--skip_sdk_update_check',
    ]

    # CHEAP TRICK
    # By default you only want to bind on loopback but I'm testing over a
    # headless computer so it's useful to be able to access the test instance
    # remotely.
    if os.environ.get('GAE_LISTEN_ALL', '') == 'true':
      cmd.extend(('-a', '0.0.0.0'))
    logging.info(' '.join(cmd))
    self.test_server = subprocess2.Popen(
        cmd, stdout=stdout, stderr=stderr, cwd=self.rietveld_app)
    # Loop until port 127.0.0.1:port opens or the process dies.
    while not test_port(self.port):
      self.test_server.poll()
      if self.test_server.returncode is not None:
        if not verbose:
          out = self.test_server.communicate()[0]
        shutil.rmtree(self.tempdir)
        self.tempdir = None
        raise Failure(
            'Test rietveld instance failed early on port %s\n%s' %
              (self.port, out))
      time.sleep(0.01)

  def stop_server(self):
    if self.test_server:
      try:
        self.test_server.kill()
      except OSError:
        pass
      self.test_server.wait()
      self.test_server = None
      self.port = None
    if self.tempdir:
      shutil.rmtree(self.tempdir)
      self.tempdir = None


def main():
  parser = optparse.OptionParser()
  parser.add_option('-v', '--verbose', action='store_true')
  options, args = parser.parse_args()
  if args:
    parser.error('Unknown arguments: %s' % ' '.join(args))
  instance = LocalRietveld()
  try:
    instance.start_server(verbose=options.verbose)
    print 'Local rietveld instance started on port %d' % instance.port
    while True:
      time.sleep(0.1)
  finally:
    instance.stop_server()


if __name__ == '__main__':
  main()
