Browse Source

Clean up how we handle cherrypy's calls of os._exit(70)

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
Jim Paris 4 years ago
parent
commit
ea67e45be9
3 changed files with 73 additions and 32 deletions
  1. +24
    -20
      nilmdb/scripts/nilmdb_server.py
  2. +21
    -11
      nilmdb/server/serverutil.py
  3. +28
    -1
      tests/test_misc.py

+ 24
- 20
nilmdb/scripts/nilmdb_server.py View File

@@ -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()

+ 21
- 11
nilmdb/server/serverutil.py View File

@@ -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()




+ 28
- 1
tests/test_misc.py View File

@@ -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()

Loading…
Cancel
Save