~j5-dev/+junk/cherrypy3-3.2.0rc1

« back to all changes in this revision

Viewing changes to cherrypy/process/servers.py

  • Committer: steveh at sjsoft
  • Date: 2010-07-01 13:07:15 UTC
  • Revision ID: steveh@sjsoft.com-20100701130715-w56oim8346qzqlka
New upstream release

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""Adapt an HTTP server."""
 
2
 
 
3
import time
 
4
 
 
5
 
 
6
class ServerAdapter(object):
 
7
    """Adapter for an HTTP server.
 
8
    
 
9
    If you need to start more than one HTTP server (to serve on multiple
 
10
    ports, or protocols, etc.), you can manually register each one and then
 
11
    start them all with bus.start:
 
12
    
 
13
        s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
 
14
        s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
 
15
        s1.subscribe()
 
16
        s2.subscribe()
 
17
        bus.start()
 
18
    """
 
19
    
 
20
    def __init__(self, bus, httpserver=None, bind_addr=None):
 
21
        self.bus = bus
 
22
        self.httpserver = httpserver
 
23
        self.bind_addr = bind_addr
 
24
        self.interrupt = None
 
25
        self.running = False
 
26
    
 
27
    def subscribe(self):
 
28
        self.bus.subscribe('start', self.start)
 
29
        self.bus.subscribe('stop', self.stop)
 
30
    
 
31
    def unsubscribe(self):
 
32
        self.bus.unsubscribe('start', self.start)
 
33
        self.bus.unsubscribe('stop', self.stop)
 
34
    
 
35
    def start(self):
 
36
        """Start the HTTP server."""
 
37
        if self.bind_addr is None:
 
38
            on_what = "unknown interface (dynamic?)"
 
39
        elif isinstance(self.bind_addr, tuple):
 
40
            host, port = self.bind_addr
 
41
            on_what = "%s:%s" % (host, port)
 
42
        else:
 
43
            on_what = "socket file: %s" % self.bind_addr
 
44
        
 
45
        if self.running:
 
46
            self.bus.log("Already serving on %s" % on_what)
 
47
            return
 
48
        
 
49
        self.interrupt = None
 
50
        if not self.httpserver:
 
51
            raise ValueError("No HTTP server has been created.")
 
52
        
 
53
        # Start the httpserver in a new thread.
 
54
        if isinstance(self.bind_addr, tuple):
 
55
            wait_for_free_port(*self.bind_addr)
 
56
        
 
57
        import threading
 
58
        t = threading.Thread(target=self._start_http_thread)
 
59
        t.setName("HTTPServer " + t.getName())
 
60
        t.start()
 
61
        
 
62
        self.wait()
 
63
        self.running = True
 
64
        self.bus.log("Serving on %s" % on_what)
 
65
    start.priority = 75
 
66
    
 
67
    def _start_http_thread(self):
 
68
        """HTTP servers MUST be running in new threads, so that the
 
69
        main thread persists to receive KeyboardInterrupt's. If an
 
70
        exception is raised in the httpserver's thread then it's
 
71
        trapped here, and the bus (and therefore our httpserver)
 
72
        are shut down.
 
