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.
 
 
 

692 lines
27 KiB

  1. """CherryPy-based server for accessing NILM database via HTTP"""
  2. # Need absolute_import so that "import nilmdb" won't pull in
  3. # nilmdb.py, but will pull the nilmdb module instead.
  4. from __future__ import absolute_import
  5. import nilmdb.server
  6. from nilmdb.utils.printf import *
  7. from nilmdb.server.errors import NilmDBError
  8. from nilmdb.utils.time import string_to_timestamp
  9. import cherrypy
  10. import sys
  11. import os
  12. import socket
  13. import simplejson as json
  14. import decorator
  15. import psutil
  16. import traceback
  17. class NilmApp(object):
  18. def __init__(self, db):
  19. self.db = db
  20. # Decorators
  21. def chunked_response(func):
  22. """Decorator to enable chunked responses."""
  23. # Set this to False to get better tracebacks from some requests
  24. # (/stream/extract, /stream/intervals).
  25. func._cp_config = { 'response.stream': True }
  26. return func
  27. def response_type(content_type):
  28. """Return a decorator-generating function that sets the
  29. response type to the specified string."""
  30. def wrapper(func, *args, **kwargs):
  31. cherrypy.response.headers['Content-Type'] = content_type
  32. return func(*args, **kwargs)
  33. return decorator.decorator(wrapper)
  34. @decorator.decorator
  35. def workaround_cp_bug_1200(func, *args, **kwargs): # pragma: no cover
  36. """Decorator to work around CherryPy bug #1200 in a response
  37. generator.
  38. Even if chunked responses are disabled, LookupError or
  39. UnicodeError exceptions may still be swallowed by CherryPy due to
  40. bug #1200. This throws them as generic Exceptions instead so that
  41. they make it through.
  42. """
  43. exc_info = None
  44. try:
  45. for val in func(*args, **kwargs):
  46. yield val
  47. except (LookupError, UnicodeError):
  48. # Re-raise it, but maintain the original traceback
  49. exc_info = sys.exc_info()
  50. new_exc = Exception(exc_info[0].__name__ + ": " + str(exc_info[1]))
  51. raise new_exc, None, exc_info[2]
  52. finally:
  53. del exc_info
  54. def exception_to_httperror(*expected):
  55. """Return a decorator-generating function that catches expected
  56. errors and throws a HTTPError describing it instead.
  57. @exception_to_httperror(NilmDBError, ValueError)
  58. def foo():
  59. pass
  60. """
  61. def wrapper(func, *args, **kwargs):
  62. exc_info = None
  63. try:
  64. return func(*args, **kwargs)
  65. except expected:
  66. # Re-raise it, but maintain the original traceback
  67. exc_info = sys.exc_info()
  68. new_exc = cherrypy.HTTPError("400 Bad Request", str(exc_info[1]))
  69. raise new_exc, None, exc_info[2]
  70. finally:
  71. del exc_info
  72. # We need to preserve the function's argspecs for CherryPy to
  73. # handle argument errors correctly. Decorator.decorator takes
  74. # care of that.
  75. return decorator.decorator(wrapper)
  76. # Custom CherryPy tools
  77. def CORS_allow(methods):
  78. """This does several things:
  79. Handles CORS preflight requests.
  80. Adds Allow: header to all requests.
  81. Raise 405 if request.method not in method.
  82. It is similar to cherrypy.tools.allow, with the CORS stuff added.
  83. """
  84. request = cherrypy.request.headers
  85. response = cherrypy.response.headers
  86. if not isinstance(methods, (tuple, list)): # pragma: no cover
  87. methods = [ methods ]
  88. methods = [ m.upper() for m in methods if m ]
  89. if not methods: # pragma: no cover
  90. methods = [ 'GET', 'HEAD' ]
  91. elif 'GET' in methods and 'HEAD' not in methods: # pragma: no cover
  92. methods.append('HEAD')
  93. response['Allow'] = ', '.join(methods)
  94. # Allow all origins
  95. if 'Origin' in request:
  96. response['Access-Control-Allow-Origin'] = request['Origin']
  97. # If it's a CORS request, send response.
  98. request_method = request.get("Access-Control-Request-Method", None)
  99. request_headers = request.get("Access-Control-Request-Headers", None)
  100. if (cherrypy.request.method == "OPTIONS" and
  101. request_method and request_headers):
  102. response['Access-Control-Allow-Headers'] = request_headers
  103. response['Access-Control-Allow-Methods'] = ', '.join(methods)
  104. # Try to stop further processing and return a 200 OK
  105. cherrypy.response.status = "200 OK"
  106. cherrypy.response.body = ""
  107. cherrypy.request.handler = lambda: ""
  108. return
  109. # Reject methods that were not explicitly allowed
  110. if cherrypy.request.method not in methods:
  111. raise cherrypy.HTTPError(405)
  112. cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow)
  113. # Helper for json_in tool to process JSON data into normal request
  114. # parameters.
  115. def json_to_request_params(body):
  116. cherrypy.lib.jsontools.json_processor(body)
  117. if not isinstance(cherrypy.request.json, dict):
  118. raise cherrypy.HTTPError(415)
  119. cherrypy.request.params.update(cherrypy.request.json)
  120. # CherryPy apps
  121. class Root(NilmApp):
  122. """Root application for NILM database"""
  123. def __init__(self, db):
  124. super(Root, self).__init__(db)
  125. # /
  126. @cherrypy.expose
  127. def index(self):
  128. cherrypy.response.headers['Content-Type'] = 'text/plain'
  129. msg = sprintf("This is NilmDB version %s, running on %s.\n",
  130. nilmdb.__version__, socket.gethostname())
  131. return msg
  132. # /favicon.ico
  133. @cherrypy.expose
  134. def favicon_ico(self):
  135. raise cherrypy.NotFound()
  136. # /version
  137. @cherrypy.expose
  138. @cherrypy.tools.json_out()
  139. def version(self):
  140. return nilmdb.__version__
  141. # /dbinfo
  142. @cherrypy.expose
  143. @cherrypy.tools.json_out()
  144. def dbinfo(self):
  145. """Return a dictionary with the database path,
  146. size of the database in bytes, and free disk space in bytes"""
  147. path = self.db.get_basepath()
  148. usage = psutil.disk_usage(path)
  149. dbsize = nilmdb.utils.du(path)
  150. return { "path": path,
  151. "size": dbsize,
  152. "other": usage.used - dbsize,
  153. "reserved": usage.total - usage.used - usage.free,
  154. "free": usage.free }
  155. class Stream(NilmApp):
  156. """Stream-specific operations"""
  157. # Helpers
  158. def _get_times(self, start_param, end_param):
  159. (start, end) = (None, None)
  160. if start_param is not None:
  161. start = string_to_timestamp(start_param)
  162. if end_param is not None:
  163. end = string_to_timestamp(end_param)
  164. if start is not None and end is not None:
  165. if start >= end:
  166. raise cherrypy.HTTPError(
  167. "400 Bad Request",
  168. sprintf("start must precede end (%s >= %s)",
  169. start_param, end_param))
  170. return (start, end)
  171. # /stream/list
  172. # /stream/list?layout=float32_8
  173. # /stream/list?path=/newton/prep&extended=1
  174. @cherrypy.expose
  175. @cherrypy.tools.json_out()
  176. def list(self, path = None, layout = None, extended = None):
  177. """List all streams in the database. With optional path or
  178. layout parameter, just list streams that match the given path
  179. or layout.
  180. If extent is not given, returns a list of lists containing
  181. the path and layout: [ path, layout ]
  182. If extended is provided, returns a list of lists containing
  183. extended info: [ path, layout, extent_min, extent_max,
  184. total_rows, total_seconds ]. More data may be added.
  185. """
  186. return self.db.stream_list(path, layout, bool(extended))
  187. # /stream/create?path=/newton/prep&layout=float32_8
  188. @cherrypy.expose
  189. @cherrypy.tools.json_in()
  190. @cherrypy.tools.json_out()
  191. @exception_to_httperror(NilmDBError, ValueError)
  192. @cherrypy.tools.CORS_allow(methods = ["POST"])
  193. def create(self, path, layout):
  194. """Create a new stream in the database. Provide path
  195. and one of the nilmdb.layout.layouts keys.
  196. """
  197. return self.db.stream_create(path, layout)
  198. # /stream/destroy?path=/newton/prep
  199. @cherrypy.expose
  200. @cherrypy.tools.json_in()
  201. @cherrypy.tools.json_out()
  202. @exception_to_httperror(NilmDBError)
  203. @cherrypy.tools.CORS_allow(methods = ["POST"])
  204. def destroy(self, path):
  205. """Delete a stream. Fails if any data is still present."""
  206. return self.db.stream_destroy(path)
  207. # /stream/rename?oldpath=/newton/prep&newpath=/newton/prep/1
  208. @cherrypy.expose
  209. @cherrypy.tools.json_in()
  210. @cherrypy.tools.json_out()
  211. @exception_to_httperror(NilmDBError, ValueError)
  212. @cherrypy.tools.CORS_allow(methods = ["POST"])
  213. def rename(self, oldpath, newpath):
  214. """Rename a stream."""
  215. return self.db.stream_rename(oldpath, newpath)
  216. # /stream/get_metadata?path=/newton/prep
  217. # /stream/get_metadata?path=/newton/prep&key=foo&key=bar
  218. @cherrypy.expose
  219. @cherrypy.tools.json_out()
  220. def get_metadata(self, path, key=None):
  221. """Get metadata for the named stream. If optional
  222. key parameters are specified, only return metadata
  223. matching the given keys."""
  224. try:
  225. data = self.db.stream_get_metadata(path)
  226. except nilmdb.server.nilmdb.StreamError as e:
  227. raise cherrypy.HTTPError("404 Not Found", e.message)
  228. if key is None: # If no keys specified, return them all
  229. key = data.keys()
  230. elif not isinstance(key, list):
  231. key = [ key ]
  232. result = {}
  233. for k in key:
  234. if k in data:
  235. result[k] = data[k]
  236. else: # Return "None" for keys with no matching value
  237. result[k] = None
  238. return result
  239. # Helper for set_metadata and get_metadata
  240. def _metadata_helper(self, function, path, data):
  241. if not isinstance(data, dict):
  242. try:
  243. data = dict(json.loads(data))
  244. except TypeError as e:
  245. raise NilmDBError("can't parse 'data' parameter: " + e.message)
  246. for key in data:
  247. if not (isinstance(data[key], basestring) or
  248. isinstance(data[key], float) or
  249. isinstance(data[key], int)):
  250. raise NilmDBError("metadata values must be a string or number")
  251. function(path, data)
  252. # /stream/set_metadata?path=/newton/prep&data=<json>
  253. @cherrypy.expose
  254. @cherrypy.tools.json_in()
  255. @cherrypy.tools.json_out()
  256. @exception_to_httperror(NilmDBError, LookupError)
  257. @cherrypy.tools.CORS_allow(methods = ["POST"])
  258. def set_metadata(self, path, data):
  259. """Set metadata for the named stream, replacing any existing
  260. metadata. Data can be json-encoded or a plain dictionary."""
  261. self._metadata_helper(self.db.stream_set_metadata, path, data)
  262. # /stream/update_metadata?path=/newton/prep&data=<json>
  263. @cherrypy.expose
  264. @cherrypy.tools.json_in()
  265. @cherrypy.tools.json_out()
  266. @exception_to_httperror(NilmDBError, LookupError, ValueError)
  267. @cherrypy.tools.CORS_allow(methods = ["POST"])
  268. def update_metadata(self, path, data):
  269. """Set metadata for the named stream, replacing any existing
  270. metadata. Data can be json-encoded or a plain dictionary."""
  271. self._metadata_helper(self.db.stream_update_metadata, path, data)
  272. # /stream/insert?path=/newton/prep
  273. @cherrypy.expose
  274. @cherrypy.tools.json_out()
  275. @exception_to_httperror(NilmDBError, ValueError)
  276. @cherrypy.tools.CORS_allow(methods = ["PUT"])
  277. def insert(self, path, start, end, binary = False):
  278. """
  279. Insert new data into the database. Provide textual data
  280. (matching the path's layout) as a HTTP PUT.
  281. If 'binary' is True, expect raw binary data, rather than lines
  282. of ASCII-formatted data. Raw binary data is always
  283. little-endian and matches the database types (including an
  284. int64 timestamp).
  285. """
  286. # Important that we always read the input before throwing any
  287. # errors, to keep lengths happy for persistent connections.
  288. # Note that CherryPy 3.2.2 has a bug where this fails for GET
  289. # requests, if we ever want to handle those (issue #1134)
  290. body = cherrypy.request.body.read()
  291. # Verify content type for binary data
  292. content_type = cherrypy.request.headers.get('content-type')
  293. if binary and content_type:
  294. if content_type != "application/octet-stream":
  295. raise cherrypy.HTTPError("400", "Content type must be "
  296. "application/octet-stream for "
  297. "binary data, not " + content_type)
  298. # Check path and get layout
  299. if len(self.db.stream_list(path = path)) != 1:
  300. raise cherrypy.HTTPError("404", "No such stream: " + path)
  301. # Check limits
  302. (start, end) = self._get_times(start, end)
  303. # Pass the data directly to nilmdb, which will parse it and
  304. # raise a ValueError if there are any problems.
  305. self.db.stream_insert(path, start, end, body, binary)
  306. # Done
  307. return
  308. # /stream/remove?path=/newton/prep
  309. # /stream/remove?path=/newton/prep&start=1234567890.0&end=1234567899.0
  310. @cherrypy.expose
  311. @cherrypy.tools.json_in()
  312. @cherrypy.tools.CORS_allow(methods = ["POST"])
  313. @chunked_response
  314. @response_type("application/x-json-stream")
  315. def remove(self, path, start = None, end = None):
  316. """
  317. Remove data from the backend database. Removes all data in
  318. the interval [start, end).
  319. Returns the number of data points removed. Since this is a potentially
  320. long-running operation, multiple numbers may be returned as the
  321. data gets removed from the backend database. The total number of
  322. points removed is the sum of all of these numbers.
  323. """
  324. (start, end) = self._get_times(start, end)
  325. if len(self.db.stream_list(path = path)) != 1:
  326. raise cherrypy.HTTPError("404", "No such stream: " + path)
  327. @workaround_cp_bug_1200
  328. def content(start, end):
  329. # Note: disable chunked responses to see tracebacks from here.
  330. while True:
  331. (removed, restart) = self.db.stream_remove(path, start, end)
  332. yield json.dumps(removed) + "\r\n"
  333. if restart is None:
  334. break
  335. start = restart
  336. return content(start, end)
  337. # /stream/intervals?path=/newton/prep
  338. # /stream/intervals?path=/newton/prep&start=1234567890.0&end=1234567899.0
  339. # /stream/intervals?path=/newton/prep&diffpath=/newton/prep2
  340. @cherrypy.expose
  341. @chunked_response
  342. @response_type("application/x-json-stream")
  343. def intervals(self, path, start = None, end = None, diffpath = None):
  344. """
  345. Get intervals from backend database. Streams the resulting
  346. intervals as JSON strings separated by CR LF pairs. This may
  347. make multiple requests to the nilmdb backend to avoid causing
  348. it to block for too long.
  349. Returns intervals between 'start' and 'end' belonging to
  350. 'path'. If 'diff' is provided, the set-difference between
  351. intervals in 'path' and intervals in 'diffpath' are
  352. returned instead.
  353. Note that the response type is the non-standard
  354. 'application/x-json-stream' for lack of a better option.
  355. """
  356. (start, end) = self._get_times(start, end)
  357. if len(self.db.stream_list(path = path)) != 1:
  358. raise cherrypy.HTTPError("404", "No such stream: " + path)
  359. if diffpath and len(self.db.stream_list(path = diffpath)) != 1:
  360. raise cherrypy.HTTPError("404", "No such stream: " + diffpath)
  361. @workaround_cp_bug_1200
  362. def content(start, end):
  363. # Note: disable chunked responses to see tracebacks from here.
  364. while True:
  365. (ints, restart) = self.db.stream_intervals(path, start, end,
  366. diffpath)
  367. response = ''.join([ json.dumps(i) + "\r\n" for i in ints ])
  368. yield response
  369. if restart is None:
  370. break
  371. start = restart
  372. return content(start, end)
  373. # /stream/extract?path=/newton/prep&start=1234567890.0&end=1234567899.0
  374. @cherrypy.expose
  375. @chunked_response
  376. def extract(self, path, start = None, end = None,
  377. count = False, markup = False, binary = False):
  378. """
  379. Extract data from backend database. Streams the resulting
  380. entries as ASCII text lines separated by newlines. This may
  381. make multiple requests to the nilmdb backend to avoid causing
  382. it to block for too long.
  383. If 'count' is True, returns a count rather than actual data.
  384. If 'markup' is True, adds comments to the stream denoting each
  385. interval's start and end timestamp.
  386. If 'binary' is True, return raw binary data, rather than lines
  387. of ASCII-formatted data. Raw binary data is always
  388. little-endian and matches the database types (including an
  389. int64 timestamp).
  390. """
  391. (start, end) = self._get_times(start, end)
  392. # Check path and get layout
  393. if len(self.db.stream_list(path = path)) != 1:
  394. raise cherrypy.HTTPError("404", "No such stream: " + path)
  395. if binary:
  396. content_type = "application/octet-stream"
  397. if markup or count:
  398. raise cherrypy.HTTPError("400", "can't mix binary and "
  399. "markup or count modes")
  400. else:
  401. content_type = "text/plain"
  402. cherrypy.response.headers['Content-Type'] = content_type
  403. @workaround_cp_bug_1200
  404. def content(start, end):
  405. # Note: disable chunked responses to see tracebacks from here.
  406. if count:
  407. matched = self.db.stream_extract(path, start, end,
  408. count = True)
  409. yield sprintf("%d\n", matched)
  410. return
  411. while True:
  412. (data, restart) = self.db.stream_extract(
  413. path, start, end, count = False,
  414. markup = markup, binary = binary)
  415. yield data
  416. if restart is None:
  417. return
  418. start = restart
  419. return content(start, end)
  420. class Exiter(object):
  421. """App that exits the server, for testing"""
  422. @cherrypy.expose
  423. def index(self):
  424. cherrypy.response.headers['Content-Type'] = 'text/plain'
  425. def content():
  426. yield 'Exiting by request'
  427. raise SystemExit
  428. return content()
  429. index._cp_config = { 'response.stream': True }
  430. class Server(object):
  431. def __init__(self, db, host = '127.0.0.1', port = 8080,
  432. stoppable = False, # whether /exit URL exists
  433. embedded = True, # hide diagnostics and output, etc
  434. fast_shutdown = False, # don't wait for clients to disconn.
  435. force_traceback = False, # include traceback in all errors
  436. basepath = '', # base URL path for cherrypy.tree
  437. ):
  438. # Save server version, just for verification during tests
  439. self.version = nilmdb.__version__
  440. self.embedded = embedded
  441. self.db = db
  442. if not getattr(db, "_thread_safe", None):
  443. raise KeyError("Database object " + str(db) + " doesn't claim "
  444. "to be thread safe. You should pass "
  445. "nilmdb.utils.serializer_proxy(NilmDB)(args) "
  446. "rather than NilmDB(args).")
  447. # Build up global server configuration
  448. cherrypy.config.update({
  449. 'server.socket_host': host,
  450. 'server.socket_port': port,
  451. 'engine.autoreload_on': False,
  452. 'server.max_request_body_size': 8*1024*1024,
  453. })
  454. if self.embedded:
  455. cherrypy.config.update({ 'environment': 'embedded' })
  456. # Build up application specific configuration
  457. app_config = {}
  458. app_config.update({
  459. 'error_page.default': self.json_error_page,
  460. })
  461. # Some default headers to just help identify that things are working
  462. app_config.update({ 'response.headers.X-Jim-Is-Awesome': 'yeah' })
  463. # Set up Cross-Origin Resource Sharing (CORS) handler so we
  464. # can correctly respond to browsers' CORS preflight requests.
  465. # This also limits verbs to GET and HEAD by default.
  466. app_config.update({ 'tools.CORS_allow.on': True,
  467. 'tools.CORS_allow.methods': ['GET', 'HEAD'] })
  468. # Configure the 'json_in' tool to also allow other content-types
  469. # (like x-www-form-urlencoded), and to treat JSON as a dict that
  470. # fills requests.param.
  471. app_config.update({ 'tools.json_in.force': False,
  472. 'tools.json_in.processor': json_to_request_params })
  473. # Send tracebacks in error responses. They're hidden by the
  474. # error_page function for client errors (code 400-499).
  475. app_config.update({ 'request.show_tracebacks' : True })
  476. self.force_traceback = force_traceback
  477. # Patch CherryPy error handler to never pad out error messages.
  478. # This isn't necessary, but then again, neither is padding the
  479. # error messages.
  480. cherrypy._cperror._ie_friendly_error_sizes = {}
  481. # Build up the application and mount it
  482. root = Root(self.db)
  483. root.stream = Stream(self.db)
  484. if stoppable:
  485. root.exit = Exiter()
  486. cherrypy.tree.apps = {}
  487. cherrypy.tree.mount(root, basepath, config = { "/" : app_config })
  488. # Shutdowns normally wait for clients to disconnect. To speed
  489. # up tests, set fast_shutdown = True
  490. if fast_shutdown:
  491. # Setting timeout to 0 triggers os._exit(70) at shutdown, grr...
  492. cherrypy.server.shutdown_timeout = 0.01
  493. else:
  494. cherrypy.server.shutdown_timeout = 5
  495. # Set up the WSGI application pointer for external programs
  496. self.wsgi_application = cherrypy.tree
  497. def json_error_page(self, status, message, traceback, version):
  498. """Return a custom error page in JSON so the client can parse it"""
  499. errordata = { "status" : status,
  500. "message" : message,
  501. "traceback" : traceback }
  502. # Don't send a traceback if the error was 400-499 (client's fault)
  503. try:
  504. code = int(status.split()[0])
  505. if not self.force_traceback:
  506. if code >= 400 and code <= 499:
  507. errordata["traceback"] = ""
  508. except Exception: # pragma: no cover
  509. pass
  510. # Override the response type, which was previously set to text/html
  511. cherrypy.serving.response.headers['Content-Type'] = (
  512. "application/json;charset=utf-8" )
  513. # Undo the HTML escaping that cherrypy's get_error_page function applies
  514. # (cherrypy issue 1135)
  515. for k, v in errordata.iteritems():
  516. v = v.replace("&lt;","<")
  517. v = v.replace("&gt;",">")
  518. v = v.replace("&amp;","&")
  519. errordata[k] = v
  520. return json.dumps(errordata, separators=(',',':'))
  521. def start(self, blocking = False, event = None):
  522. if not self.embedded: # pragma: no cover
  523. # Handle signals nicely
  524. if hasattr(cherrypy.engine, "signal_handler"):
  525. cherrypy.engine.signal_handler.subscribe()
  526. if hasattr(cherrypy.engine, "console_control_handler"):
  527. cherrypy.engine.console_control_handler.subscribe()
  528. # Cherrypy stupidly calls os._exit(70) when it can't bind the
  529. # port. At least try to print a reasonable error and continue
  530. # in this case, rather than just dying silently (as we would
  531. # otherwise do in embedded mode)
  532. real_exit = os._exit
  533. def fake_exit(code): # pragma: no cover
  534. if code == os.EX_SOFTWARE:
  535. fprintf(sys.stderr, "error: CherryPy called os._exit!\n")
  536. else:
  537. real_exit(code)
  538. os._exit = fake_exit
  539. cherrypy.engine.start()
  540. os._exit = real_exit
  541. # Signal that the engine has started successfully
  542. if event is not None:
  543. event.set()
  544. if blocking:
  545. try:
  546. cherrypy.engine.wait(cherrypy.engine.states.EXITING,
  547. interval = 0.1, channel = 'main')
  548. except (KeyboardInterrupt, IOError): # pragma: no cover
  549. cherrypy.engine.log('Keyboard Interrupt: shutting down bus')
  550. cherrypy.engine.exit()
  551. except SystemExit: # pragma: no cover
  552. cherrypy.engine.log('SystemExit raised: shutting down bus')
  553. cherrypy.engine.exit()
  554. raise
  555. def stop(self):
  556. cherrypy.engine.exit()
  557. # Use a single global nilmdb.server.NilmDB and nilmdb.server.Server
  558. # instance since the database can only be opened once. For this to
  559. # work, the web server must use only a single process and single
  560. # Python interpreter. Multiple threads are OK.
  561. _wsgi_server = None
  562. def wsgi_application(dbpath, basepath): # pragma: no cover
  563. """Return a WSGI application object with a database at the
  564. specified path.
  565. 'dbpath' is a filesystem location, e.g. /home/nilm/db
  566. 'basepath' is the URL path of the application base, which
  567. is the same as the first argument to Apache's WSGIScriptAlias
  568. directive.
  569. """
  570. def application(environ, start_response):
  571. global _wsgi_server
  572. if _wsgi_server is None:
  573. # Try to start the server
  574. try:
  575. db = nilmdb.utils.serializer_proxy(nilmdb.server.NilmDB)(dbpath)
  576. _wsgi_server = nilmdb.server.Server(
  577. db, embedded = True,
  578. basepath = basepath.rstrip('/'))
  579. except Exception:
  580. # Build an error message on failure
  581. import pprint
  582. err = sprintf("Initializing database at path '%s' failed:\n\n",
  583. dbpath)
  584. err += traceback.format_exc()
  585. try:
  586. import pwd
  587. import grp
  588. err += sprintf("\nRunning as: uid=%d (%s), gid=%d (%s) "
  589. "on host %s, pid %d\n",
  590. os.getuid(), pwd.getpwuid(os.getuid())[0],
  591. os.getgid(), grp.getgrgid(os.getgid())[0],
  592. socket.gethostname(), os.getpid())
  593. except ImportError:
  594. pass
  595. err += sprintf("\nEnvironment:\n%s\n", pprint.pformat(environ))
  596. if _wsgi_server is None:
  597. # Serve up the error with our own mini WSGI app.
  598. headers = [ ('Content-type', 'text/plain'),
  599. ('Content-length', str(len(err))) ]
  600. start_response("500 Internal Server Error", headers)
  601. return [err]
  602. # Call the normal application
  603. return _wsgi_server.wsgi_application(environ, start_response)
  604. return application