Ticket #22: singleserver.diff

File singleserver.diff, 12.3 kB (added by trac, 2 years ago)

Non-threading non-forking variant of fastcgi

  • /dev/null

    old new  
     1# Copyright (c) 2005, 2006 Allan Saddi <allan@saddi.com> 
     2# All rights reserved. 
     3# 
     4# Redistribution and use in source and binary forms, with or without 
     5# modification, are permitted provided that the following conditions 
     6# are met: 
     7# 1. Redistributions of source code must retain the above copyright 
     8#    notice, this list of conditions and the following disclaimer. 
     9# 2. Redistributions in binary form must reproduce the above copyright 
     10#    notice, this list of conditions and the following disclaimer in the 
     11#    documentation and/or other materials provided with the distribution. 
     12# 
     13# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 
     14# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
     15# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
     16# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 
     17# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
     18# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 
     19# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
     20# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
     21# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 
     22# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 
     23# SUCH DAMAGE. 
     24# 
     25# $Id$ 
     26 
     27""" 
     28fcgi - a FastCGI/WSGI gateway. 
     29 
     30For more information about FastCGI, see <http://www.fastcgi.com/>. 
     31 
     32For more information about the Web Server Gateway Interface, see 
     33<http://www.python.org/peps/pep-0333.html>. 
     34 
     35Example usage: 
     36 
     37  #!/usr/bin/env python 
     38  from myapplication import app # Assume app is your WSGI application object 
     39  from fcgi import WSGIServer 
     40  WSGIServer(app).run() 
     41 
     42See the documentation for WSGIServer for more information. 
     43 
     44On most platforms, fcgi will fallback to regular CGI behavior if run in a 
     45non-FastCGI context. If you want to force CGI behavior, set the environment 
     46variable FCGI_FORCE_CGI to "Y" or "y". 
     47""" 
     48 
     49__author__ = 'Allan Saddi <allan@saddi.com>' 
     50__version__ = '$Revision$' 
     51 
     52import os 
     53 
     54from flup.server.fcgi_base import BaseFCGIServer, FCGI_RESPONDER, \ 
     55     FCGI_MAX_CONNS, FCGI_MAX_REQS, FCGI_MPXS_CONNS 
     56from flup.server.singleserver import SingleServer 
     57 
     58__all__ = ['WSGIServer'] 
     59 
     60class WSGIServer(BaseFCGIServer, SingleServer): 
     61    """ 
     62    FastCGI server that supports the Web Server Gateway Interface. See 
     63    <http://www.python.org/peps/pep-0333.html>. 
     64    """ 
     65    def __init__(self, application, environ=None, 
     66                 bindAddress=None, umask=None, multiplexed=False, 
     67                 debug=True, roles=(FCGI_RESPONDER,), **kw): 
     68        """ 
     69        environ, if present, must be a dictionary-like object. Its 
     70        contents will be copied into application's environ. Useful 
     71        for passing application-specific variables. 
     72 
     73        bindAddress, if present, must either be a string or a 2-tuple. If 
     74        present, run() will open its own listening socket. You would use 
     75        this if you wanted to run your application as an 'external' FastCGI 
     76        app. (i.e. the webserver would no longer be responsible for starting 
     77        your app) If a string, it will be interpreted as a filename and a UNIX 
     78        socket will be opened. If a tuple, the first element, a string, 
     79        is the interface name/IP to bind to, and the second element (an int) 
     80        is the port number. 
     81        """ 
     82        BaseFCGIServer.__init__(self, application, 
     83                                environ=environ, 
     84                                multithreaded=False, 
     85                                multiprocess=False, 
     86                                bindAddress=bindAddress, 
     87                                umask=umask, 
     88                                multiplexed=multiplexed, 
     89                                debug=debug, 
     90                                roles=roles) 
     91        for key in ('jobClass', 'jobArgs'): 
     92            if kw.has_key(key): 
     93                del kw[key] 
     94        SingleServer.__init__(self, jobClass=self._connectionClass, 
     95                              jobArgs=(self,), **kw) 
     96        self.capability = { 
     97            FCGI_MAX_CONNS: 1, 
     98            FCGI_MAX_REQS: 1, 
     99            FCGI_MPXS_CONNS: 0 
     100            } 
     101 
     102    def _isClientAllowed(self, addr): 
     103        return self._web_server_addrs is None or \ 
     104               (len(addr) == 2 and addr[0] in self._web_server_addrs) 
     105 
     106    def run(self): 
     107        """ 
     108        The main loop. Exits on SIGHUP, SIGINT, SIGTERM. Returns True if 
     109        SIGHUP was received, False otherwise. 
     110        """ 
     111        self._web_server_addrs = os.environ.get('FCGI_WEB_SERVER_ADDRS') 
     112        if self._web_server_addrs is not None: 
     113            self._web_server_addrs = map(lambda x: x.strip(), 
     114                                         self._web_server_addrs.split(',')) 
     115 
     116        sock = self._setupSocket() 
     117 
     118        ret = SingleServer.run(self, sock) 
     119 
     120        self._cleanupSocket(sock) 
     121 
     122        return ret 
     123 
     124def factory(global_conf, host=None, port=None, **local): 
     125    import paste_factory 
     126    return paste_factory.helper(WSGIServer, global_conf, host, port, **local) 
     127 
     128if __name__ == '__main__': 
     129    def test_app(environ, start_response): 
     130        """Probably not the most efficient example.""" 
     131        import cgi 
     132        start_response('200 OK', [('Content-Type', 'text/html')]) 
     133        yield '<html><head><title>Hello World!</title></head>\n' \ 
     134              '<body>\n' \ 
     135              '<p>Hello World!</p>\n' \ 
     136              '<table border="1">' 
     137        names = environ.keys() 
     138        names.sort() 
     139        for name in names: 
     140            yield '<tr><td>%s</td><td>%s</td></tr>\n' % ( 
     141                name, cgi.escape(`environ[name]`)) 
     142 
     143        form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ, 
     144                                keep_blank_values=1) 
     145        if form.list: 
     146            yield '<tr><th colspan="2">Form data</th></tr>' 
     147 
     148        for field in form.list: 
     149            yield '<tr><td>%s</td><td>%s</td></tr>\n' % ( 
     150                field.name, field.value) 
     151 
     152        yield '</table>\n' \ 
     153              '</body></html>\n' 
     154 
     155    from wsgiref import validate 
     156    test_app = validate.validator(test_app) 
     157    WSGIServer(test_app).run() 
  • /dev/null

    old new  
     1# Copyright (c) 2005 Allan Saddi <allan@saddi.com> 
     2# All rights reserved. 
     3# 
     4# Redistribution and use in source and binary forms, with or without 
     5# modification, are permitted provided that the following conditions 
     6# are met: 
     7# 1. Redistributions of source code must retain the above copyright 
     8#    notice, this list of conditions and the following disclaimer. 
     9# 2. Redistributions in binary form must reproduce the above copyright 
     10#    notice, this list of conditions and the following disclaimer in the 
     11#    documentation and/or other materials provided with the distribution. 
     12# 
     13# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 
     14# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
     15# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
     16# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 
     17# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
     18# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 
     19# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
     20# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
     21# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 
     22# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 
     23# SUCH DAMAGE. 
     24# 
     25# $Id$ 
     26 
     27__author__ = 'Allan Saddi <allan@saddi.com>' 
     28__version__ = '$Revision$' 
     29 
     30import sys 
     31import socket 
     32import select 
     33import signal 
     34import errno 
     35 
     36try: 
     37    import fcntl 
     38except ImportError: 
     39    def setCloseOnExec(sock): 
     40        pass 
     41else: 
     42    def setCloseOnExec(sock): 
     43        fcntl.fcntl(sock.fileno(), fcntl.F_SETFD, fcntl.FD_CLOEXEC) 
     44 
     45__all__ = ['SingleServer'] 
     46 
     47class SingleServer(object): 
     48    def __init__(self, jobClass=None, jobArgs=(), **kw): 
     49        self._jobClass = jobClass 
     50        self._jobArgs = jobArgs 
     51 
     52    def run(self, sock, timeout=1.0): 
     53        """ 
     54        The main loop. Pass a socket that is ready to accept() client 
     55        connections. Return value will be True or False indiciating whether 
     56        or not the loop was exited due to SIGHUP. 
     57        """ 
     58        # Set up signal handlers. 
     59        self._keepGoing = True 
     60        self._hupReceived = False 
     61 
     62        # Might need to revisit this? 
     63        if not sys.platform.startswith('win'): 
     64            self._installSignalHandlers() 
     65 
     66        # Set close-on-exec 
     67        setCloseOnExec(sock) 
     68         
     69        # Main loop. 
     70        while self._keepGoing: 
     71            try: 
     72                r, w, e = select.select([sock], [], [], timeout) 
     73            except select.error, e: 
     74                if e[0] == errno.EINTR: 
     75                    continue 
     76                raise 
     77 
     78            if r: 
     79                try: 
     80                    clientSock, addr = sock.accept() 
     81                except socket.error, e: 
     82                    if e[0] in (errno.EINTR, errno.EAGAIN): 
     83                        continue 
     84                    raise 
     85 
     86                setCloseOnExec(clientSock) 
     87                 
     88                if not self._isClientAllowed(addr): 
     89                    clientSock.close() 
     90                    continue 
     91 
     92                # Hand off to Connection. 
     93                conn = self._jobClass(clientSock, addr, *self._jobArgs) 
     94                conn.run() 
     95 
     96            self._mainloopPeriodic() 
     97 
     98        # Restore signal handlers. 
     99        self._restoreSignalHandlers() 
     100 
     101        # Return bool based on whether or not SIGHUP was received. 
     102        return self._hupReceived 
     103 
     104    def _mainloopPeriodic(self): 
     105        """ 
     106        Called with just about each iteration of the main loop. Meant to 
     107        be overridden. 
     108        """ 
     109        pass 
     110 
     111    def _exit(self, reload=False): 
     112        """ 
     113        Protected convenience method for subclasses to force an exit. Not 
     114        really thread-safe, which is why it isn't public. 
     115        """ 
     116        if self._keepGoing: 
     117            self._keepGoing = False 
     118            self._hupReceived = reload 
     119 
     120    def _isClientAllowed(self, addr): 
     121        """Override to provide access control.""" 
     122        return True 
     123 
     124    # Signal handlers 
     125 
     126    def _hupHandler(self, signum, frame): 
     127        self._hupReceived = True 
     128        self._keepGoing = False 
     129 
     130    def _intHandler(self, signum, frame): 
     131        self._keepGoing = False 
     132 
     133    def _installSignalHandlers(self): 
     134        supportedSignals = [signal.SIGINT, signal.SIGTERM] 
     135        if hasattr(signal, 'SIGHUP'): 
     136            supportedSignals.append(signal.SIGHUP) 
     137 
     138        self._oldSIGs = [(x,signal.getsignal(x)) for x in supportedSignals] 
     139 
     140        for sig in supportedSignals: 
     141            if hasattr(signal, 'SIGHUP') and sig == signal.SIGHUP: 
     142                signal.signal(sig, self._hupHandler) 
     143            else: 
     144                signal.signal(sig, self._intHandler) 
     145 
     146    def _restoreSignalHandlers(self): 
     147        for signum,handler in self._oldSIGs: 
     148            signal.signal(signum, handler) 
     149 
     150if __name__ == '__main__': 
     151    class TestJob(object): 
     152        def __init__(self, sock, addr): 
     153            self._sock = sock 
     154            self._addr = addr 
     155        def run(self): 
     156            print "Client connection opened from %s:%d" % self._addr 
     157            self._sock.send('Hello World!\n') 
     158            self._sock.setblocking(1) 
     159            self._sock.recv(1) 
     160            self._sock.close() 
     161            print "Client connection closed from %s:%d" % self._addr 
     162    sock = socket.socket() 
     163    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
     164    sock.bind(('', 8080)) 
     165    sock.listen(socket.SOMAXCONN) 
     166    SingleServer(jobClass=TestJob).run(sock)