You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

225 lines
7.6 KiB

  1. """CherryPy-based server for running NILM filters via HTTP"""
  2. import cherrypy
  3. import sys
  4. import os
  5. import socket
  6. import simplejson as json
  7. import decorator
  8. import psutil
  9. import traceback
  10. import argparse
  11. import nilmdb
  12. from nilmdb.utils.printf import *
  13. from nilmdb.server.serverutil import (
  14. chunked_response,
  15. response_type,
  16. workaround_cp_bug_1200,
  17. exception_to_httperror,
  18. CORS_allow,
  19. json_to_request_params,
  20. json_error_page,
  21. cherrypy_start,
  22. cherrypy_stop,
  23. )
  24. import nilmrun
  25. import nilmrun.trainola
  26. # Add CORS_allow tool
  27. cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow)
  28. # CherryPy apps
  29. class App(object):
  30. """Root application for NILM runner"""
  31. def __init__(self):
  32. pass
  33. # /
  34. @cherrypy.expose
  35. def index(self):
  36. cherrypy.response.headers['Content-Type'] = 'text/plain'
  37. msg = sprintf("This is NilmRun version %s, running on host %s.\n",
  38. nilmdb.__version__, socket.getfqdn())
  39. return msg
  40. # /favicon.ico
  41. @cherrypy.expose
  42. def favicon_ico(self):
  43. raise cherrypy.NotFound()
  44. # /version
  45. @cherrypy.expose
  46. @cherrypy.tools.json_out()
  47. def version(self):
  48. return nilmrun.__version__
  49. class AppThread(object):
  50. def __init__(self, manager):
  51. self.manager = manager
  52. def thread_status(self, pid):
  53. return {
  54. "pid": pid,
  55. "alive": self.manager[pid].alive,
  56. "name": self.manager[pid].name,
  57. "start_time": self.manager[pid].start_time,
  58. "parameters": self.manager[pid].parameters,
  59. "log": self.manager[pid].log,
  60. }
  61. # /thread/status
  62. @cherrypy.expose
  63. @cherrypy.tools.json_out()
  64. def status(self, pid, clear = False):
  65. if pid not in self.manager:
  66. raise cherrypy.NotFound()
  67. status = thread_status(pid)
  68. if clear:
  69. self.manager[pid].clear_log()
  70. return status
  71. # /thread/kill
  72. @cherrypy.expose
  73. @cherrypy.tools.json_in()
  74. @cherrypy.tools.json_out()
  75. @cherrypy.tools.CORS_allow(methods = ["POST"])
  76. def kill(self, pid):
  77. if pid not in self.manager:
  78. raise CherryPy.NotFound()
  79. if not self.manager.terminate(pid):
  80. raise cherrypy.HTTPError("503 Service Unavailable",
  81. "Failed to stop thread")
  82. status = thread_status(pid)
  83. manager.remove(pid)
  84. class AppFilter(object):
  85. # /filter/trainola
  86. @cherrypy.expose
  87. @cherrypy.tools.json_in()
  88. @cherrypy.tools.json_out()
  89. @exception_to_httperror(KeyError, ValueError, NilmDBError)
  90. @cherrypy.tools.CORS_allow(methods = ["POST"])
  91. def trainola(self, data):
  92. return nilmrun.trainola.trainola(data)
  93. class Server(object):
  94. def __init__(self, host = '127.0.0.1', port = 8080,
  95. embedded = True, # hide diagnostics and output, etc
  96. force_traceback = False, # include traceback in all errors
  97. basepath = '', # base URL path for cherrypy.tree
  98. ):
  99. self.embedded = embedded
  100. # Build up global server configuration
  101. cherrypy.config.update({
  102. 'server.socket_host': host,
  103. 'server.socket_port': port,
  104. 'engine.autoreload_on': False,
  105. 'server.max_request_body_size': 8*1024*1024,
  106. })
  107. if self.embedded:
  108. cherrypy.config.update({ 'environment': 'embedded' })
  109. # Build up application specific configuration
  110. app_config = {}
  111. app_config.update({
  112. 'error_page.default': self.json_error_page,
  113. })
  114. # Some default headers to just help identify that things are working
  115. app_config.update({ 'response.headers.X-Jim-Is-Awesome': 'yeah' })
  116. # Set up Cross-Origin Resource Sharing (CORS) handler so we
  117. # can correctly respond to browsers' CORS preflight requests.
  118. # This also limits verbs to GET and HEAD by default.
  119. app_config.update({ 'tools.CORS_allow.on': True,
  120. 'tools.CORS_allow.methods': ['GET', 'HEAD'] })
  121. # Configure the 'json_in' tool to also allow other content-types
  122. # (like x-www-form-urlencoded), and to treat JSON as a dict that
  123. # fills requests.param.
  124. app_config.update({ 'tools.json_in.force': False,
  125. 'tools.json_in.processor': json_to_request_params })
  126. # Send tracebacks in error responses. They're hidden by the
  127. # error_page function for client errors (code 400-499).
  128. app_config.update({ 'request.show_tracebacks' : True })
  129. self.force_traceback = force_traceback
  130. # Patch CherryPy error handler to never pad out error messages.
  131. # This isn't necessary, but then again, neither is padding the
  132. # error messages.
  133. cherrypy._cperror._ie_friendly_error_sizes = {}
  134. # Build up the application and mount it
  135. root = App()
  136. root.thread = AppThread()
  137. root.filter = AppFilter()
  138. cherrypy.tree.apps = {}
  139. cherrypy.tree.mount(root, basepath, config = { "/" : app_config })
  140. # Set up the WSGI application pointer for external programs
  141. self.wsgi_application = cherrypy.tree
  142. def json_error_page(self, status, message, traceback, version):
  143. """Return a custom error page in JSON so the client can parse it"""
  144. return json_error_page(status, message, traceback, version,
  145. self.force_traceback)
  146. def start(self, blocking = False, event = None):
  147. cherrypy_start(blocking, event, self.embedded)
  148. def stop(self):
  149. cherrypy_stop()
  150. # Multiple processes and threads should be OK here, but we'll still
  151. # follow the NilmDB approach of having just one globally initialized
  152. # copy of the server object.
  153. _wsgi_server = None
  154. def wsgi_application(basepath): # pragma: no cover
  155. """Return a WSGI application object.
  156. 'basepath' is the URL path of the application base, which
  157. is the same as the first argument to Apache's WSGIScriptAlias
  158. directive.
  159. """
  160. def application(environ, start_response):
  161. global _wsgi_server
  162. if _wsgi_server is None:
  163. # Try to start the server
  164. try:
  165. _wsgi_server = nilmrun.server.Server(
  166. embedded = True,
  167. basepath = basepath.rstrip('/'))
  168. except Exception:
  169. # Build an error message on failure
  170. import pprint
  171. err = sprintf("Initializing nilmrun failed:\n\n",
  172. dbpath)
  173. err += traceback.format_exc()
  174. try:
  175. import pwd
  176. import grp
  177. err += sprintf("\nRunning as: uid=%d (%s), gid=%d (%s) "
  178. "on host %s, pid %d\n",
  179. os.getuid(), pwd.getpwuid(os.getuid())[0],
  180. os.getgid(), grp.getgrgid(os.getgid())[0],
  181. socket.gethostname(), os.getpid())
  182. except ImportError:
  183. pass
  184. err += sprintf("\nEnvironment:\n%s\n", pprint.pformat(environ))
  185. if _wsgi_server is None:
  186. # Serve up the error with our own mini WSGI app.
  187. headers = [ ('Content-type', 'text/plain'),
  188. ('Content-length', str(len(err))) ]
  189. start_response("500 Internal Server Error", headers)
  190. return [err]
  191. # Call the normal application
  192. return _wsgi_server.wsgi_application(environ, start_response)
  193. return application