blob: 33d7d6ec5dccabe58a5f0309c31e587da9bf92b9 [file] [log] [blame]
Mike Frysingera7f08bc2019-08-27 15:16:33 -04001# -*- coding: utf-8 -*-
Gilad Arnold11fbef42014-02-10 11:04:13 -08002# 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
8This module contains patches and add-ons for the stock CherryPy distribution.
9Everything in here is compatible with the CherryPy version used in the chroot,
10as well as the recent stable version as used (for example) in the lab. This
11premise is verified by the corresponding unit tests.
12"""
13
Amin Hassanic5af4262019-11-13 13:37:20 -080014from __future__ import print_function
15
Gilad Arnold11fbef42014-02-10 11:04:13 -080016import os
17
Amin Hassanic5af4262019-11-13 13:37:20 -080018import cherrypy # pylint: disable=import-error
19
Gilad Arnold11fbef42014-02-10 11:04:13 -080020
21class 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 Hassani853ea912019-12-06 16:18:16 -080076 with open(self.portfile, 'w') as f:
Gilad Arnold11fbef42014-02-10 11:04:13 -080077 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 Hassani853ea912019-12-06 16:18:16 -0800104 except Exception:
Gilad Arnold11fbef42014-02-10 11:04:13 -0800105 self.bus.log('Failed to remove port file: %r.' % self.portfile)
Amin Hassanic5af4262019-11-13 13:37:20 -0800106
107
108class 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)