Gilad Arnold | 11fbef4 | 2014-02-10 19:04:13 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | |
| 3 | # Copyright (c) 2014 The Chromium OS Authors. All rights reserved. |
| 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | |
| 7 | """Extensions for CherryPy. |
| 8 | |
| 9 | This module contains patches and add-ons for the stock CherryPy distribution. |
| 10 | Everything in here is compatible with the CherryPy version used in the chroot, |
| 11 | as well as the recent stable version as used (for example) in the lab. This |
| 12 | premise is verified by the corresponding unit tests. |
| 13 | """ |
| 14 | |
| 15 | import cherrypy |
| 16 | import os |
| 17 | |
| 18 | |
| 19 | class PortFile(cherrypy.process.plugins.SimplePlugin): |
| 20 | """CherryPy plugin for maintaining a port file via a WSPBus. |
| 21 | |
| 22 | This is a hack, because we're using arbitrary bus signals (like 'start' and |
| 23 | 'log') to trigger checking whether the server has already bound the listening |
| 24 | socket to a port, in which case we write it to a file. It would work as long |
| 25 | as the server (for example) logs the fact that it is up and serving *after* |
| 26 | it has bound the port, which happens to be the case. The upside is that we |
| 27 | don't have to use ad hoc signals, nor do we need to change the implementaiton |
| 28 | of various CherryPy classes (like ServerAdapter) to use such signals. |
| 29 | |
| 30 | In all other respects, this plugin mirrors the behavior of the stock |
| 31 | cherrypy.process.plugins.PIDFile plugin. Note that it will not work correctly |
| 32 | in the presence of multiple server threads, nor is it meant to; it will only |
| 33 | write the port of the main server instance (cherrypy.server), if present. |
| 34 | """ |
| 35 | |
| 36 | def __init__(self, bus, portfile): |
| 37 | super(PortFile, self).__init__(bus) |
| 38 | self.portfile = portfile |
| 39 | self.stopped = True |
| 40 | self.written = False |
| 41 | |
| 42 | @staticmethod |
| 43 | def get_port_from_httpserver(): |
| 44 | """Pulls the actual bound port number from CherryPy's HTTP server. |
| 45 | |
| 46 | This assumes that cherrypy.server is the main server instance, |
| 47 | cherrypy.server.httpserver the underlying HTTP server, and |
| 48 | cherrypy.server.httpserver.socket the socket used for serving. These appear |
| 49 | to be well accepted conventions throughout recent versions of CherryPy. |
| 50 | |
| 51 | Returns: |
| 52 | The actual bound port; zero if not bound or could not be retrieved. |
| 53 | """ |
| 54 | server_socket = (getattr(cherrypy.server, 'httpserver', None) and |
| 55 | getattr(cherrypy.server.httpserver, 'socket', None)) |
| 56 | bind_addr = server_socket and server_socket.getsockname() |
| 57 | return bind_addr[1] if (bind_addr and isinstance(bind_addr, tuple)) else 0 |
| 58 | |
| 59 | def _check_and_write_port(self): |
| 60 | """Check if a port has been bound, and if so write it to file. |
| 61 | |
| 62 | This maintains a flag to denote whether or not the server has started (to |
| 63 | avoid doing unnecessary work) and another flag denoting whether a port was |
| 64 | already written to file (so it can be removed upon 'stop'). |
| 65 | |
| 66 | IMPORTANT: to avoid infinite recursion, do not emit any bus event (e.g. |
| 67 | self.bus.log()) until after setting self.written to True! |
| 68 | """ |
| 69 | if self.stopped or self.written: |
| 70 | return |
| 71 | port = self.get_port_from_httpserver() |
| 72 | if not port: |
| 73 | return |
| 74 | with open(self.portfile, "wb") as f: |
| 75 | f.write(str(port)) |
| 76 | self.written = True |
| 77 | self.bus.log('Port %r written to %r.' % (port, self.portfile)) |
| 78 | |
| 79 | def start(self): |
| 80 | self.stopped = False |
| 81 | self._check_and_write_port() |
| 82 | start.priority = 50 |
| 83 | |
| 84 | def log(self, _msg, _level): |
| 85 | self._check_and_write_port() |
| 86 | |
| 87 | def stop(self): |
| 88 | """Removes the port file. |
| 89 | |
| 90 | IMPORTANT: to avoid re-writing the port file via other signals (e.g. |
| 91 | self.bus.log()) be sure to set self.stopped to True before setting |
| 92 | self.written to False! |
| 93 | """ |
| 94 | self.stopped = True |
| 95 | if self.written: |
| 96 | self.written = False |
| 97 | try: |
| 98 | os.remove(self.portfile) |
| 99 | self.bus.log('Port file removed: %r.' % self.portfile) |
| 100 | except (KeyboardInterrupt, SystemExit): |
| 101 | raise |
| 102 | except: |
| 103 | self.bus.log('Failed to remove port file: %r.' % self.portfile) |
| 104 | |
| 105 | |
| 106 | class ZeroPortPatcher(object): |
| 107 | """Patches a CherryPy module to support binding to any available port.""" |
| 108 | |
| 109 | # The cached value of the actual port bound by the HTTP server. |
| 110 | cached_port = 0 |
| 111 | |
| 112 | @classmethod |
| 113 | def _WrapWaitForPort(cls, cherrypy_module, func_name, use_cached): |
| 114 | """Ensures that a port is not zero before calling a wait-for-port function. |
| 115 | |
| 116 | This wraps stock CherryPy module-level functions that wait for a port to be |
| 117 | free/occupied with a conditional that ensures the port argument isn't zero. |
| 118 | Prior to that, the wrapper attempts to pull the actual bound port number |
| 119 | from CherryPy's underlying HTTP server, if present. In this case, it'll |
| 120 | also cache the pulled out value, so it can be used in subsequent calls; one |
| 121 | such scenario is checking when a previously bound (actual) port has been |
| 122 | released after server shutdown. This makes those functions do their |
| 123 | intended job when the server is configured to bind to an arbitrary |
| 124 | available port (server.socket_port is zero), a necessary feature. |
| 125 | |
| 126 | Raises: |
| 127 | AttributeError: if func_name is not an attribute of cherrypy_module. |
| 128 | """ |
| 129 | module = cherrypy_module.process.servers |
| 130 | func = getattr(module, func_name) # Will fail if not present. |
| 131 | |
| 132 | def wrapped_func(host, port): |
| 133 | if not port: |
| 134 | actual_port = PortFile.get_port_from_httpserver() |
| 135 | if use_cached: |
| 136 | port = cls.cached_port |
| 137 | using = 'cached' |
| 138 | else: |
| 139 | port = actual_port |
| 140 | using = 'actual' |
| 141 | |
| 142 | if port: |
| 143 | cherrypy_module.engine.log('(%s) Waiting for %s port %s.' % |
| 144 | (func_name, using, port)) |
| 145 | else: |
| 146 | cherrypy_module.engine.log('(%s) No %s port to wait for.' % |
| 147 | (func_name, using)) |
| 148 | |
| 149 | cls.cached_port = port |
| 150 | |
| 151 | if port: |
| 152 | return func(host, port) |
| 153 | |
| 154 | setattr(module, func_name, wrapped_func) |
| 155 | |
| 156 | @classmethod |
| 157 | def DoPatch(cls, cherrypy_module): |
| 158 | """Patches a given CherryPy module. |
| 159 | |
| 160 | Raises: |
| 161 | AttributeError: when fails to patch CherryPy. |
| 162 | """ |
| 163 | cls._WrapWaitForPort(cherrypy_module, 'wait_for_free_port', True) |
| 164 | cls._WrapWaitForPort(cherrypy_module, 'wait_for_occupied_port', False) |