changeset 91:fa0bc062585c

Merge from flup-server.
author Allan Saddi <allan@saddi.com>
date Fri, 15 May 2009 20:09:51 -0700
parents b98a1a0b4950 a020f1b2c456
children 69b39aae73e7
files flup/server/fcgi.py flup/server/fcgi_base.py flup/server/fcgi_fork.py flup/server/fcgi_single.py flup/server/preforkserver.py flup/server/singleserver.py
diffstat 7 files changed, 361 insertions(+), 20 deletions(-) [+]
line wrap: on
line diff
     1.1 --- a/ChangeLog	Fri Dec 05 16:43:28 2008 -0800
     1.2 +++ b/ChangeLog	Fri May 15 20:09:51 2009 -0700
     1.3 @@ -1,3 +1,11 @@
     1.4 +2009-02-02  Allan Saddi  <allan@saddi.com>
     1.5 +
     1.6 +	* Add forceCGI keyword argument to FastCGI servers to
     1.7 +	  programmatically force CGI behavior.
     1.8 +
     1.9 +	* Merge Tommi Virtanen's "single server" (sequential server)
    1.10 +	  patch.
    1.11 +
    1.12  2008-12-03  Allan Saddi  <allan@saddi.com>
    1.13  
    1.14  	* Update ez_setup.py.
     2.1 --- a/flup/server/fcgi.py	Fri Dec 05 16:43:28 2008 -0800
     2.2 +++ b/flup/server/fcgi.py	Fri May 15 20:09:51 2009 -0700
     2.3 @@ -64,7 +64,7 @@
     2.4      def __init__(self, application, environ=None,
     2.5                   multithreaded=True, multiprocess=False,
     2.6                   bindAddress=None, umask=None, multiplexed=False,
     2.7 -                 debug=True, roles=(FCGI_RESPONDER,), **kw):
     2.8 +                 debug=True, roles=(FCGI_RESPONDER,), forceCGI=False, **kw):
     2.9          """
    2.10          environ, if present, must be a dictionary-like object. Its
    2.11          contents will be copied into application's environ. Useful
    2.12 @@ -87,7 +87,8 @@
    2.13                                  umask=umask,
    2.14                                  multiplexed=multiplexed,
    2.15                                  debug=debug,
    2.16 -                                roles=roles)
    2.17 +                                roles=roles,
    2.18 +                                forceCGI=forceCGI)
    2.19          for key in ('jobClass', 'jobArgs'):
    2.20              if key in kw:
    2.21                  del kw[key]
     3.1 --- a/flup/server/fcgi_base.py	Fri Dec 05 16:43:28 2008 -0800
     3.2 +++ b/flup/server/fcgi_base.py	Fri May 15 20:09:51 2009 -0700
     3.3 @@ -902,7 +902,8 @@
     3.4      def __init__(self, application, environ=None,
     3.5                   multithreaded=True, multiprocess=False,
     3.6                   bindAddress=None, umask=None, multiplexed=False,
     3.7 -                 debug=True, roles=(FCGI_RESPONDER,)):
     3.8 +                 debug=True, roles=(FCGI_RESPONDER,),
     3.9 +                 forceCGI=False):
    3.10          """
    3.11          bindAddress, if present, must either be a string or a 2-tuple. If
    3.12          present, run() will open its own listening socket. You would use
    3.13 @@ -934,6 +935,7 @@
    3.14          self.multiprocess = multiprocess
    3.15          self.debug = debug
    3.16          self.roles = roles
    3.17 +        self.forceCGI = forceCGI
    3.18  
    3.19          self._bindAddress = bindAddress
    3.20          self._umask = umask
    3.21 @@ -989,7 +991,7 @@
    3.22              # if you want to run your app as a simple CGI. (You can do
    3.23              # this with Apache's mod_env [not loaded by default in OS X
    3.24              # client, ha ha] and the SetEnv directive.)
    3.25 -            if not isFCGI or \
    3.26 +            if not isFCGI or self.forceCGI or \
    3.27                 os.environ.get('FCGI_FORCE_CGI', 'N').upper().startswith('Y'):
    3.28                  req = self.cgirequest_class(self)
    3.29                  req.run()
     4.1 --- a/flup/server/fcgi_fork.py	Fri Dec 05 16:43:28 2008 -0800
     4.2 +++ b/flup/server/fcgi_fork.py	Fri May 15 20:09:51 2009 -0700
     4.3 @@ -51,7 +51,7 @@
     4.4  
     4.5  import os
     4.6  
     4.7 -from .fcgi_base import BaseFCGIServer, \
     4.8 +from .fcgi_base import BaseFCGIServer, FCGI_RESPONDER, \
     4.9       FCGI_MAX_CONNS, FCGI_MAX_REQS, FCGI_MPXS_CONNS
    4.10  from .preforkserver import PreforkServer
    4.11  
    4.12 @@ -64,7 +64,7 @@
    4.13      """
    4.14      def __init__(self, application, environ=None,
    4.15                   bindAddress=None, umask=None, multiplexed=False,
    4.16 -                 debug=True, **kw):
    4.17 +                 debug=True, roles=(FCGI_RESPONDER,), forceCGI=False, **kw):
    4.18          """
    4.19          environ, if present, must be a dictionary-like object. Its
    4.20          contents will be copied into application's environ. Useful
    4.21 @@ -86,7 +86,9 @@
    4.22                                  bindAddress=bindAddress,
    4.23                                  umask=umask,
    4.24                                  multiplexed=multiplexed,
    4.25 -                                debug=debug)
    4.26 +                                debug=debug,
    4.27 +                                roles=roles,
    4.28 +                                forceCGI=forceCGI)
    4.29          for key in ('multithreaded', 'multiprocess', 'jobClass', 'jobArgs'):
    4.30              if key in kw:
    4.31                  del kw[key]
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/flup/server/fcgi_single.py	Fri May 15 20:09:51 2009 -0700
     5.3 @@ -0,0 +1,157 @@
     5.4 +# Copyright (c) 2005, 2006 Allan Saddi <allan@saddi.com>
     5.5 +# All rights reserved.
     5.6 +#
     5.7 +# Redistribution and use in source and binary forms, with or without
     5.8 +# modification, are permitted provided that the following conditions
     5.9 +# are met:
    5.10 +# 1. Redistributions of source code must retain the above copyright
    5.11 +#    notice, this list of conditions and the following disclaimer.
    5.12 +# 2. Redistributions in binary form must reproduce the above copyright
    5.13 +#    notice, this list of conditions and the following disclaimer in the
    5.14 +#    documentation and/or other materials provided with the distribution.
    5.15 +#
    5.16 +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
    5.17 +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    5.18 +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    5.19 +# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
    5.20 +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    5.21 +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
    5.22 +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
    5.23 +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
    5.24 +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
    5.25 +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
    5.26 +# SUCH DAMAGE.
    5.27 +#
    5.28 +# $Id$
    5.29 +
    5.30 +"""
    5.31 +fcgi - a FastCGI/WSGI gateway.
    5.32 +
    5.33 +For more information about FastCGI, see <http://www.fastcgi.com/>.
    5.34 +
    5.35 +For more information about the Web Server Gateway Interface, see
    5.36 +<http://www.python.org/peps/pep-0333.html>.
    5.37 +
    5.38 +Example usage:
    5.39 +
    5.40 +  #!/usr/bin/env python
    5.41 +  from myapplication import app # Assume app is your WSGI application object
    5.42 +  from fcgi import WSGIServer
    5.43 +  WSGIServer(app).run()
    5.44 +
    5.45 +See the documentation for WSGIServer for more information.
    5.46 +
    5.47 +On most platforms, fcgi will fallback to regular CGI behavior if run in a
    5.48 +non-FastCGI context. If you want to force CGI behavior, set the environment
    5.49 +variable FCGI_FORCE_CGI to "Y" or "y".
    5.50 +"""
    5.51 +
    5.52 +__author__ = 'Allan Saddi <allan@saddi.com>'
    5.53 +__version__ = '$Revision$'
    5.54 +
    5.55 +import os
    5.56 +
    5.57 +from .fcgi_base import BaseFCGIServer, FCGI_RESPONDER, \
    5.58 +     FCGI_MAX_CONNS, FCGI_MAX_REQS, FCGI_MPXS_CONNS
    5.59 +from .singleserver import SingleServer
    5.60 +
    5.61 +__all__ = ['WSGIServer']
    5.62 +
    5.63 +class WSGIServer(BaseFCGIServer, SingleServer):
    5.64 +    """
    5.65 +    FastCGI server that supports the Web Server Gateway Interface. See
    5.66 +    <http://www.python.org/peps/pep-0333.html>.
    5.67 +    """
    5.68 +    def __init__(self, application, environ=None,
    5.69 +                 bindAddress=None, umask=None, multiplexed=False,
    5.70 +                 debug=True, roles=(FCGI_RESPONDER,), forceCGI=False, **kw):
    5.71 +        """
    5.72 +        environ, if present, must be a dictionary-like object. Its
    5.73 +        contents will be copied into application's environ. Useful
    5.74 +        for passing application-specific variables.
    5.75 +
    5.76 +        bindAddress, if present, must either be a string or a 2-tuple. If
    5.77 +        present, run() will open its own listening socket. You would use
    5.78 +        this if you wanted to run your application as an 'external' FastCGI
    5.79 +        app. (i.e. the webserver would no longer be responsible for starting
    5.80 +        your app) If a string, it will be interpreted as a filename and a UNIX
    5.81 +        socket will be opened. If a tuple, the first element, a string,
    5.82 +        is the interface name/IP to bind to, and the second element (an int)
    5.83 +        is the port number.
    5.84 +        """
    5.85 +        BaseFCGIServer.__init__(self, application,
    5.86 +                                environ=environ,
    5.87 +                                multithreaded=False,
    5.88 +                                multiprocess=False,
    5.89 +                                bindAddress=bindAddress,
    5.90 +                                umask=umask,
    5.91 +                                multiplexed=multiplexed,
    5.92 +                                debug=debug,
    5.93 +                                roles=roles,
    5.94 +                                forceCGI=forceCGI)
    5.95 +        for key in ('jobClass', 'jobArgs'):
    5.96 +            if key in kw:
    5.97 +                del kw[key]
    5.98 +        SingleServer.__init__(self, jobClass=self._connectionClass,
    5.99 +                              jobArgs=(self,), **kw)
   5.100 +        self.capability = {
   5.101 +            FCGI_MAX_CONNS: 1,
   5.102 +            FCGI_MAX_REQS: 1,
   5.103 +            FCGI_MPXS_CONNS: 0
   5.104 +            }
   5.105 +
   5.106 +    def _isClientAllowed(self, addr):
   5.107 +        return self._web_server_addrs is None or \
   5.108 +               (len(addr) == 2 and addr[0] in self._web_server_addrs)
   5.109 +
   5.110 +    def run(self):
   5.111 +        """
   5.112 +        The main loop. Exits on SIGHUP, SIGINT, SIGTERM. Returns True if
   5.113 +        SIGHUP was received, False otherwise.
   5.114 +        """
   5.115 +        self._web_server_addrs = os.environ.get('FCGI_WEB_SERVER_ADDRS')
   5.116 +        if self._web_server_addrs is not None:
   5.117 +            self._web_server_addrs = [x.strip() for x in self._web_server_addrs.split(',')]
   5.118 +
   5.119 +        sock = self._setupSocket()
   5.120 +
   5.121 +        ret = SingleServer.run(self, sock)
   5.122 +
   5.123 +        self._cleanupSocket(sock)
   5.124 +
   5.125 +        return ret
   5.126 +
   5.127 +def factory(global_conf, host=None, port=None, **local):
   5.128 +    from . import paste_factory
   5.129 +    return paste_factory.helper(WSGIServer, global_conf, host, port, **local)
   5.130 +
   5.131 +if __name__ == '__main__':
   5.132 +    def test_app(environ, start_response):
   5.133 +        """Probably not the most efficient example."""
   5.134 +        from . import cgi
   5.135 +        start_response('200 OK', [('Content-Type', 'text/html')])
   5.136 +        yield '<html><head><title>Hello World!</title></head>\n' \
   5.137 +              '<body>\n' \
   5.138 +              '<p>Hello World!</p>\n' \
   5.139 +              '<table border="1">'
   5.140 +        names = list(environ.keys())
   5.141 +        names.sort()
   5.142 +        for name in names:
   5.143 +            yield '<tr><td>%s</td><td>%s</td></tr>\n' % (
   5.144 +                name, cgi.escape(repr(environ[name])))
   5.145 +
   5.146 +        form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ,
   5.147 +                                keep_blank_values=1)
   5.148 +        if form.list:
   5.149 +            yield '<tr><th colspan="2">Form data</th></tr>'
   5.150 +
   5.151 +        for field in form.list:
   5.152 +            yield '<tr><td>%s</td><td>%s</td></tr>\n' % (
   5.153 +                field.name, field.value)
   5.154 +
   5.155 +        yield '</table>\n' \
   5.156 +              '</body></html>\n'
   5.157 +
   5.158 +    from wsgiref import validate
   5.159 +    test_app = validate.validator(test_app)
   5.160 +    WSGIServer(test_app).run()
     6.1 --- a/flup/server/preforkserver.py	Fri Dec 05 16:43:28 2008 -0800
     6.2 +++ b/flup/server/preforkserver.py	Fri May 15 20:09:51 2009 -0700
     6.3 @@ -309,6 +309,21 @@
     6.4          """Override to provide access control."""
     6.5          return True
     6.6  
     6.7 +    def _notifyParent(self, parent, msg):
     6.8 +        """Send message to parent, ignoring EPIPE and retrying on EAGAIN"""
     6.9 +        while True:
    6.10 +            try:
    6.11 +                parent.send(msg)
    6.12 +                return True
    6.13 +            except socket.error as e:
    6.14 +                if e[0] == errno.EPIPE:
    6.15 +                    return False # Parent is gone
    6.16 +                if e[0] == errno.EAGAIN:
    6.17 +                    # Wait for socket change before sending again
    6.18 +                    select.select([], [parent], [])
    6.19 +                else:
    6.20 +                    raise
    6.21 +                
    6.22      def _child(self, sock, parent):
    6.23          """Main loop for children."""
    6.24          requestCount = 0
    6.25 @@ -353,12 +368,7 @@
    6.26                  continue
    6.27  
    6.28              # Notify parent we're no longer available.
    6.29 -            try:
    6.30 -                parent.send('\x00')
    6.31 -            except socket.error as e:
    6.32 -                # If parent is gone, finish up this request.
    6.33 -                if e[0] != errno.EPIPE:
    6.34 -                    raise
    6.35 +            self._notifyParent(parent, '\x00')
    6.36  
    6.37              # Do the job.
    6.38              self._jobClass(clientSock, addr, *self._jobArgs).run()
    6.39 @@ -370,13 +380,8 @@
    6.40                      break
    6.41                  
    6.42              # Tell parent we're free again.
    6.43 -            try:
    6.44 -                parent.send('\xff')
    6.45 -            except socket.error as e:
    6.46 -                if e[0] == errno.EPIPE:
    6.47 -                    # Parent is gone.
    6.48 -                    return
    6.49 -                raise
    6.50 +            if not self._notifyParent(parent, '\xff'):
    6.51 +                return # Parent is gone.
    6.52  
    6.53      # Signal handlers
    6.54  
     7.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.2 +++ b/flup/server/singleserver.py	Fri May 15 20:09:51 2009 -0700
     7.3 @@ -0,0 +1,166 @@
     7.4 +# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
     7.5 +# All rights reserved.
     7.6 +#
     7.7 +# Redistribution and use in source and binary forms, with or without
     7.8 +# modification, are permitted provided that the following conditions
     7.9 +# are met:
    7.10 +# 1. Redistributions of source code must retain the above copyright
    7.11 +#    notice, this list of conditions and the following disclaimer.
    7.12 +# 2. Redistributions in binary form must reproduce the above copyright
    7.13 +#    notice, this list of conditions and the following disclaimer in the
    7.14 +#    documentation and/or other materials provided with the distribution.
    7.15 +#
    7.16 +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
    7.17 +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    7.18 +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    7.19 +# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
    7.20 +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    7.21 +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
    7.22 +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
    7.23 +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
    7.24 +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
    7.25 +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
    7.26 +# SUCH DAMAGE.
    7.27 +#
    7.28 +# $Id$
    7.29 +
    7.30 +__author__ = 'Allan Saddi <allan@saddi.com>'
    7.31 +__version__ = '$Revision$'
    7.32 +
    7.33 +import sys
    7.34 +import socket
    7.35 +import select
    7.36 +import signal
    7.37 +import errno
    7.38 +
    7.39 +try:
    7.40 +    import fcntl
    7.41 +except ImportError:
    7.42 +    def setCloseOnExec(sock):
    7.43 +        pass
    7.44 +else:
    7.45 +    def setCloseOnExec(sock):
    7.46 +        fcntl.fcntl(sock.fileno(), fcntl.F_SETFD, fcntl.FD_CLOEXEC)
    7.47 +
    7.48 +__all__ = ['SingleServer']
    7.49 +
    7.50 +class SingleServer(object):
    7.51 +    def __init__(self, jobClass=None, jobArgs=(), **kw):
    7.52 +        self._jobClass = jobClass
    7.53 +        self._jobArgs = jobArgs
    7.54 +
    7.55 +    def run(self, sock, timeout=1.0):
    7.56 +        """
    7.57 +        The main loop. Pass a socket that is ready to accept() client
    7.58 +        connections. Return value will be True or False indiciating whether
    7.59 +        or not the loop was exited due to SIGHUP.
    7.60 +        """
    7.61 +        # Set up signal handlers.
    7.62 +        self._keepGoing = True
    7.63 +        self._hupReceived = False
    7.64 +
    7.65 +        # Might need to revisit this?
    7.66 +        if not sys.platform.startswith('win'):
    7.67 +            self._installSignalHandlers()
    7.68 +
    7.69 +        # Set close-on-exec
    7.70 +        setCloseOnExec(sock)
    7.71 +        
    7.72 +        # Main loop.
    7.73 +        while self._keepGoing:
    7.74 +            try:
    7.75 +                r, w, e = select.select([sock], [], [], timeout)
    7.76 +            except select.error as e:
    7.77 +                if e[0] == errno.EINTR:
    7.78 +                    continue
    7.79 +                raise
    7.80 +
    7.81 +            if r:
    7.82 +                try:
    7.83 +                    clientSock, addr = sock.accept()
    7.84 +                except socket.error as e:
    7.85 +                    if e[0] in (errno.EINTR, errno.EAGAIN):
    7.86 +                        continue
    7.87 +                    raise
    7.88 +
    7.89 +                setCloseOnExec(clientSock)
    7.90 +                
    7.91 +                if not self._isClientAllowed(addr):
    7.92 +                    clientSock.close()
    7.93 +                    continue
    7.94 +
    7.95 +                # Hand off to Connection.
    7.96 +                conn = self._jobClass(clientSock, addr, *self._jobArgs)
    7.97 +                conn.run()
    7.98 +
    7.99 +            self._mainloopPeriodic()
   7.100 +
   7.101 +        # Restore signal handlers.
   7.102 +        self._restoreSignalHandlers()
   7.103 +
   7.104 +        # Return bool based on whether or not SIGHUP was received.
   7.105 +        return self._hupReceived
   7.106 +
   7.107 +    def _mainloopPeriodic(self):
   7.108 +        """
   7.109 +        Called with just about each iteration of the main loop. Meant to
   7.110 +        be overridden.
   7.111 +        """
   7.112 +        pass
   7.113 +
   7.114 +    def _exit(self, reload=False):
   7.115 +        """
   7.116 +        Protected convenience method for subclasses to force an exit. Not
   7.117 +        really thread-safe, which is why it isn't public.
   7.118 +        """
   7.119 +        if self._keepGoing:
   7.120 +            self._keepGoing = False
   7.121 +            self._hupReceived = reload
   7.122 +
   7.123 +    def _isClientAllowed(self, addr):
   7.124 +        """Override to provide access control."""
   7.125 +        return True
   7.126 +
   7.127 +    # Signal handlers
   7.128 +
   7.129 +    def _hupHandler(self, signum, frame):
   7.130 +        self._hupReceived = True
   7.131 +        self._keepGoing = False
   7.132 +
   7.133 +    def _intHandler(self, signum, frame):
   7.134 +        self._keepGoing = False
   7.135 +
   7.136 +    def _installSignalHandlers(self):
   7.137 +        supportedSignals = [signal.SIGINT, signal.SIGTERM]
   7.138 +        if hasattr(signal, 'SIGHUP'):
   7.139 +            supportedSignals.append(signal.SIGHUP)
   7.140 +
   7.141 +        self._oldSIGs = [(x,signal.getsignal(x)) for x in supportedSignals]
   7.142 +
   7.143 +        for sig in supportedSignals:
   7.144 +            if hasattr(signal, 'SIGHUP') and sig == signal.SIGHUP:
   7.145 +                signal.signal(sig, self._hupHandler)
   7.146 +            else:
   7.147 +                signal.signal(sig, self._intHandler)
   7.148 +
   7.149 +    def _restoreSignalHandlers(self):
   7.150 +        for signum,handler in self._oldSIGs:
   7.151 +            signal.signal(signum, handler)
   7.152 +
   7.153 +if __name__ == '__main__':
   7.154 +    class TestJob(object):
   7.155 +        def __init__(self, sock, addr):
   7.156 +            self._sock = sock
   7.157 +            self._addr = addr
   7.158 +        def run(self):
   7.159 +            print("Client connection opened from %s:%d" % self._addr)
   7.160 +            self._sock.send('Hello World!\n')
   7.161 +            self._sock.setblocking(1)
   7.162 +            self._sock.recv(1)
   7.163 +            self._sock.close()
   7.164 +            print("Client connection closed from %s:%d" % self._addr)
   7.165 +    sock = socket.socket()
   7.166 +    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
   7.167 +    sock.bind(('', 8080))
   7.168 +    sock.listen(socket.SOMAXCONN)
   7.169 +    SingleServer(jobClass=TestJob).run(sock)