blob: e819453e5890b4deb067571448dd309b4e6c4114 [file] [log] [blame]
Gilad Arnold11fbef42014-02-10 19:04:131#!/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
9This module contains patches and add-ons for the stock CherryPy distribution.
10Everything in here is compatible with the CherryPy version used in the chroot,
11as well as the recent stable version as used (for example) in the lab. This
12premise is verified by the corresponding unit tests.
13"""
14
15import cherrypy
16import os
17
18
19class 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
106class 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)