With this solution, we can catch it cleanly in the standalone nilmdb-server, and test the error paths in our normal test suite.tags/nilmdb-2.0.0
@@ -5,6 +5,7 @@ import argparse | |||||
import os | import os | ||||
import socket | import socket | ||||
import cherrypy | import cherrypy | ||||
import sys | |||||
def main(): | def main(): | ||||
"""Main entry point for the 'nilmdb-server' command line script""" | """Main entry point for the 'nilmdb-server' command line script""" | ||||
@@ -61,27 +62,30 @@ def main(): | |||||
print("----") | print("----") | ||||
# Run it | # Run it | ||||
if args.yappi: | |||||
print("Running in yappi") | |||||
try: | |||||
import yappi | |||||
yappi.start() | |||||
try: | |||||
if args.yappi: | |||||
print("Running in yappi") | |||||
try: | |||||
import yappi | |||||
yappi.start() | |||||
server.start(blocking = True) | |||||
finally: | |||||
yappi.stop() | |||||
stats = yappi.get_func_stats() | |||||
stats.sort("ttot") | |||||
stats.print_all() | |||||
from IPython import embed | |||||
embed(header = "Use the `yappi` or `stats` object to explore " | |||||
"further, quit to exit") | |||||
else: | |||||
server.start(blocking = True) | server.start(blocking = True) | ||||
finally: | |||||
yappi.stop() | |||||
stats = yappi.get_func_stats() | |||||
stats.sort("ttot") | |||||
stats.print_all() | |||||
from IPython import embed | |||||
embed(header = "Use the `yappi` or `stats` object to explore " | |||||
"further, quit to exit") | |||||
else: | |||||
server.start(blocking = True) | |||||
# Clean up | |||||
if not args.quiet: | |||||
print("Closing database") | |||||
db.close() | |||||
except nilmdb.server.serverutil.CherryPyExit as e: | |||||
print("Exiting due to CherryPy error", file=sys.stderr) | |||||
raise | |||||
finally: | |||||
if not args.quiet: | |||||
print("Closing database") | |||||
db.close() | |||||
if __name__ == "__main__": | if __name__ == "__main__": | ||||
main() | main() |
@@ -143,28 +143,38 @@ def json_error_page(status, message, traceback, version, | |||||
errordata[k] = v | errordata[k] = v | ||||
return json.dumps(errordata, separators=(',',':')) | return json.dumps(errordata, separators=(',',':')) | ||||
# Start/stop CherryPy standalone server | |||||
def cherrypy_start(blocking = False, event = False): | |||||
"""Start the CherryPy server, handling errors and signals | |||||
somewhat gracefully.""" | |||||
class CherryPyExit(SystemExit): | |||||
pass | |||||
def cherrypy_patch_exit(): | |||||
# Cherrypy stupidly calls os._exit(70) when it can't bind the port | # Cherrypy stupidly calls os._exit(70) when it can't bind the port | ||||
# and exits. Replace os._exit with a version that at least prints | |||||
# a message, since otherwise it's totally unclear what happened | |||||
# and exits. Instead of that, raise a CherryPyExit (derived from | |||||
# SystemExit). This exception may not make it back up to the caller | |||||
# due to internal thread use in the CherryPy engine, but there should | |||||
# be at least some indication that it happened. | |||||
bus = cherrypy.process.wspbus.bus | |||||
if "_patched_exit" in bus.__dict__: | |||||
return | |||||
bus._patched_exit = True | |||||
def patched_exit(orig): | def patched_exit(orig): | ||||
real_exit = os._exit | real_exit = os._exit | ||||
def fake_exit(code): # pragma: no cover (hard to invoke from tests; | |||||
# standalone server isn't used in production) | |||||
print("Cherrypy called os._exit(%d)" % code, file=sys.stderr) | |||||
real_exit(code) | |||||
def fake_exit(code): | |||||
raise CherryPyExit(code) | |||||
os._exit = fake_exit | os._exit = fake_exit | ||||
try: | try: | ||||
orig() | orig() | ||||
finally: | finally: | ||||
os._exit = real_exit | os._exit = real_exit | ||||
bus = cherrypy.process.wspbus.bus | |||||
bus.exit = functools.partial(patched_exit, bus.exit) | bus.exit = functools.partial(patched_exit, bus.exit) | ||||
# Start/stop CherryPy standalone server | |||||
def cherrypy_start(blocking = False, event = False): | |||||
"""Start the CherryPy server, handling errors and signals | |||||
somewhat gracefully.""" | |||||
cherrypy_patch_exit() | |||||
# Start the server | # Start the server | ||||
cherrypy.engine.start() | cherrypy.engine.start() | ||||
@@ -6,6 +6,8 @@ import io | |||||
import os | import os | ||||
import sys | import sys | ||||
import time | import time | ||||
import socket | |||||
import cherrypy | |||||
import nilmdb.server | import nilmdb.server | ||||
from nilmdb.utils import timer, lock | from nilmdb.utils import timer, lock | ||||
@@ -95,7 +97,7 @@ class TestMisc(object): | |||||
nilmdb.utils.diskusage.du("/dev/null/bogus") | nilmdb.utils.diskusage.du("/dev/null/bogus") | ||||
nilmdb.utils.diskusage.du("super-bogus-does-not-exist") | nilmdb.utils.diskusage.du("super-bogus-does-not-exist") | ||||
def tests_cors_allow(self): | |||||
def test_cors_allow(self): | |||||
# Just to get some test coverage; these code paths aren't actually | # Just to get some test coverage; these code paths aren't actually | ||||
# used in current code | # used in current code | ||||
cpy = nilmdb.server.serverutil.cherrypy | cpy = nilmdb.server.serverutil.cherrypy | ||||
@@ -110,3 +112,28 @@ class TestMisc(object): | |||||
with assert_raises(cpy.HTTPError): | with assert_raises(cpy.HTTPError): | ||||
nilmdb.server.serverutil.CORS_allow(methods=[]) | nilmdb.server.serverutil.CORS_allow(methods=[]) | ||||
(cpy.request, cpy.response) = (req, resp) | (cpy.request, cpy.response) = (req, resp) | ||||
def test_cherrypy_failure(self): | |||||
# Test failure of cherrypy to start up because the port is | |||||
# already in use. This also tests the functionality of | |||||
# serverutil:cherrypy_patch_exit() | |||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |||||
try: | |||||
sock.bind(("127.0.0.1", 32180)) | |||||
sock.listen(1) | |||||
except OSError: | |||||
raise AssertionError("port 32180 must be free for tests") | |||||
nilmdb.server.serverutil.cherrypy_patch_exit() | |||||
cherrypy.config.update({ | |||||
'environment': 'embedded', | |||||
'server.socket_host': '127.0.0.1', | |||||
'server.socket_port': 32180, | |||||
'engine.autoreload.on': False, | |||||
}) | |||||
with assert_raises(Exception) as e: | |||||
cherrypy.engine.start() | |||||
in_("Address already in use", str(e.exception)) | |||||
sock.close() |