73
        """
 
74
        try:
 
75
            self.httpserver.start()
 
76
        except KeyboardInterrupt, exc:
 
77
            self.bus.log("<Ctrl-C> hit: shutting down HTTP server")
 
78
            self.interrupt = exc
 
79
            self.bus.exit()
 
80
        except SystemExit, exc:
 
81
            self.bus.log("SystemExit raised: shutting down HTTP server")
 
82
            self.interrupt = exc
 
83
            self.bus.exit()
 
84
            raise
 
85
        except:
 
86
            import sys
 
87
            self.interrupt = sys.exc_info()[1]
 
88
            self.bus.log("Error in HTTP server: shutting down",
 
89
                         traceback=True, level=40)
 
90
            self.bus.exit()
 
91
            raise
 
92
    
 
93
    def wait(self):
 
94
        """Wait until the HTTP server is ready to receive requests."""
 
95
        while not getattr(self.httpserver, "ready", False):
 
96
            if self.interrupt:
 
97
                raise self.interrupt
 
98
            time.sleep(.1)
 
99
        
 
100
        # Wait for port to be occupied
 
101
        if isinstance(self.bind_addr, tuple):
 
102
            host, port = self.bind_addr
 
103
            wait_for_occupied_port(host, port)
 
104
    
 
105
    def stop(self):
 
106
        """Stop the HTTP server."""
 
107
        if self.running:
 
108
            # stop() MUST block until the server is *truly* stopped.
 
109
            self.httpserver.stop()
 
110
            # Wait for the socket to be truly freed.
 
111
            if isinstance(self.bind_addr, tuple):
 
112
                wait_for_free_port(*self.bind_addr)
 
113
            self.running = False
 
114
            self.bus.log("HTTP Server %s shut down" % self.httpserver)
 
115
        else:
 
116
            self.bus.log("HTTP Server %s already shut down" % self.httpserver)
 
117
    stop.priority = 25
 
118
    
 
119
    def restart(self):
 
120
        """Restart the HTTP server."""
 
121
        self.stop()
 
122
        self.start()
 
123
 
 
124
 
 
125
class FlupFCGIServer(object):
 
126
    """Adapter for a flup.server.fcgi.WSGIServer."""
 
127
    
 
128
    def __init__(self, *args, **kwargs):
 
129
        if kwargs.get('bindAddress', None) is None:
 
130
            import socket
 
131
            if not hasattr(socket.socket, 'fromfd'):
 
132
                raise ValueError(
 
133
                    'Dynamic FCGI server not available on this platform. '
 
134
                    'You must use a static or external one by providing a '
 
135
                    'legal bindAddress.')
 
136
        self.args = args
 
137
        self.kwargs = kwargs
 
138
        self.ready = False
 
139
    
 
140
    def start(self):
 
141
        """Start the FCGI server."""
 
142
        # We have to instantiate the server class here because its __init__
 
143
        # starts a threadpool. If we do it too early, daemonize won't work.
 
144
        from flup.server.fcgi import WSGIServer
 
145
        self.fcgiserver = WSGIServer(*self.args, **self.kwargs)
 
146
        # TODO: report this bug upstream to flup.
 
147
        # If we don't set _oldSIGs on Windows, we get:
 
148
        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
 
149
        #   line 108, in run
 
150
        #     self._restoreSignalHandlers()
 
151
        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
 
152
        #   line 156, in _restoreSignalHandlers
 
153
        #     for signum,handler in self._oldSIGs:
 
154
        #   AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
 
155
        self.fcgiserver._installSignalHandlers = lambda: None
 
156
        self.fcgiserver._oldSIGs = []
 
157
        self.ready = True
 
158
        self.fcgiserver.run()
 
159
    
 
160
    def stop(self):
 
161
        """Stop the HTTP server."""
 
162
        # Forcibly stop the fcgi server main event loop.
 
163
        self.fcgiserver._keepGoing = False
 
164
        # Force all worker threads to die off.
 
165
        self.fcgiserver._threadPool.maxSpare = self.fcgiserver._threadPool._idleCount
 
166
        self.ready = False
 
167
 
 
168
 
 
169
class FlupSCGIServer(object):
 
170
    """Adapter for a flup.server.scgi.WSGIServer."""
 
171
    
 
172
    def __init__(self, *args, **kwargs):
 
173
        self.args = args
 
174
        self.kwargs = kwargs
 
175
        self.ready = False
 
176
    
 
177
    def start(self):
 
178
        """Start the SCGI server."""
 
179
        # We have to instantiate the server class here because its __init__
 
180
        # starts a threadpool. If we do it too early, daemonize won't work.
 
181
        from flup.server.scgi import WSGIServer
 
182
        self.scgiserver = WSGIServer(*self.args, **self.kwargs)
 
183
        # TODO: report this bug upstream to flup.
 
184
        # If we don't set _oldSIGs on Windows, we get:
 
185
        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
 
186
        #   line 108, in run
 
187
        #     self._restoreSignalHandlers()
 
188
        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
 
189
        #   line 156, in _restoreSignalHandlers
 
190
        #     for signum,handler in self._oldSIGs:
 
191
        #   AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
 
192
        self.scgiserver._installSignalHandlers = lambda: None
 
193
        self.scgiserver._oldSIGs = []
 
194
        self.ready = True
 
195
        self.scgiserver.run()
 
196
    
 
197
    def stop(self):
 
198
        """Stop the HTTP server."""
 
199
        self.ready = False
 
200
        # Forcibly stop the scgi server main event loop.
 
201
        self.scgiserver._keepGoing = False
 
202
        # Force all worker threads to die off.
 
203
        self.scgiserver._threadPool.maxSpare = 0
 
204
 
 
205
 
 
206
def client_host(server_host):
 
207
    """Return the host on which a client can connect to the given listener."""
 
208
    if server_host == '0.0.0.0':
 
209
        # 0.0.0.0 is INADDR_ANY, which should answer on localhost.
 
210
        return '127.0.0.1'
 
211
    if server_host == '::':
 
212
        # :: is IN6ADDR_ANY, which should answer on localhost.
 
213
        return '::1'
 
214
    return server_host
 
215
 
 
216
def check_port(host, port, timeout=1.0):
 
217
    """Raise an error if the given port is not free on the given host."""
 
218
    if not host:
 
219
        raise ValueError("Host values of '' or None are not allowed.")
 
220
    host = client_host(host)
 
221
    port = int(port)
 
222
    
 
223
    import socket
 
224
    
 
225
    # AF_INET or AF_INET6 socket
 
226
    # Get the correct address family for our host (allows IPv6 addresses)
 
227
    try:
 
228
        info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
 
229
                                  socket.SOCK_STREAM)
 
230
    except socket.gaierror:
 
231
        if ':' in host:
 
232
            info = [(socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0))]
 
233
        else:
 
234
            info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (host, port))]
 
235
    
 
236
    for res in info:
 
237
        af, socktype, proto, canonname, sa = res
 
238
        s = None
 
239
        try:
 
240
            s = socket.socket(af, socktype, proto)
 
241
            # See http://groups.google.com/group/cherrypy-users/
 
242
            #        browse_frm/thread/bbfe5eb39c904fe0
 
243
            s.settimeout(timeout)
 
244
            s.connect((host, port))
 
245
            s.close()
 
246
            raise IOError("Port %s is in use on %s; perhaps the previous "
 
247
                          "httpserver did not shut down properly." %
 
248
                          (repr(port), repr(host)))
 
249
        except socket.error:
 
250
            if s:
 
251
                s.close()
 
252
 
 
253
def wait_for_free_port(host, port):
 
254
    """Wait for the specified port to become free (drop requests)."""
 
255
    if not host:
 
256
        raise ValueError("Host values of '' or None are not allowed.")
 
257
    
 
258
    for trial in range(50):
 
259
        try:
 
260
            # we are expecting a free port, so reduce the timeout
 
261
            check_port(host, port, timeout=0.1)
 
262
        except IOError:
 
263
            # Give the old server thread time to free the port.
 
264
            time.sleep(0.1)
 
265
        else:
 
266
            return
 
267
    
 
268
    raise IOError("Port %r not free on %r" % (port, host))
 
269
 
 
270
def wait_for_occupied_port(host, port):
 
271
    """Wait for the specified port to become active (receive requests)."""
 
272
    if not host:
 
273
        raise ValueError("Host values of '' or None are not allowed.")
 
274
    
 
275
    for trial in range(50):
 
276
        try:
 
277
            check_port(host, port)
 
278
        except IOError:
 
279
            return
 
280
        else:
 
281
            time.sleep(.1)
 
282
    
 
283
    raise IOError("Port %r not bound on %r" % (port, host))