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 1 year 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 socket
import cherrypy
import sys

def main():
"""Main entry point for the 'nilmdb-server' command line script"""
@@ -61,27 +62,30 @@ def main():
print("----")

# 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)
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__":
main()

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

@@ -143,28 +143,38 @@ def json_error_page(status, message, traceback, version,
errordata[k] = v
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
# 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):
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
try:
orig()
finally:
os._exit = real_exit
bus = cherrypy.process.wspbus.bus
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
cherrypy.engine.start()



+ 28
- 1
tests/test_misc.py View File

@@ -6,6 +6,8 @@ import io
import os
import sys
import time
import socket
import cherrypy

import nilmdb.server
from nilmdb.utils import timer, lock
@@ -95,7 +97,7 @@ class TestMisc(object):
nilmdb.utils.diskusage.du("/dev/null/bogus")
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
# used in current code
cpy = nilmdb.server.serverutil.cherrypy
@@ -110,3 +112,28 @@ class TestMisc(object):
with assert_raises(cpy.HTTPError):
nilmdb.server.serverutil.CORS_allow(methods=[])
(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