changeset 88:7d910a696fb2

Merge Tommi Virtanen's "single server" (sequential server) patch.
author Allan Saddi <allan@saddi.com>
date Mon, 02 Feb 2009 23:34:31 -0800
parents 68112f6a2bb9
children c3f6c5101e4d
files ChangeLog flup/server/fcgi_single.py flup/server/singleserver.py
diffstat 3 files changed, 328 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
     1.1 --- a/ChangeLog	Wed Dec 03 10:43:53 2008 -0800
     1.2 +++ b/ChangeLog	Mon Feb 02 23:34:31 2009 -0800
     1.3 @@ -1,3 +1,8 @@
     1.4 +2009-02-02  Allan Saddi  <allan@saddi.com>
     1.5 +
     1.6 +	* Merge Tommi Virtanen's "single server" (sequential server)
     1.7 +	  patch.
     1.8 +
     1.9  2008-12-03  Allan Saddi  <allan@saddi.com>
    1.10  
    1.11  	* Update ez_setup.py.
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/flup/server/fcgi_single.py	Mon Feb 02 23:34:31 2009 -0800
     2.3 @@ -0,0 +1,157 @@
     2.4 +# Copyright (c) 2005, 2006 Allan Saddi <allan@saddi.com>
     2.5 +# All rights reserved.
     2.6 +#
     2.7 +# Redistribution and use in source and binary forms, with or without
     2.8 +# modification, are permitted provided that the following conditions
     2.9 +# are met:
    2.10 +# 1. Redistributions of source code must retain the above copyright
    2.11 +#    notice, this list of conditions and the following disclaimer.
    2.12 +# 2. Redistributions in binary form must reproduce the above copyright
    2.13 +#    notice, this list of conditions and the following disclaimer in the
    2.14 +#    documentation and/or other materials provided with the distribution.
    2.15 +#
    2.16 +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
    2.17 +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    2.18 +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    2.19 +# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
    2.20 +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    2.21 +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
    2.22 +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
    2.23 +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
    2.24 +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
    2.25 +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
    2.26 +# SUCH DAMAGE.
    2.27 +#
    2.28 +# $Id$
    2.29 +
    2.30 +"""
    2.31 +fcgi - a FastCGI/WSGI gateway.
    2.32 +
    2.33 +For more information about FastCGI, see <http://www.fastcgi.com/>.
    2.34 +
    2.35 +For more information about the Web Server Gateway Interface, see
    2.36 +<http://www.python.org/peps/pep-0333.html>.
    2.37 +
    2.38 +Example usage:
    2.39 +
    2.40 +  #!/usr/bin/env python
    2.41 +  from myapplication import app # Assume app is your WSGI application object
    2.42 +  from fcgi import WSGIServer
    2.43 +  WSGIServer(app).run()
    2.44 +
    2.45 +See the documentation for WSGIServer for more information.
    2.46 +
    2.47 +On most platforms, fcgi will fallback to regular CGI behavior if run in a
    2.48 +non-FastCGI context. If you want to force CGI behavior, set the environment
    2.49 +variable FCGI_FORCE_CGI to "Y" or "y".
    2.50 +"""
    2.51 +
    2.52 +__author__ = 'Allan Saddi <allan@saddi.com>'
    2.53 +__version__ = '$Revision$'
    2.54 +
    2.55 +import os
    2.56 +
    2.57 +from flup.server.fcgi_base import BaseFCGIServer, FCGI_RESPONDER, \
    2.58 +     FCGI_MAX_CONNS, FCGI_MAX_REQS, FCGI_MPXS_CONNS
    2.59 +from flup.server.singleserver import SingleServer
    2.60 +
    2.61 +__all__ = ['WSGIServer']
    2.62 +
    2.63 +class WSGIServer(BaseFCGIServer, SingleServer):
    2.64 +    """
    2.65 +    FastCGI server that supports the Web Server Gateway Interface. See
    2.66 +    <http://www.python.org/peps/pep-0333.html>.
    2.67 +    """
    2.68 +    def __init__(self, application, environ=None,
    2.69 +                 bindAddress=None, umask=None, multiplexed=False,
    2.70 +                 debug=True, roles=(FCGI_RESPONDER,), **kw):
    2.71 +        """
    2.72 +        environ, if present, must be a dictionary-like object. Its
    2.73 +        contents will be copied into application's environ. Useful
    2.74 +        for passing application-specific variables.
    2.75 +
    2.76 +        bindAddress, if present, must either be a string or a 2-tuple. If
    2.77 +        present, run() will open its own listening socket. You would use
    2.78 +        this if you wanted to run your application as an 'external' FastCGI
    2.79 +        app. (i.e. the webserver would no longer be responsible for starting
    2.80 +        your app) If a string, it will be interpreted as a filename and a UNIX
    2.81 +        socket will be opened. If a tuple, the first element, a string,
    2.82 +        is the interface name/IP to bind to, and the second element (an int)
    2.83 +        is the port number.
    2.84 +        """
    2.85 +        BaseFCGIServer.__init__(self, application,
    2.86 +                                environ=environ,
    2.87 +                                multithreaded=False,
    2.88 +                                multiprocess=False,
    2.89 +                                bindAddress=bindAddress,
    2.90 +                                umask=umask,
    2.91 +                                multiplexed=multiplexed,
    2.92 +                                debug=debug,
    2.93 +                                roles=roles)
    2.94 +        for key in ('jobClass', 'jobArgs'):
    2.95 +            if kw.has_key(key):
    2.96 +                del kw[key]
    2.97 +        SingleServer.__init__(self, jobClass=self._connectionClass,
    2.98 +                              jobArgs=(self,), **kw)
    2.99 +        self.capability = {
   2.100 +            FCGI_MAX_CONNS: 1,
   2.101 +            FCGI_MAX_REQS: 1,
   2.102 +            FCGI_MPXS_CONNS: 0
   2.103 +            }
   2.104 +
   2.105 +    def _isClientAllowed(self, addr):
   2.106 +        return self._web_server_addrs is None or \
   2.107 +               (len(addr) == 2 and addr[0] in self._web_server_addrs)
   2.108 +
   2.109 +    def run(self):
   2.110 +        """
   2.111 +        The main loop. Exits on SIGHUP, SIGINT, SIGTERM. Returns True if
   2.112 +        SIGHUP was received, False otherwise.
   2.113 +        """
   2.114 +        self._web_server_addrs = os.environ.get('FCGI_WEB_SERVER_ADDRS')
   2.115 +        if self._web_server_addrs is not None:
   2.116 +            self._web_server_addrs = map(lambda x: x.strip(),
   2.117 +                                         self._web_server_addrs.split(','))
   2.118 +
   2.119 +        sock = self._setupSocket()
   2.120 +
   2.121 +        ret = SingleServer.run(self, sock)
   2.122 +
   2.123 +        self._cleanupSocket(sock)
   2.124 +
   2.125 +        return ret
   2.126 +
   2.127 +def factory(global_conf, host=None, port=None, **local):
   2.128 +    import paste_factory
   2.129 +    return paste_factory.helper(WSGIServer, global_conf, host, port, **local)
   2.130 +
   2.131 +if __name__ == '__main__':
   2.132 +    def test_app(environ, start_response):
   2.133 +        """Probably not the most efficient example."""
   2.134 +        import cgi
   2.135 +        start_response('200 OK', [('Content-Type', 'text/html')])
   2.136 +        yield '<html><head><title>Hello World!</title></head>\n' \
   2.137 +              '<body>\n' \
   2.138 +              '<p>Hello World!</p>\n' \
   2.139 +              '<table border="1">'
   2.140 +        names = environ.keys()
   2.141 +        names.sort()
   2.142 +        for name in names:
   2.143 +            yield '<tr><td>%s</td><td>%s</td></tr>\n' % (
   2.144 +                name, cgi.escape(`environ[name]`))
   2.145 +
   2.146 +        form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ,
   2.147 +                                keep_blank_values=1)
   2.148 +        if form.list:
   2.149 +            yield '<tr><th colspan="2">Form data</th></tr>'
   2.150 +
   2.151 +        for field in form.list:
   2.152 +            yield '<tr><td>%s</td><td>%s</td></tr>\n' % (
   2.153 +                field.name, field.value)
   2.154 +
   2.155 +        yield '</table>\n' \
   2.156 +              '</body></html>\n'
   2.157 +
   2.158 +    from wsgiref import validate
   2.159 +    test_app = validate.validator(test_app)
   2.160 +    WSGIServer(test_app).run()
     3.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     3.2 +++ b/flup/server/singleserver.py	Mon Feb 02 23:34:31 2009 -0800
     3.3 @@ -0,0 +1,166 @@
     3.4 +# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
     3.5 +# All rights reserved.
     3.6 +#
     3.7 +# Redistribution and use in source and binary forms, with or without
     3.8 +# modification, are permitted provided that the following conditions
     3.9 +# are met:
    3.10 +# 1. Redistributions of source code must retain the above copyright
    3.11 +#    notice, this list of conditions and the following disclaimer.
    3.12 +# 2. Redistributions in binary form must reproduce the above copyright
    3.13 +#    notice, this list of conditions and the following disclaimer in the
    3.14 +#    documentation and/or other materials provided with the distribution.
    3.15 +#
    3.16 +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
    3.17 +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    3.18 +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    3.19 +# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
    3.20 +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    3.21 +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
    3.22 +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
    3.23 +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
    3.24 +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
    3.25 +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
    3.26 +# SUCH DAMAGE.
    3.27 +#
    3.28 +# $Id$
    3.29 +
    3.30 +__author__ = 'Allan Saddi <allan@saddi.com>'
    3.31 +__version__ = '$Revision$'
    3.32 +
    3.33 +import sys
    3.34 +import socket
    3.35 +import select
    3.36 +import signal
    3.37 +import errno
    3.38 +
    3.39 +try:
    3.40 +    import fcntl
    3.41 +except ImportError:
    3.42 +    def setCloseOnExec(sock):
    3.43 +        pass
    3.44 +else:
    3.45 +    def setCloseOnExec(sock):
    3.46 +        fcntl.fcntl(sock.fileno(), fcntl.F_SETFD, fcntl.FD_CLOEXEC)
    3.47 +
    3.48 +__all__ = ['SingleServer']
    3.49 +
    3.50 +class SingleServer(object):
    3.51 +    def __init__(self, jobClass=None, jobArgs=(), **kw):
    3.52 +        self._jobClass = jobClass
    3.53 +        self._jobArgs = jobArgs
    3.54 +
    3.55 +    def run(self, sock, timeout=1.0):
    3.56 +        """
    3.57 +        The main loop. Pass a socket that is ready to accept() client
    3.58 +        connections. Return value will be True or False indiciating whether
    3.59 +        or not the loop was exited due to SIGHUP.
    3.60 +        """
    3.61 +        # Set up signal handlers.
    3.62 +        self._keepGoing = True
    3.63 +        self._hupReceived = False
    3.64 +
    3.65 +        # Might need to revisit this?
    3.66 +        if not sys.platform.startswith('win'):
    3.67 +            self._installSignalHandlers()
    3.68 +
    3.69 +        # Set close-on-exec
    3.70 +        setCloseOnExec(sock)
    3.71 +        
    3.72 +        # Main loop.
    3.73 +        while self._keepGoing:
    3.74 +            try:
    3.75 +                r, w, e = select.select([sock], [], [], timeout)
    3.76 +            except select.error, e:
    3.77 +                if e[0] == errno.EINTR:
    3.78 +                    continue
    3.79 +                raise
    3.80 +
    3.81 +            if r:
    3.82 +                try:
    3.83 +                    clientSock, addr = sock.accept()
    3.84 +                except socket.error, e:
    3.85 +                    if e[0] in (errno.EINTR, errno.EAGAIN):
    3.86 +                        continue
    3.87 +                    raise
    3.88 +
    3.89 +                setCloseOnExec(clientSock)
    3.90 +                
    3.91 +                if not self._isClientAllowed(addr):
    3.92 +                    clientSock.close()
    3.93 +                    continue
    3.94 +
    3.95 +                # Hand off to Connection.
    3.96 +                conn = self._jobClass(clientSock, addr, *self._jobArgs)
    3.97 +                conn.run()
    3.98 +
    3.99 +            self._mainloopPeriodic()
   3.100 +
   3.101 +        # Restore signal handlers.
   3.102 +        self._restoreSignalHandlers()
   3.103 +
   3.104 +        # Return bool based on whether or not SIGHUP was received.
   3.105 +        return self._hupReceived
   3.106 +
   3.107 +    def _mainloopPeriodic(self):
   3.108 +        """
   3.109 +        Called with just about each iteration of the main loop. Meant to
   3.110 +        be overridden.
   3.111 +        """
   3.112 +        pass
   3.113 +
   3.114 +    def _exit(self, reload=False):
   3.115 +        """
   3.116 +        Protected convenience method for subclasses to force an exit. Not
   3.117 +        really thread-safe, which is why it isn't public.
   3.118 +        """
   3.119 +        if self._keepGoing:
   3.120 +            self._keepGoing = False
   3.121 +            self._hupReceived = reload
   3.122 +
   3.123 +    def _isClientAllowed(self, addr):
   3.124 +        """Override to provide access control."""
   3.125 +        return True
   3.126 +
   3.127 +    # Signal handlers
   3.128 +
   3.129 +    def _hupHandler(self, signum, frame):
   3.130 +        self._hupReceived = True
   3.131 +        self._keepGoing = False
   3.132 +
   3.133 +    def _intHandler(self, signum, frame):
   3.134 +        self._keepGoing = False
   3.135 +
   3.136 +    def _installSignalHandlers(self):
   3.137 +        supportedSignals = [signal.SIGINT, signal.SIGTERM]
   3.138 +        if hasattr(signal, 'SIGHUP'):
   3.139 +            supportedSignals.append(signal.SIGHUP)
   3.140 +
   3.141 +        self._oldSIGs = [(x,signal.getsignal(x)) for x in supportedSignals]
   3.142 +
   3.143 +        for sig in supportedSignals:
   3.144 +            if hasattr(signal, 'SIGHUP') and sig == signal.SIGHUP:
   3.145 +                signal.signal(sig, self._hupHandler)
   3.146 +            else:
   3.147 +                signal.signal(sig, self._intHandler)
   3.148 +
   3.149 +    def _restoreSignalHandlers(self):
   3.150 +        for signum,handler in self._oldSIGs:
   3.151 +            signal.signal(signum, handler)
   3.152 +
   3.153 +if __name__ == '__main__':
   3.154 +    class TestJob(object):
   3.155 +        def __init__(self, sock, addr):
   3.156 +            self._sock = sock
   3.157 +            self._addr = addr
   3.158 +        def run(self):
   3.159 +            print "Client connection opened from %s:%d" % self._addr
   3.160 +            self._sock.send('Hello World!\n')
   3.161 +            self._sock.setblocking(1)
   3.162 +            self._sock.recv(1)
   3.163 +            self._sock.close()
   3.164 +            print "Client connection closed from %s:%d" % self._addr
   3.165 +    sock = socket.socket()
   3.166 +    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
   3.167 +    sock.bind(('', 8080))
   3.168 +    sock.listen(socket.SOMAXCONN)
   3.169 +    SingleServer(jobClass=TestJob).run(sock)