1r"""
2Starting in CherryPy 3.1, cherrypy.server is implemented as an
3:ref:`Engine Plugin<plugins>`. It's an instance of
4:class:`cherrypy._cpserver.Server`, which is a subclass of
5:class:`cherrypy.process.servers.ServerAdapter`. The ``ServerAdapter`` class
6is designed to control other servers, as well.
7
8Multiple servers/ports
9======================
10
11If you need to start more than one HTTP server (to serve on multiple ports, or
12protocols, etc.), you can manually register each one and then start them all
13with engine.start::
14
15    s1 = ServerAdapter(
16        cherrypy.engine,
17        MyWSGIServer(host='0.0.0.0', port=80)
18    )
19    s2 = ServerAdapter(
20        cherrypy.engine,
21        another.HTTPServer(host='127.0.0.1', SSL=True)
22    )
23    s1.subscribe()
24    s2.subscribe()
25    cherrypy.engine.start()
26
27.. index:: SCGI
28
29FastCGI/SCGI
30============
31
32There are also Flup\ **F**\ CGIServer and Flup\ **S**\ CGIServer classes in
33:mod:`cherrypy.process.servers`. To start an fcgi server, for example,
34wrap an instance of it in a ServerAdapter::
35
36    addr = ('0.0.0.0', 4000)
37    f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=addr)
38    s = servers.ServerAdapter(cherrypy.engine, httpserver=f, bind_addr=addr)
39    s.subscribe()
40
41The :doc:`cherryd</deployguide/cherryd>` startup script will do the above for
42you via its `-f` flag.
43Note that you need to download and install `flup <http://trac.saddi.com/flup>`_
44yourself, whether you use ``cherryd`` or not.
45
46.. _fastcgi:
47.. index:: FastCGI
48
49FastCGI
50-------
51
52A very simple setup lets your cherry run with FastCGI.
53You just need the flup library,
54plus a running Apache server (with ``mod_fastcgi``) or lighttpd server.
55
56CherryPy code
57^^^^^^^^^^^^^
58
59hello.py::
60
61    #!/usr/bin/python
62    import cherrypy
63
64    class HelloWorld:
65        '''Sample request handler class.'''
66        @cherrypy.expose
67        def index(self):
68            return "Hello world!"
69
70    cherrypy.tree.mount(HelloWorld())
71    # CherryPy autoreload must be disabled for the flup server to work
72    cherrypy.config.update({'engine.autoreload.on':False})
73
74Then run :doc:`/deployguide/cherryd` with the '-f' arg::
75
76    cherryd -c <myconfig> -d -f -i hello.py
77
78Apache
79^^^^^^
80
81At the top level in httpd.conf::
82
83    FastCgiIpcDir /tmp
84    FastCgiServer /path/to/cherry.fcgi -idle-timeout 120 -processes 4
85
86And inside the relevant VirtualHost section::
87
88    # FastCGI config
89    AddHandler fastcgi-script .fcgi
90    ScriptAliasMatch (.*$) /path/to/cherry.fcgi$1
91
92Lighttpd
93^^^^^^^^
94
95For `Lighttpd <http://www.lighttpd.net/>`_ you can follow these
96instructions. Within ``lighttpd.conf`` make sure ``mod_fastcgi`` is
97active within ``server.modules``. Then, within your ``$HTTP["host"]``
98directive, configure your fastcgi script like the following::
99
100    $HTTP["url"] =~ "" {
101      fastcgi.server = (
102        "/" => (
103          "script.fcgi" => (
104            "bin-path" => "/path/to/your/script.fcgi",
105            "socket"          => "/tmp/script.sock",
106            "check-local"     => "disable",
107            "disable-time"    => 1,
108            "min-procs"       => 1,
109            "max-procs"       => 1, # adjust as needed
110          ),
111        ),
112      )
113    } # end of $HTTP["url"] =~ "^/"
114
115Please see `Lighttpd FastCGI Docs
116<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for
117an explanation of the possible configuration options.
118"""
119
120import os
121import sys
122import time
123import warnings
124import contextlib
125
126import portend
127
128
129class Timeouts:
130    occupied = 5
131    free = 1
132
133
134class ServerAdapter(object):
135
136    """Adapter for an HTTP server.
137
138    If you need to start more than one HTTP server (to serve on multiple
139    ports, or protocols, etc.), you can manually register each one and then
140    start them all with bus.start::
141
142        s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
143        s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
144        s1.subscribe()
145        s2.subscribe()
146        bus.start()
147    """
148
149    def __init__(self, bus, httpserver=None, bind_addr=None):
150        self.bus = bus
151        self.httpserver = httpserver
152        self.bind_addr = bind_addr
153        self.interrupt = None
154        self.running = False
155
156    def subscribe(self):
157        self.bus.subscribe('start', self.start)
158        self.bus.subscribe('stop', self.stop)
159
160    def unsubscribe(self):
161        self.bus.unsubscribe('start', self.start)
162        self.bus.unsubscribe('stop', self.stop)
163
164    def start(self):
165        """Start the HTTP server."""
166        if self.running:
167            self.bus.log('Already serving on %s' % self.description)
168            return
169
170        self.interrupt = None
171        if not self.httpserver:
172            raise ValueError('No HTTP server has been created.')
173
174        if not os.environ.get('LISTEN_PID', None):
175            # Start the httpserver in a new thread.
176            if isinstance(self.bind_addr, tuple):
177                portend.free(*self.bind_addr, timeout=Timeouts.free)
178
179        import threading
180        t = threading.Thread(target=self._start_http_thread)
181        t.name = 'HTTPServer ' + t.name
182        t.start()
183
184        self.wait()
185        self.running = True
186        self.bus.log('Serving on %s' % self.description)
187    start.priority = 75
188
189    @property
190    def description(self):
191        """
192        A description about where this server is bound.
193        """
194        if self.bind_addr is None:
195            on_what = 'unknown interface (dynamic?)'
196        elif isinstance(self.bind_addr, tuple):
197            on_what = self._get_base()
198        else:
199            on_what = 'socket file: %s' % self.bind_addr
200        return on_what
201
202    def _get_base(self):
203        if not self.httpserver:
204            return ''
205        host, port = self.bound_addr
206        if getattr(self.httpserver, 'ssl_adapter', None):
207            scheme = 'https'
208            if port != 443:
209                host += ':%s' % port
210        else:
211            scheme = 'http'
212            if port != 80:
213                host += ':%s' % port
214
215        return '%s://%s' % (scheme, host)
216
217    def _start_http_thread(self):
218        """HTTP servers MUST be running in new threads, so that the
219        main thread persists to receive KeyboardInterrupt's. If an
220        exception is raised in the httpserver's thread then it's
221        trapped here, and the bus (and therefore our httpserver)
222        are shut down.
223        """
224        try:
225            self.httpserver.start()
226        except KeyboardInterrupt:
227            self.bus.log('<Ctrl-C> hit: shutting down HTTP server')
228            self.interrupt = sys.exc_info()[1]
229            self.bus.exit()
230        except SystemExit:
231            self.bus.log('SystemExit raised: shutting down HTTP server')
232            self.interrupt = sys.exc_info()[1]
233            self.bus.exit()
234            raise
235        except Exception:
236            self.interrupt = sys.exc_info()[1]
237            self.bus.log('Error in HTTP server: shutting down',
238                         traceback=True, level=40)
239            self.bus.exit()
240            raise
241
242    def wait(self):
243        """Wait until the HTTP server is ready to receive requests."""
244        while not getattr(self.httpserver, 'ready', False):
245            if self.interrupt:
246                raise self.interrupt
247            time.sleep(.1)
248
249        # bypass check when LISTEN_PID is set
250        if os.environ.get('LISTEN_PID', None):
251            return
252
253        # bypass check when running via socket-activation
254        # (for socket-activation the port will be managed by systemd)
255        if not isinstance(self.bind_addr, tuple):
256            return
257
258        # wait for port to be occupied
259        with _safe_wait(*self.bound_addr):
260            portend.occupied(*self.bound_addr, timeout=Timeouts.occupied)
261
262    @property
263    def bound_addr(self):
264        """
265        The bind address, or if it's an ephemeral port and the
266        socket has been bound, return the actual port bound.
267        """
268        host, port = self.bind_addr
269        if port == 0 and self.httpserver.socket:
270            # Bound to ephemeral port. Get the actual port allocated.
271            port = self.httpserver.socket.getsockname()[1]
272        return host, port
273
274    def stop(self):
275        """Stop the HTTP server."""
276        if self.running:
277            # stop() MUST block until the server is *truly* stopped.
278            self.httpserver.stop()
279            # Wait for the socket to be truly freed.
280            if isinstance(self.bind_addr, tuple):
281                portend.free(*self.bound_addr, timeout=Timeouts.free)
282            self.running = False
283            self.bus.log('HTTP Server %s shut down' % self.httpserver)
284        else:
285            self.bus.log('HTTP Server %s already shut down' % self.httpserver)
286    stop.priority = 25
287
288    def restart(self):
289        """Restart the HTTP server."""
290        self.stop()
291        self.start()
292
293
294class FlupCGIServer(object):
295
296    """Adapter for a flup.server.cgi.WSGIServer."""
297
298    def __init__(self, *args, **kwargs):
299        self.args = args
300        self.kwargs = kwargs
301        self.ready = False
302
303    def start(self):
304        """Start the CGI server."""
305        # We have to instantiate the server class here because its __init__
306        # starts a threadpool. If we do it too early, daemonize won't work.
307        from flup.server.cgi import WSGIServer
308
309        self.cgiserver = WSGIServer(*self.args, **self.kwargs)
310        self.ready = True
311        self.cgiserver.run()
312
313    def stop(self):
314        """Stop the HTTP server."""
315        self.ready = False
316
317
318class FlupFCGIServer(object):
319
320    """Adapter for a flup.server.fcgi.WSGIServer."""
321
322    def __init__(self, *args, **kwargs):
323        if kwargs.get('bindAddress', None) is None:
324            import socket
325            if not hasattr(socket, 'fromfd'):
326                raise ValueError(
327                    'Dynamic FCGI server not available on this platform. '
328                    'You must use a static or external one by providing a '
329                    'legal bindAddress.')
330        self.args = args
331        self.kwargs = kwargs
332        self.ready = False
333
334    def start(self):
335        """Start the FCGI server."""
336        # We have to instantiate the server class here because its __init__
337        # starts a threadpool. If we do it too early, daemonize won't work.
338        from flup.server.fcgi import WSGIServer
339        self.fcgiserver = WSGIServer(*self.args, **self.kwargs)
340        # TODO: report this bug upstream to flup.
341        # If we don't set _oldSIGs on Windows, we get:
342        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
343        #   line 108, in run
344        #     self._restoreSignalHandlers()
345        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
346        #   line 156, in _restoreSignalHandlers
347        #     for signum,handler in self._oldSIGs:
348        #   AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
349        self.fcgiserver._installSignalHandlers = lambda: None
350        self.fcgiserver._oldSIGs = []
351        self.ready = True
352        self.fcgiserver.run()
353
354    def stop(self):
355        """Stop the HTTP server."""
356        # Forcibly stop the fcgi server main event loop.
357        self.fcgiserver._keepGoing = False
358        # Force all worker threads to die off.
359        self.fcgiserver._threadPool.maxSpare = (
360            self.fcgiserver._threadPool._idleCount)
361        self.ready = False
362
363
364class FlupSCGIServer(object):
365
366    """Adapter for a flup.server.scgi.WSGIServer."""
367
368    def __init__(self, *args, **kwargs):
369        self.args = args
370        self.kwargs = kwargs
371        self.ready = False
372
373    def start(self):
374        """Start the SCGI server."""
375        # We have to instantiate the server class here because its __init__
376        # starts a threadpool. If we do it too early, daemonize won't work.
377        from flup.server.scgi import WSGIServer
378        self.scgiserver = WSGIServer(*self.args, **self.kwargs)
379        # TODO: report this bug upstream to flup.
380        # If we don't set _oldSIGs on Windows, we get:
381        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
382        #   line 108, in run
383        #     self._restoreSignalHandlers()
384        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
385        #   line 156, in _restoreSignalHandlers
386        #     for signum,handler in self._oldSIGs:
387        #   AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
388        self.scgiserver._installSignalHandlers = lambda: None
389        self.scgiserver._oldSIGs = []
390        self.ready = True
391        self.scgiserver.run()
392
393    def stop(self):
394        """Stop the HTTP server."""
395        self.ready = False
396        # Forcibly stop the scgi server main event loop.
397        self.scgiserver._keepGoing = False
398        # Force all worker threads to die off.
399        self.scgiserver._threadPool.maxSpare = 0
400
401
402@contextlib.contextmanager
403def _safe_wait(host, port):
404    """
405    On systems where a loopback interface is not available and the
406    server is bound to all interfaces, it's difficult to determine
407    whether the server is in fact occupying the port. In this case,
408    just issue a warning and move on. See issue #1100.
409    """
410    try:
411        yield
412    except portend.Timeout:
413        if host == portend.client_host(host):
414            raise
415        msg = 'Unable to verify that the server is bound on %r' % port
416        warnings.warn(msg)
417