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.
 
 
 

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