blob: 001024ecb3f124c54b465b29c13f2a209f49e8fd [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Extensions for CherryPy.
This module contains patches and add-ons for the stock CherryPy distribution.
Everything in here is compatible with the CherryPy version used in the chroot,
as well as the recent stable version as used (for example) in the lab. This
premise is verified by the corresponding unit tests.
"""
from __future__ import print_function
import os
import cherrypy # pylint: disable=import-error
class PortFile(cherrypy.process.plugins.SimplePlugin):
"""CherryPy plugin for maintaining a port file via a WSPBus.
This is a hack, because we're using arbitrary bus signals (like 'start' and
'log') to trigger checking whether the server has already bound the listening
socket to a port, in which case we write it to a file. It would work as long
as the server (for example) logs the fact that it is up and serving *after*
it has bound the port, which happens to be the case. The upside is that we
don't have to use ad hoc signals, nor do we need to change the implementaiton
of various CherryPy classes (like ServerAdapter) to use such signals.
In all other respects, this plugin mirrors the behavior of the stock
cherrypy.process.plugins.PIDFile plugin. Note that it will not work correctly
in the presence of multiple server threads, nor is it meant to; it will only
write the port of the main server instance (cherrypy.server), if present.
"""
def __init__(self, bus, portfile):
super(PortFile, self).__init__(bus)
self.portfile = portfile
self.stopped = True
self.written = False
@staticmethod
def get_port_from_httpserver():
"""Pulls the actual bound port number from CherryPy's HTTP server.
This assumes that cherrypy.server is the main server instance,
cherrypy.server.httpserver the underlying HTTP server, and
cherrypy.server.httpserver.socket the socket used for serving. These appear
to be well accepted conventions throughout recent versions of CherryPy.
Returns:
The actual bound port; zero if not bound or could not be retrieved.
"""
server_socket = (getattr(cherrypy.server, 'httpserver', None) and
getattr(cherrypy.server.httpserver, 'socket', None))
bind_addr = server_socket and server_socket.getsockname()
return bind_addr[1] if (bind_addr and isinstance(bind_addr, tuple)) else 0
def _check_and_write_port(self):
"""Check if a port has been bound, and if so write it to file.
This maintains a flag to denote whether or not the server has started (to
avoid doing unnecessary work) and another flag denoting whether a port was
already written to file (so it can be removed upon 'stop').
IMPORTANT: to avoid infinite recursion, do not emit any bus event (e.g.
self.bus.log()) until after setting self.written to True!
"""
if self.stopped or self.written:
return
port = self.get_port_from_httpserver()
if not port:
return
with open(self.portfile, 'w') as f:
f.write(str(port))
self.written = True
self.bus.log('Port %r written to %r.' % (port, self.portfile))
def start(self):
self.stopped = False
self._check_and_write_port()
start.priority = 50
def log(self, _msg, _level):
self._check_and_write_port()
def stop(self):
"""Removes the port file.
IMPORTANT: to avoid re-writing the port file via other signals (e.g.
self.bus.log()) be sure to set self.stopped to True before setting
self.written to False!
"""
self.stopped = True
if self.written:
self.written = False
try:
os.remove(self.portfile)
self.bus.log('Port file removed: %r.' % self.portfile)
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
self.bus.log('Failed to remove port file: %r.' % self.portfile)