| # -*- 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) |