blob: 4117d71f25d1fb9f3099d599c622321a72419c4d [file] [log] [blame]
# Copyright 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# pylint: disable=E1101
"""Umpire HTTP POST upload handlers
Handlers should be static methods, returned a tuple
(HTTP_STATUS, HTTP_CONTENT_DATA)
where HTTP_CONTENT_DATA can be anything JSON-serializable
URL:
http://umpire_server_address:umpire_webapp_port/upload/<handler_name>
Internal handlers accept every post fields as a list, even if there's only one
value in that field. **kwargs cannot guarantee order of args on keys, but order
of same field will be kept in list.
Files will be treated as a string contains its content, without any other
information.
Note that only functions decorated by @internal_handler are treated as handler.
External handlers are located in {server_toolkit}/usr/local/factory/bin/.
Args will be flatten into a list of string, field name follows values.
Fields named 'file' or with prefix 'file-' will be saved as named temp file,
passing filename instead of file body.
"""
import logging
import os
import tempfile
from twisted.internet import defer
from twisted.internet import protocol
from twisted.internet import reactor
from twisted.internet import threads
from twisted.web import http
import factory_common # pylint: disable=W0611
env = [{}]
_post_handlers = {}
EXTERNAL = 'RunExternalHandler'
def InternalHandler(func):
"""Decorator of internal handler.
Register a function as a internal handler. Only registered functions can be
called by POST requests.
"""
_post_handlers[func.__name__] = func
return func
def GetPostHandler(name):
return _post_handlers.get(name, None)
def SetEnv(environ):
env[0] = environ
class HandlerError(Exception):
pass
@InternalHandler
def Echo(**kwargs):
"""Echo received args.
Raise:
HandlerError if args contains 'exception'.
"""
if 'exception' in kwargs:
raise HandlerError()
ret = {}
for k, v in kwargs.iteritems():
value = repr(v)
if len(value) > 128:
value = value[:128] + '......'
ret[k] = value
return defer.succeed((http.OK, ret))
class ExternalProcessProtocol(protocol.ProcessProtocol):
"""Twisted process event handler.
It records output and exit code, invokes callback to handle response on
process exit.
"""
def __init__(self, handler, files=None):
"""Initializes an process event handler.
Args:
handler: Path of executable.
files: Reference of temporary files.
"""
self.stdout = []
self.stderr = []
self.exit_code = -1
self.handler = handler
# Keep references of files to prevent temp_file be garbage collected and
# closed automatically.
self.files = files
self.ended = False
# spawnProcess() won't wait for execution, so make a new empty Deferred
# to register callback, then let processEnded event invokes it manually.
self.deferred = defer.Deferred()
def outReceived(self, data):
logging.info('stdout: %s', data)
self.stdout.append(data)
def errReceived(self, data):
logging.info('stderr: %s', data)
self.stderr.append(data)
def processEnded(self, status):
logging.info('.. process end with %s', status)
self.exit_code = status.value.exitCode
content = {}
content['stdout'] = ''.join(self.stdout)
content['stderr'] = ''.join(self.stderr)
content['exit_code'] = self.exit_code
logging.info('POST Handler: external executable %s returns exit code %s',
self.handler, content['exit_code'])
status = http.OK
if content['exit_code'] != 0:
status = http.INTERNAL_SERVER_ERROR
self.deferred.callback((status, content))
@InternalHandler
def RunExternalHandler(handler, **kwargs):
"""Spawn external handler to handle request.
Note that we only guarantee argument order of same field, NOT between fields.
Returns a deferred object
"""
# Twisted default saved args using list, even if there's only 1 value
# in that field.
if isinstance(handler, list):
handler = handler[0]
handler_path = str(_GetFullHandlerPath(handler))
# To prevent temp files to be recycled before Spawn(), keep references.
files = {}
proto = ExternalProcessProtocol(handler_path, files)
def _Spawn(args_list):
args = [handler_path]
args.extend(args_list)
reactor.spawnProcess(proto, handler_path, args)
def _ReturnErrorResponse(fail):
# TODO: When spawnProcess() failed (ex. file not found) it won't raise
# exception, return non-zero exit code and write error message to stderr
# instead.
# errback cannot catch it. Maybe need other ways to recognize them, or
# just ignore this problem, treats it as execution error?
logging.info('POST Handler: SpawnProcess() causes error %s', fail)
proto.deferred.errback((http.INTERNAL_SERVER_ERROR, {'exception': repr(fail)}))
# Defer function that contains file IO, then spawn process, but don't return.
d = threads.deferToThread(_TranslateArgs, kwargs, files)
d.addCallback(_Spawn)
d.addErrback(_ReturnErrorResponse)
return proto.deferred
def _TranslateArgs(args, files):
"""Translate arguments to strings for Spawn external handler.
It unpacks args into a string list for Spawn(), saving files in tempfile
and replaces them with name of tempfile.
"""
ret = []
for k in args.keys():
# Check if it's file. We received file body as string from Twisted,
# unable to recognize with other field, so we recognize field name.
ret.append(k)
if k == 'file' or k.startswith('file-'):
file_list = []
for fb in args[k]:
temp_file = tempfile.NamedTemporaryFile()
temp_file.write(fb)
temp_file.flush()
file_list.append(temp_file)
ret.append(temp_file.name)
files[k] = file_list
else:
ret.extend(args[k])
return ret
def _GetFullHandlerPath(handler_name):
return os.path.join(env[0].server_toolkits_dir, 'usr/local/factory/bin',
handler_name)