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.
 
 
 

252 lines
8.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 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.trainola
  27. import nilmrun.dummyfilter
  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("trainola", nilmrun.trainola.trainola, data)
  108. # /filter/dummy
  109. @cherrypy.expose
  110. @cherrypy.tools.json_in()
  111. @cherrypy.tools.json_out()
  112. @exception_to_httperror(KeyError, ValueError)
  113. @cherrypy.tools.CORS_allow(methods = ["POST"])
  114. def dummy(self, count):
  115. return self.manager.run("dummy", nilmrun.dummyfilter.dummy, int(count))
  116. class Server(object):
  117. def __init__(self, host = '127.0.0.1', port = 8080,
  118. embedded = True, # hide diagnostics and output, etc
  119. force_traceback = False, # include traceback in all errors
  120. basepath = '', # base URL path for cherrypy.tree
  121. ):
  122. self.embedded = embedded
  123. # Build up global server configuration
  124. cherrypy.config.update({
  125. 'server.socket_host': host,
  126. 'server.socket_port': port,
  127. 'engine.autoreload_on': False,
  128. 'server.max_request_body_size': 8*1024*1024,
  129. })
  130. if self.embedded:
  131. cherrypy.config.update({ 'environment': 'embedded' })
  132. # Build up application specific configuration
  133. app_config = {}
  134. app_config.update({
  135. 'error_page.default': self.json_error_page,
  136. })
  137. # Some default headers to just help identify that things are working
  138. app_config.update({ 'response.headers.X-Jim-Is-Awesome': 'yeah' })
  139. # Set up Cross-Origin Resource Sharing (CORS) handler so we
  140. # can correctly respond to browsers' CORS preflight requests.
  141. # This also limits verbs to GET and HEAD by default.
  142. app_config.update({ 'tools.CORS_allow.on': True,
  143. 'tools.CORS_allow.methods': ['GET', 'HEAD'] })
  144. # Configure the 'json_in' tool to also allow other content-types
  145. # (like x-www-form-urlencoded), and to treat JSON as a dict that
  146. # fills requests.param.
  147. app_config.update({ 'tools.json_in.force': False,
  148. 'tools.json_in.processor': json_to_request_params })
  149. # Send tracebacks in error responses. They're hidden by the
  150. # error_page function for client errors (code 400-499).
  151. app_config.update({ 'request.show_tracebacks' : True })
  152. self.force_traceback = force_traceback
  153. # Patch CherryPy error handler to never pad out error messages.
  154. # This isn't necessary, but then again, neither is padding the
  155. # error messages.
  156. cherrypy._cperror._ie_friendly_error_sizes = {}
  157. # Build up the application and mount it
  158. manager = nilmrun.processmanager.ProcessManager()
  159. root = App()
  160. root.process = AppProcess(manager)
  161. root.filter = AppFilter(manager)
  162. cherrypy.tree.apps = {}
  163. cherrypy.tree.mount(root, basepath, config = { "/" : app_config })
  164. # Set up the WSGI application pointer for external programs
  165. self.wsgi_application = cherrypy.tree
  166. def json_error_page(self, status, message, traceback, version):
  167. """Return a custom error page in JSON so the client can parse it"""
  168. return json_error_page(status, message, traceback, version,
  169. self.force_traceback)
  170. def start(self, blocking = False, event = None):
  171. cherrypy_start(blocking, event, self.embedded)
  172. def stop(self):
  173. cherrypy_stop()
  174. # Multiple processes and threads should be OK here, but we'll still
  175. # follow the NilmDB approach of having just one globally initialized
  176. # copy of the server object.
  177. _wsgi_server = None
  178. def wsgi_application(basepath): # pragma: no cover
  179. """Return a WSGI application object.
  180. 'basepath' is the URL path of the application base, which
  181. is the same as the first argument to Apache's WSGIScriptAlias
  182. directive.
  183. """
  184. def application(environ, start_response):
  185. global _wsgi_server
  186. if _wsgi_server is None:
  187. # Try to start the server
  188. try:
  189. _wsgi_server = nilmrun.server.Server(
  190. embedded = True,
  191. basepath = basepath.rstrip('/'))
  192. except Exception:
  193. # Build an error message on failure
  194. import pprint
  195. err = sprintf("Initializing nilmrun failed:\n\n",
  196. dbpath)
  197. err += traceback.format_exc()
  198. try:
  199. import pwd
  200. import grp
  201. err += sprintf("\nRunning as: uid=%d (%s), gid=%d (%s) "
  202. "on host %s, pid %d\n",
  203. os.getuid(), pwd.getpwuid(os.getuid())[0],
  204. os.getgid(), grp.getgrgid(os.getgid())[0],
  205. socket.gethostname(), os.getpid())
  206. except ImportError:
  207. pass
  208. err += sprintf("\nEnvironment:\n%s\n", pprint.pformat(environ))
  209. if _wsgi_server is None:
  210. # Serve up the error with our own mini WSGI app.
  211. headers = [ ('Content-type', 'text/plain'),
  212. ('Content-length', str(len(err))) ]
  213. start_response("500 Internal Server Error", headers)
  214. return [err]
  215. # Call the normal application
  216. return _wsgi_server.wsgi_application(environ, start_response)
  217. return application