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