blob: 52e168d3f69971fd25002ea842caf3df024a79b6 [file] [log] [blame]
#!/usr/bin/env python2
# -*- 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.
"""
import cherrypy
import os
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, "wb") 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:
self.bus.log('Failed to remove port file: %r.' % self.portfile)
class ZeroPortPatcher(object):
"""Patches a CherryPy module to support binding to any available port."""
# The cached value of the actual port bound by the HTTP server.
cached_port = 0
@classmethod
def _WrapWaitForPort(cls, cherrypy_module, func_name, use_cached):
"""Ensures that a port is not zero before calling a wait-for-port function.
This wraps stock CherryPy module-level functions that wait for a port to be
free/occupied with a conditional that ensures the port argument isn't zero.
Prior to that, the wrapper attempts to pull the actual bound port number
from CherryPy's underlying HTTP server, if present. In this case, it'll
also cache the pulled out value, so it can be used in subsequent calls; one
such scenario is checking when a previously bound (actual) port has been
released after server shutdown. This makes those functions do their
intended job when the server is configured to bind to an arbitrary
available port (server.socket_port is zero), a necessary feature.
Raises:
AttributeError: if func_name is not an attribute of cherrypy_module.
"""
module = cherrypy_module.process.servers
func = getattr(module, func_name) # Will fail if not present.
def wrapped_func(host, port):
if not port:
actual_port = PortFile.get_port_from_httpserver()
if use_cached:
port = cls.cached_port
using = 'cached'
else:
port = actual_port
using = 'actual'
if port:
cherrypy_module.engine.log('(%s) Waiting for %s port %s.' %
(func_name, using, port))
else:
cherrypy_module.engine.log('(%s) No %s port to wait for.' %
(func_name, using))
cls.cached_port = port
if port:
return func(host, port)
setattr(module, func_name, wrapped_func)
@classmethod
def DoPatch(cls, cherrypy_module):
"""Patches a given CherryPy module.
Raises:
AttributeError: when fails to patch CherryPy.
"""
cls._WrapWaitForPort(cherrypy_module, 'wait_for_free_port', True)
cls._WrapWaitForPort(cherrypy_module, 'wait_for_occupied_port', False)