diff --git a/README.txt b/README.txt index ce291ec..c310ba3 100644 --- a/README.txt +++ b/README.txt @@ -7,10 +7,9 @@ Prerequisites: sudo apt-get install python2.7 python-setuptools # Base dependencies - sudo apt-get install python-cherrypy3 python-simplejson sudo apt-get install python-numpy python-scipy - nilmdb (1.6.3+) + nilmdb (1.8.0+) Install: diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..94a8e2c --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +# Command line scripts diff --git a/scripts/nilmrun_server.py b/scripts/nilmrun_server.py new file mode 100755 index 0000000..f595959 --- /dev/null +++ b/scripts/nilmrun_server.py @@ -0,0 +1,58 @@ +#!/usr/bin/python + +import nilmrun.server +import argparse +import os +import socket + +def main(): + """Main entry point for the 'nilmrun-server' command line script""" + + parser = argparse.ArgumentParser( + description = 'Run the NilmRun server', + formatter_class = argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument("-V", "--version", action="version", + version = nilmrun.__version__) + + group = parser.add_argument_group("Standard options") + group.add_argument('-a', '--address', + help = 'Only listen on the given address', + default = '0.0.0.0') + group.add_argument('-p', '--port', help = 'Listen on the given port', + type = int, default = 12380) + group.add_argument('-q', '--quiet', help = 'Silence output', + action = 'store_true') + group.add_argument('-t', '--traceback', + help = 'Provide tracebacks in client errors', + action = 'store_true', default = False) + + args = parser.parse_args() + + # Configure the server + if args.quiet: + embedded = True + else: + embedded = False + server = nilmrun.server.Server(host = args.address, + port = args.port, + embedded = embedded, + force_traceback = args.traceback) + + # Print info + if not args.quiet: + print "Version: %s" % nilmrun.__version__ + if args.address == '0.0.0.0' or args.address == '::': + host = socket.getfqdn() + else: + host = args.address + print "NilmRun Server URL: http://%s:%d/" % ( host, args.port) + print "----" + + server.start(blocking = True) + + if not args.quiet: + print "Shutting down" + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index e9813e4..3f40996 100755 --- a/setup.py +++ b/setup.py @@ -61,17 +61,20 @@ setup(name='nilmrun', long_description = "NILM Database Filter Runner", license = "Proprietary", author_email = 'jim@jtan.com', - install_requires = [ 'nilmdb >= 1.6.3', + install_requires = [ 'nilmdb >= 1.8.0', 'nilmtools >= 1.2.2', 'numpy', 'scipy', ], packages = [ 'nilmrun', + 'nilmrun.scripts', ], - package_dir = { 'nilmrun': 'src' }, + package_dir = { 'nilmrun': 'src', + 'nilmrun.scripts': 'scripts', + }, entry_points = { 'console_scripts': [ - 'nilmrun-server = nilmrun.server:main', + 'nilmrun-server = nilmrun.scripts.nilmrun_server:main', ], }, zip_safe = False, diff --git a/src/server.py b/src/server.py new file mode 100644 index 0000000..279db9b --- /dev/null +++ b/src/server.py @@ -0,0 +1,170 @@ +"""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 + +# Add CORS_allow tool +cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow) + +# CherryPy apps +class NilmRunApp(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", + nilmdb.__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 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 + root = NilmRunApp() + 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