| # -*- coding: utf-8 -*- | |
| # Copyright 2014 The ChromiumOS Authors | |
| # 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) |