"""CherryPy-based server for running NILM filters via HTTP""" import cherrypy import sys import os import socket import simplejson as json import decorator import psutil import traceback import argparse import nilmdb from nilmdb.utils.printf import * from nilmdb.server.serverutil import ( chunked_response, response_type, workaround_cp_bug_1200, exception_to_httperror, CORS_allow, json_to_request_params, json_error_page, cherrypy_start, cherrypy_stop, ) import nilmrun import nilmrun.trainola # 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): return { "pid": pid, "alive": self.manager[pid].alive, "error": self.manager[pid].error, "name": self.manager[pid].name, "start_time": self.manager[pid].start_time, "parameters": self.manager[pid].parameters, "log": self.manager[pid].log, } # /process/status @cherrypy.expose @cherrypy.tools.json_out() def status(self, pid, clear = False): 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 list(self.manager) # /process/kill @cherrypy.expose @cherrypy.tools.json_in() @cherrypy.tools.json_out() @cherrypy.tools.CORS_allow(methods = ["POST"]) def kill(self, pid): if pid not in self.manager: raise cherrypy.HTTPError("404 Not Found", "No such PID") if not self.manager.terminate(pid): raise cherrypy.HTTPError("503 Service Unavailable", "Failed to stop process") status = self.process_status(pid) manager.remove(pid) class AppFilter(object): def __init__(self, manager): self.manager = manager # /filter/trainola @cherrypy.expose @cherrypy.tools.json_in() @cherrypy.tools.json_out() @exception_to_httperror(KeyError, ValueError) @cherrypy.tools.CORS_allow(methods = ["POST"]) def trainola(self, data): return self.manager.run("trainola", nilmrun.trainola.trainola, data) class Server(object): def __init__(self, host = '127.0.0.1', port = 8080, embedded = True, # hide diagnostics and output, etc force_traceback = False, # include traceback in all errors basepath = '', # base URL path for cherrypy.tree ): self.embedded = embedded # Build up global server configuration cherrypy.config.update({ 'server.socket_host': host, 'server.socket_port': port, 'engine.autoreload_on': False, 'server.max_request_body_size': 8*1024*1024, }) if self.embedded: cherrypy.config.update({ 'environment': 'embedded' }) # 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 = {} # Build up the application and mount it manager = nilmrun.processmanager.ProcessManager() root = App() root.process = AppProcess(manager) root.filter = AppFilter(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, self.embedded) 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( embedded = True, basepath = basepath.rstrip('/')) except Exception: # Build an error message on failure import pprint err = sprintf("Initializing nilmrun failed:\n\n", dbpath) 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