diff options
author | Allan Saddi <allan@saddi.com> | 2009-02-02 23:34:31 -0800 |
---|---|---|
committer | Allan Saddi <allan@saddi.com> | 2009-02-02 23:34:31 -0800 |
commit | 517cbc0c9858b07c86f3585f454842562cee188e (patch) | |
tree | 1fc6d09767a5b97a497579fe3433050d0aa1b8f0 /flup | |
parent | ca3ba16836416556cdad565f3e2ef3c56df6606a (diff) | |
download | flup-517cbc0c9858b07c86f3585f454842562cee188e.tar.gz |
Merge Tommi Virtanen's "single server" (sequential server)
patch.
Diffstat (limited to 'flup')
-rw-r--r-- | flup/server/fcgi_single.py | 157 | ||||
-rw-r--r-- | flup/server/singleserver.py | 166 |
2 files changed, 323 insertions, 0 deletions
diff --git a/flup/server/fcgi_single.py b/flup/server/fcgi_single.py new file mode 100644 index 0000000..5c9367a --- /dev/null +++ b/flup/server/fcgi_single.py @@ -0,0 +1,157 @@ +# Copyright (c) 2005, 2006 Allan Saddi <allan@saddi.com> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# $Id$ + +""" +fcgi - a FastCGI/WSGI gateway. + +For more information about FastCGI, see <http://www.fastcgi.com/>. + +For more information about the Web Server Gateway Interface, see +<http://www.python.org/peps/pep-0333.html>. + +Example usage: + + #!/usr/bin/env python + from myapplication import app # Assume app is your WSGI application object + from fcgi import WSGIServer + WSGIServer(app).run() + +See the documentation for WSGIServer for more information. + +On most platforms, fcgi will fallback to regular CGI behavior if run in a +non-FastCGI context. If you want to force CGI behavior, set the environment +variable FCGI_FORCE_CGI to "Y" or "y". +""" + +__author__ = 'Allan Saddi <allan@saddi.com>' +__version__ = '$Revision$' + +import os + +from flup.server.fcgi_base import BaseFCGIServer, FCGI_RESPONDER, \ + FCGI_MAX_CONNS, FCGI_MAX_REQS, FCGI_MPXS_CONNS +from flup.server.singleserver import SingleServer + +__all__ = ['WSGIServer'] + +class WSGIServer(BaseFCGIServer, SingleServer): + """ + FastCGI server that supports the Web Server Gateway Interface. See + <http://www.python.org/peps/pep-0333.html>. + """ + def __init__(self, application, environ=None, + bindAddress=None, umask=None, multiplexed=False, + debug=True, roles=(FCGI_RESPONDER,), **kw): + """ + environ, if present, must be a dictionary-like object. Its + contents will be copied into application's environ. Useful + for passing application-specific variables. + + bindAddress, if present, must either be a string or a 2-tuple. If + present, run() will open its own listening socket. You would use + this if you wanted to run your application as an 'external' FastCGI + app. (i.e. the webserver would no longer be responsible for starting + your app) If a string, it will be interpreted as a filename and a UNIX + socket will be opened. If a tuple, the first element, a string, + is the interface name/IP to bind to, and the second element (an int) + is the port number. + """ + BaseFCGIServer.__init__(self, application, + environ=environ, + multithreaded=False, + multiprocess=False, + bindAddress=bindAddress, + umask=umask, + multiplexed=multiplexed, + debug=debug, + roles=roles) + for key in ('jobClass', 'jobArgs'): + if kw.has_key(key): + del kw[key] + SingleServer.__init__(self, jobClass=self._connectionClass, + jobArgs=(self,), **kw) + self.capability = { + FCGI_MAX_CONNS: 1, + FCGI_MAX_REQS: 1, + FCGI_MPXS_CONNS: 0 + } + + def _isClientAllowed(self, addr): + return self._web_server_addrs is None or \ + (len(addr) == 2 and addr[0] in self._web_server_addrs) + + def run(self): + """ + The main loop. Exits on SIGHUP, SIGINT, SIGTERM. Returns True if + SIGHUP was received, False otherwise. + """ + self._web_server_addrs = os.environ.get('FCGI_WEB_SERVER_ADDRS') + if self._web_server_addrs is not None: + self._web_server_addrs = map(lambda x: x.strip(), + self._web_server_addrs.split(',')) + + sock = self._setupSocket() + + ret = SingleServer.run(self, sock) + + self._cleanupSocket(sock) + + return ret + +def factory(global_conf, host=None, port=None, **local): + import paste_factory + return paste_factory.helper(WSGIServer, global_conf, host, port, **local) + +if __name__ == '__main__': + def test_app(environ, start_response): + """Probably not the most efficient example.""" + import cgi + start_response('200 OK', [('Content-Type', 'text/html')]) + yield '<html><head><title>Hello World!</title></head>\n' \ + '<body>\n' \ + '<p>Hello World!</p>\n' \ + '<table border="1">' + names = environ.keys() + names.sort() + for name in names: + yield '<tr><td>%s</td><td>%s</td></tr>\n' % ( + name, cgi.escape(`environ[name]`)) + + form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ, + keep_blank_values=1) + if form.list: + yield '<tr><th colspan="2">Form data</th></tr>' + + for field in form.list: + yield '<tr><td>%s</td><td>%s</td></tr>\n' % ( + field.name, field.value) + + yield '</table>\n' \ + '</body></html>\n' + + from wsgiref import validate + test_app = validate.validator(test_app) + WSGIServer(test_app).run() diff --git a/flup/server/singleserver.py b/flup/server/singleserver.py new file mode 100644 index 0000000..59fa6ea --- /dev/null +++ b/flup/server/singleserver.py @@ -0,0 +1,166 @@ +# Copyright (c) 2005 Allan Saddi <allan@saddi.com> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# $Id$ + +__author__ = 'Allan Saddi <allan@saddi.com>' +__version__ = '$Revision$' + +import sys +import socket +import select +import signal +import errno + +try: + import fcntl +except ImportError: + def setCloseOnExec(sock): + pass +else: + def setCloseOnExec(sock): + fcntl.fcntl(sock.fileno(), fcntl.F_SETFD, fcntl.FD_CLOEXEC) + +__all__ = ['SingleServer'] + +class SingleServer(object): + def __init__(self, jobClass=None, jobArgs=(), **kw): + self._jobClass = jobClass + self._jobArgs = jobArgs + + def run(self, sock, timeout=1.0): + """ + The main loop. Pass a socket that is ready to accept() client + connections. Return value will be True or False indiciating whether + or not the loop was exited due to SIGHUP. + """ + # Set up signal handlers. + self._keepGoing = True + self._hupReceived = False + + # Might need to revisit this? + if not sys.platform.startswith('win'): + self._installSignalHandlers() + + # Set close-on-exec + setCloseOnExec(sock) + + # Main loop. + while self._keepGoing: + try: + r, w, e = select.select([sock], [], [], timeout) + except select.error, e: + if e[0] == errno.EINTR: + continue + raise + + if r: + try: + clientSock, addr = sock.accept() + except socket.error, e: + if e[0] in (errno.EINTR, errno.EAGAIN): + continue + raise + + setCloseOnExec(clientSock) + + if not self._isClientAllowed(addr): + clientSock.close() + continue + + # Hand off to Connection. + conn = self._jobClass(clientSock, addr, *self._jobArgs) + conn.run() + + self._mainloopPeriodic() + + # Restore signal handlers. + self._restoreSignalHandlers() + + # Return bool based on whether or not SIGHUP was received. + return self._hupReceived + + def _mainloopPeriodic(self): + """ + Called with just about each iteration of the main loop. Meant to + be overridden. + """ + pass + + def _exit(self, reload=False): + """ + Protected convenience method for subclasses to force an exit. Not + really thread-safe, which is why it isn't public. + """ + if self._keepGoing: + self._keepGoing = False + self._hupReceived = reload + + def _isClientAllowed(self, addr): + """Override to provide access control.""" + return True + + # Signal handlers + + def _hupHandler(self, signum, frame): + self._hupReceived = True + self._keepGoing = False + + def _intHandler(self, signum, frame): + self._keepGoing = False + + def _installSignalHandlers(self): + supportedSignals = [signal.SIGINT, signal.SIGTERM] + if hasattr(signal, 'SIGHUP'): + supportedSignals.append(signal.SIGHUP) + + self._oldSIGs = [(x,signal.getsignal(x)) for x in supportedSignals] + + for sig in supportedSignals: + if hasattr(signal, 'SIGHUP') and sig == signal.SIGHUP: + signal.signal(sig, self._hupHandler) + else: + signal.signal(sig, self._intHandler) + + def _restoreSignalHandlers(self): + for signum,handler in self._oldSIGs: + signal.signal(signum, handler) + +if __name__ == '__main__': + class TestJob(object): + def __init__(self, sock, addr): + self._sock = sock + self._addr = addr + def run(self): + print "Client connection opened from %s:%d" % self._addr + self._sock.send('Hello World!\n') + self._sock.setblocking(1) + self._sock.recv(1) + self._sock.close() + print "Client connection closed from %s:%d" % self._addr + sock = socket.socket() + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('', 8080)) + sock.listen(socket.SOMAXCONN) + SingleServer(jobClass=TestJob).run(sock) |