blob: 47741564eb1f7fd276ec9dc2dd8dea0ca73d80b8 [file] [log] [blame]
#!/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)