|
- """CherryPy-based server for running NILM filters via HTTP"""
-
- import cherrypy
- import os
- import socket
- import traceback
-
- from nilmdb.utils.printf import sprintf
- from nilmdb.server.serverutil import (
- exception_to_httperror,
- CORS_allow,
- json_to_request_params,
- json_error_page,
- cherrypy_start,
- cherrypy_stop,
- bool_param,
- )
- from nilmdb.utils import serializer_proxy
- import nilmrun
- import nilmrun.processmanager
-
- # Add CORS_allow tool
- cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow)
-
-
- # CherryPy apps
- class App(object):
- """Root application for NILM runner"""
-
- def __init__(self):
- pass
-
- # /
- @cherrypy.expose
- def index(self):
- cherrypy.response.headers['Content-Type'] = 'text/plain'
- msg = sprintf("This is NilmRun version %s, running on host %s.\n",
- nilmrun.__version__, socket.getfqdn())
- return msg
-
- # /favicon.ico
- @cherrypy.expose
- def favicon_ico(self):
- raise cherrypy.NotFound()
-
- # /version
- @cherrypy.expose
- @cherrypy.tools.json_out()
- def version(self):
- return nilmrun.__version__
-
-
- class AppProcess(object):
-
- def __init__(self, manager):
- self.manager = manager
-
- def process_status(self, pid):
- # We need to convert the log (which is bytes) to Unicode
- # characters, in order to send it via JSON. Treat it as UTF-8
- # but replace invalid characters with markers.
- log = self.manager[pid].log.decode('utf-8', errors='replace')
- return {
- "pid": pid,
- "alive": self.manager[pid].alive,
- "exitcode": self.manager[pid].exitcode,
- "start_time": self.manager[pid].start_time,
- "log": log
- }
-
- # /process/status
- @cherrypy.expose
- @cherrypy.tools.json_out()
- def status(self, pid, clear=False):
- """Return status about a process. If clear = True, also clear
- the log."""
- clear = bool_param(clear)
- if pid not in self.manager:
- raise cherrypy.HTTPError("404 Not Found", "No such PID")
- status = self.process_status(pid)
- if clear:
- self.manager[pid].clear_log()
- return status
-
- # /process/list
- @cherrypy.expose
- @cherrypy.tools.json_out()
- def list(self):
- """Return a list of processes in the manager."""
- return list(self.manager)
-
- # /process/info
- @cherrypy.expose
- @cherrypy.tools.json_out()
- def info(self):
- """Return detailed CPU and memory info about the system and
- all processes"""
- return self.manager.get_info()
-
- # /process/remove
- @cherrypy.expose
- @cherrypy.tools.json_in()
- @cherrypy.tools.json_out()
- @cherrypy.tools.CORS_allow(methods=["POST"])
- def remove(self, pid):
- """Remove a process from the manager, killing it if necessary."""
- if pid not in self.manager:
- raise cherrypy.HTTPError("404 Not Found", "No such PID")
- if not self.manager.terminate(pid): # pragma: no cover
- raise cherrypy.HTTPError("503 Service Unavailable",
- "Failed to stop process")
- status = self.process_status(pid)
- self.manager.remove(pid)
- return status
-
-
- class AppRun(object):
- def __init__(self, manager):
- self.manager = manager
-
- # /run/command
- @cherrypy.expose
- @cherrypy.tools.json_in()
- @cherrypy.tools.json_out()
- @exception_to_httperror(nilmrun.processmanager.ProcessError)
- @cherrypy.tools.CORS_allow(methods=["POST"])
- def command(self, argv):
- """Execute an arbitrary program on the server. argv is a
- list of the program and its arguments: 'argv[0]' is the program
- and 'argv[1:]' are arguments"""
- if not isinstance(argv, list):
- raise cherrypy.HTTPError("400 Bad Request",
- "argv must be a list of strings")
- return self.manager.run_command(argv)
-
- # /run/code
- @cherrypy.expose
- @cherrypy.tools.json_in()
- @cherrypy.tools.json_out()
- @exception_to_httperror(nilmrun.processmanager.ProcessError)
- @cherrypy.tools.CORS_allow(methods=["POST"])
- def code(self, code, args=None):
- """Execute arbitrary Python code. 'code' is a formatted string.
- It will be run as if it were written into a Python file and
- executed. 'args' is a list of strings, and they are passed
- on the command line as additional arguments (i.e., they end up
- in sys.argv[1:])"""
- if args is None:
- args = []
- if not isinstance(args, list):
- raise cherrypy.HTTPError("400 Bad Request",
- "args must be a list of strings")
- return self.manager.run_code(code, args)
-
-
- class Server(object):
- def __init__(self, host='127.0.0.1', port=8080,
- force_traceback=False, # include traceback in all errors
- basepath='', # base URL path for cherrypy.tree
- ):
-
- # Build up global server configuration
- cherrypy.config.update({
- 'environment': 'embedded',
- 'server.socket_host': host,
- 'server.socket_port': port,
- 'engine.autoreload_on': False,
- 'server.max_request_body_size': 8*1024*1024,
- })
-
- # Build up application specific configuration
- app_config = {}
- app_config.update({
- 'error_page.default': self.json_error_page,
- })
-
- # Some default headers to just help identify that things are working
- app_config.update({'response.headers.X-Jim-Is-Awesome': 'yeah'})
-
- # Set up Cross-Origin Resource Sharing (CORS) handler so we
- # can correctly respond to browsers' CORS preflight requests.
- # This also limits verbs to GET and HEAD by default.
- app_config.update({
- 'tools.CORS_allow.on': True,
- 'tools.CORS_allow.methods': ['GET', 'HEAD']
- })
-
- # Configure the 'json_in' tool to also allow other content-types
- # (like x-www-form-urlencoded), and to treat JSON as a dict that
- # fills requests.param.
- app_config.update({'tools.json_in.force': False,
- 'tools.json_in.processor': json_to_request_params})
-
- # Send tracebacks in error responses. They're hidden by the
- # error_page function for client errors (code 400-499).
- app_config.update({'request.show_tracebacks': True})
- self.force_traceback = force_traceback
-
- # Patch CherryPy error handler to never pad out error messages.
- # This isn't necessary, but then again, neither is padding the
- # error messages.
- cherrypy._cperror._ie_friendly_error_sizes = {}
-
- # The manager maintains internal state and isn't necessarily
- # thread-safe, so wrap it in the serializer.
- manager = serializer_proxy(nilmrun.processmanager.ProcessManager)()
-
- # Build up the application and mount it
- self._manager = manager
- root = App()
- root.process = AppProcess(manager)
- root.run = AppRun(manager)
- cherrypy.tree.apps = {}
- cherrypy.tree.mount(root, basepath, config={"/": app_config})
-
- # Set up the WSGI application pointer for external programs
- self.wsgi_application = cherrypy.tree
-
- def json_error_page(self, status, message, traceback, version):
- """Return a custom error page in JSON so the client can parse it"""
- return json_error_page(status, message, traceback, version,
- self.force_traceback)
-
- def start(self, blocking=False, event=None):
- cherrypy_start(blocking, event)
-
- def stop(self):
- cherrypy_stop()
-
-
- # Multiple processes and threads should be OK here, but we'll still
- # follow the NilmDB approach of having just one globally initialized
- # copy of the server object.
- _wsgi_server = None
-
-
- def wsgi_application(basepath): # pragma: no cover
- """Return a WSGI application object.
-
- 'basepath' is the URL path of the application base, which
- is the same as the first argument to Apache's WSGIScriptAlias
- directive.
- """
- def application(environ, start_response):
- global _wsgi_server
- if _wsgi_server is None:
- # Try to start the server
- try:
- _wsgi_server = nilmrun.server.Server(
- basepath=basepath.rstrip('/'))
- except Exception:
- # Build an error message on failure
- import pprint
- err = "Initializing nilmrun failed:\n\n"
- err += traceback.format_exc()
- try:
- import pwd
- import grp
- err += sprintf("\nRunning as: uid=%d (%s), gid=%d (%s) "
- "on host %s, pid %d\n",
- os.getuid(), pwd.getpwuid(os.getuid())[0],
- os.getgid(), grp.getgrgid(os.getgid())[0],
- socket.gethostname(), os.getpid())
- except ImportError:
- pass
- err += sprintf("\nEnvironment:\n%s\n", pprint.pformat(environ))
- if _wsgi_server is None:
- # Serve up the error with our own mini WSGI app.
- headers = [
- ('Content-type', 'text/plain'),
- ('Content-length', str(len(err)))
- ]
- start_response("500 Internal Server Error", headers)
- return [err]
-
- # Call the normal application
- return _wsgi_server.wsgi_application(environ, start_response)
- return application
|