#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Serves content for "script" handlers using the Go runtime."""


import logging
import os
import os.path
import sys
import threading

from google.appengine.api import appinfo
from google.appengine.tools.devappserver2 import application_configuration
from google.appengine.tools.devappserver2 import go_application
from google.appengine.tools.devappserver2 import http_runtime
from google.appengine.tools.devappserver2 import instance

_REBUILD_CONFIG_CHANGES = frozenset(
    [application_configuration.SKIP_FILES_CHANGED,
     application_configuration.NOBUILD_FILES_CHANGED])


class _GoBuildFailureRuntimeProxy(instance.RuntimeProxy):
  """Serves an error page for a Go application build failure."""

  def __init__(self, failure_exception):
    self._failure_exception = failure_exception

  def start(self):
    pass

  def quit(self):
    pass

  def handle(self, environ, start_response, url_map, match, request_id,
             request_type):
    """Serves a request by displaying an error page.

    Args:
      environ: An environ dict for the request as defined in PEP-333.
      start_response: A function with semantics defined in PEP-333.
      url_map: An appinfo.URLMap instance containing the configuration for the
          handler matching this request.
      match: A re.MatchObject containing the result of the matched URL pattern.
      request_id: A unique string id associated with the request.
      request_type: The type of the request. See instance.*_REQUEST module
          constants.

    Yields:
      A sequence of strings containing the body of the HTTP response.
    """
    start_response('500 Internal Server Error',
                   [('Content-Type', 'text/plain; charset=utf-8')])
    yield 'The Go application could not be built.\n'
    yield '\n'
    yield str(self._failure_exception)


class GoRuntimeInstanceFactory(instance.InstanceFactory):
  """A factory that creates new Go runtime Instances."""

  START_URL_MAP = appinfo.URLMap(
      url='/_ah/start',
      script='_go_app',
      login='admin')
  WARMUP_URL_MAP = appinfo.URLMap(
      url='/_ah/warmup',
      script='_go_app',
      login='admin')
  FILE_CHANGE_INSTANCE_RESTART_POLICY = instance.ALWAYS

  def __init__(self, request_data, runtime_config_getter, module_configuration):
    """Initializer for GoRuntimeInstanceFactory.

    Args:
      request_data: A wsgi_request_info.WSGIRequestInfo that will be provided
          with request information for use by API stubs.
      runtime_config_getter: A function that can be called without arguments
          and returns the runtime_config_pb2.RuntimeConfig containing the
          configuration for the runtime.
      module_configuration: An application_configuration.ModuleConfiguration
          instance respresenting the configuration of the module that owns the
          runtime.
    """
    super(GoRuntimeInstanceFactory, self).__init__(request_data, 1)
    self._runtime_config_getter = runtime_config_getter
    self._module_configuration = module_configuration
    self._application_lock = threading.Lock()
    self._go_application = go_application.GoApplication(
        self._module_configuration)
    self._modified_since_last_build = False
    self._last_build_error = None

  def get_restart_directories(self):
    """Returns a list of directories changes in which should trigger a restart.

    Returns:
      A list of src directory paths in the GOPATH. Changes (i.e. files added,
      deleted or modified) in these directories will trigger a restart of all
      instances created with this factory.
    """
    try:
      go_path = os.environ['GOPATH']
    except KeyError:
      return []
    else:
      if sys.platform.startswith('win32'):
        roots = go_path.split(';')
      else:
        roots = go_path.split(':')
      dirs = [os.path.join(r, 'src') for r in roots]
      return [d for d in dirs if os.path.isdir(d)]

  def files_changed(self):
    """Called when a file relevant to the factory *might* have changed."""
    with self._application_lock:
      self._modified_since_last_build = True

  def configuration_changed(self, config_changes):
    """Called when the configuration of the module has changed.

    Args:
      config_changes: A set containing the changes that occured. See the
          *_CHANGED constants in the application_configuration module.
    """
    if config_changes & _REBUILD_CONFIG_CHANGES:
      with self._application_lock:
        self._modified_since_last_build = True

  def new_instance(self, instance_id, expect_ready_request=False):
    """Create and return a new Instance.

    Args:
      instance_id: A string or integer representing the unique (per module) id
          of the instance.
      expect_ready_request: If True then the instance will be sent a special
          request (i.e. /_ah/warmup or /_ah/start) before it can handle external
          requests.

    Returns:
      The newly created instance.Instance.
    """

    def instance_config_getter():
      runtime_config = self._runtime_config_getter()
      runtime_config.instance_id = str(instance_id)
      return runtime_config

    with self._application_lock:
      try:
        if self._go_application.maybe_build(self._modified_since_last_build):
          if self._last_build_error:
            logging.info('Go application successfully built.')
          self._last_build_error = None
      except go_application.BuildError as e:
        logging.error('Failed to build Go application: %s', e)
        # Deploy a failure proxy now and each time a new instance is requested.
        self._last_build_error = e

      self._modified_since_last_build = False

      if self._last_build_error:
        logging.debug('Deploying new instance of failure proxy.')
        proxy = _GoBuildFailureRuntimeProxy(self._last_build_error)
      else:
        proxy = http_runtime.HttpRuntimeProxy(
            self._go_application.go_executable,
            instance_config_getter,
            self._module_configuration,
            self._go_application.get_environment())

    return instance.Instance(self.request_data,
                             instance_id,
                             proxy,
                             self.max_concurrent_requests,
                             self.max_background_threads,
                             expect_ready_request)
