flup-py3.0
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 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)
