summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHelmut Grohne <helmut@subdivi.de>2013-06-06 10:31:29 +0200
committerHelmut Grohne <helmut@subdivi.de>2013-06-06 10:31:29 +0200
commit3651d77be1d44b7fb02ab70fb2685bb401b1105b (patch)
tree63dfef261cf8a90a831fff82625501f00ab82517
parentb0938bb51c915ea5d888e2e88bbb62f4d1da199c (diff)
parented6d6c8f06404489ba2301955c8e6f82f8f4f454 (diff)
downloadwsgitools-3651d77be1d44b7fb02ab70fb2685bb401b1105b.tar.gz
Merge tag 'wsgitools-0.2.4' into py3k
The intent is to port the changes from 0.2.4 to py3k. Conflicts: README test.py wsgitools/scgi/forkpool.py All conflicts were resolved in a minimal way. The test suite now fails for all python versions.
-rw-r--r--LICENSE2
-rw-r--r--MANIFEST.in1
-rwxr-xr-xsetup.py4
-rwxr-xr-xtest.py72
-rw-r--r--wsgitools/scgi/forkpool.py55
5 files changed, 113 insertions, 21 deletions
diff --git a/LICENSE b/LICENSE
index caef583..daf0179 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
Applies to all files in this repository:
-Copyright (C) 2007-2008 Helmut Grohne
+Copyright (C) 2007-2013 Helmut Grohne
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
diff --git a/MANIFEST.in b/MANIFEST.in
index d09c3ce..88ba387 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,2 +1,3 @@
include LICENSE
include epydoc.conf
+include test.py
diff --git a/setup.py b/setup.py
index 777ef6b..13f0992 100755
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
from distutils.core import setup
setup(name="wsgitools",
- version="0.2.3",
+ version="0.2.4",
description="a set of tools working with WSGI (see PEP 333)",
author="Helmut Grohne",
author_email="helmut@subdivi.de",
@@ -12,7 +12,7 @@ setup(name="wsgitools",
license="GPL",
keywords=["wsgi", "pep333", "scgi"],
classifiers=[
- "Development Status :: 4 - Beta",
+ "Development Status :: 5 - Production/Stable",
"Environment :: No Input/Output (Daemon)",
"Environment :: Web Environment",
"Intended Audience :: Developers",
diff --git a/test.py b/test.py
index 1183d63..8e628cd 100755
--- a/test.py
+++ b/test.py
@@ -3,6 +3,7 @@
import base64
import unittest
import doctest
+import re
import wsgiref.validate
import io
from hashlib import md5
@@ -36,21 +37,25 @@ class Request:
"""
@type key: str
@type value: str
+ @returns: self
"""
self.environ[key] = value
+ return self
def setmethod(self, request_method):
"""
@type request_method: str
+ @returns: self
"""
- self.setenv("REQUEST_METHOD", request_method)
+ return self.setenv("REQUEST_METHOD", request_method)
def setheader(self, name, value):
"""
@type name: str
@type value: str
+ @returns: self
"""
- self.setenv(name.upper().replace('-', '_'), value)
+ return self.setenv("HTTP_" + name.upper().replace('-', '_'), value)
def copy(self):
req = Request(self.testcase)
@@ -178,14 +183,14 @@ class AuthDigestMiddlewareTest(unittest.TestCase):
def test401garbage(self):
req = self.req.copy()
- req.setheader('http-authorization', 'Garbage')
+ req.setheader('Authorization', 'Garbage')
res = req(self.app)
res.status(401)
res.header("WWW-Authenticate", lambda _: True)
def test401digestgarbage(self):
req = self.req.copy()
- req.setheader('http-authorization', 'Digest ","')
+ req.setheader('Authorization', 'Digest ","')
res = req(self.app)
res.status(401)
res.header("WWW-Authenticate", lambda _: True)
@@ -199,7 +204,7 @@ class AuthDigestMiddlewareTest(unittest.TestCase):
token = md5(str2bytes("bar:foo:%s" % password)).hexdigest()
other = md5(str2bytes("GET:")).hexdigest()
resp = md5(str2bytes("%s:%s:%s" % (token, nonce, other))).hexdigest()
- req.setheader('http-authorization', 'Digest algorithm=md5,nonce="%s",' \
+ req.setheader('Authorization', 'Digest algorithm=md5,nonce="%s",' \
'uri=,username=bar,response="%s"' % (nonce, resp))
res = req(self.app)
res.status(status)
@@ -220,7 +225,7 @@ class AuthDigestMiddlewareTest(unittest.TestCase):
other = md5(str2bytes("GET:")).hexdigest()
resp = "%s:%s:1:qux:auth:%s" % (token, nonce, other)
resp = md5(str2bytes(resp)).hexdigest()
- req.setheader('http-authorization', 'Digest algorithm=md5,nonce="%s",' \
+ req.setheader('Authorization', 'Digest algorithm=md5,nonce="%s",' \
'uri=,username=bar,response="%s",qop=auth,nc=1,' \
'cnonce=qux' % (nonce, resp))
res = req(self.app)
@@ -292,6 +297,37 @@ class ContentLengthMiddlewareTest(unittest.TestCase):
res.status("200 Found")
res.header("Content-length", "7")
+class CachingMiddlewareTest(unittest.TestCase):
+ def setUp(self):
+ self.cached = middlewares.CachingMiddleware(self.app)
+ self.accessed = dict()
+
+ def app(self, environ, start_response):
+ count = self.accessed.get(environ["SCRIPT_NAME"], 0) + 1
+ self.accessed[environ["SCRIPT_NAME"]] = count
+ headers = [("Content-Type", "text/plain")]
+ if "maxage0" in environ["SCRIPT_NAME"]:
+ headers.append(("Cache-Control", "max-age=0"))
+ start_response("200 Found", headers)
+ return ["%d" % count]
+
+ def testCache(self):
+ res = Request(self)(self.cached)
+ res.status(200)
+ self.assertEqual(res.get_data(), "1")
+ res = Request(self)(self.cached)
+ res.status(200)
+ self.assertEqual(res.get_data(), "1")
+
+ def testNoCache(self):
+ res = Request(self)(self.cached)
+ res.status(200)
+ self.assertEqual(res.get_data(), "1")
+ res = Request(self).setheader(
+ "Cache-Control", "max-age=0")(self.cached)
+ res.status(200)
+ self.assertEqual(res.get_data(), "2")
+
class BasicAuthMiddlewareTest(unittest.TestCase):
def setUp(self):
self.staticapp = applications.StaticContent(
@@ -308,14 +344,14 @@ class BasicAuthMiddlewareTest(unittest.TestCase):
def test401garbage(self):
req = self.req.copy()
- req.setheader('http-authorization', 'Garbage')
+ req.setheader('Authorization', 'Garbage')
res = req(self.app)
res.status(401)
res.header("WWW-Authenticate", lambda _: True)
def test401basicgarbage(self):
req = self.req.copy()
- req.setheader('http-authorization', 'Basic ()')
+ req.setheader('Authorization', 'Basic ()')
res = req(self.app)
res.status(401)
res.header("WWW-Authenticate", lambda _: True)
@@ -324,7 +360,7 @@ class BasicAuthMiddlewareTest(unittest.TestCase):
req = self.req.copy()
token = "bar:%s" % password
token = bytes2str(base64.b64encode(str2bytes(token)))
- req.setheader('http-authorization', 'Basic %s' % token)
+ req.setheader('Authorization', 'Basic %s' % token)
res = req(self.app)
res.status(status)
@@ -337,6 +373,22 @@ class BasicAuthMiddlewareTest(unittest.TestCase):
from wsgitools import filters
import gzip
+class RequestLogWSGIFilterTest(unittest.TestCase):
+ def testSimple(self):
+ app = applications.StaticContent("200 Found",
+ [("Content-Type", "text/plain")], "nothing")
+ log = io.StringIO()
+ logfilter = filters.RequestLogWSGIFilter.creator(log)
+ app = filters.WSGIFilterMiddleware(app, logfilter)
+ req = Request(self)
+ req.environ["REMOTE_ADDR"] = "1.2.3.4"
+ req.environ["PATH_INFO"] = "/"
+ req.environ["HTTP_USER_AGENT"] = "wsgitools-test"
+ res = req(app)
+ logged = log.getvalue()
+ self.assert_(re.match(r'^1\.2\.3\.4 - - \[[^]]+\] "GET /" '
+ r'200 7 - "wsgitools-test"', logged))
+
class GzipWSGIFilterTest(unittest.TestCase):
def testSimple(self):
app = applications.StaticContent("200 Found",
@@ -357,8 +409,10 @@ fullsuite.addTest(alltests(StaticContentTest))
fullsuite.addTest(alltests(StaticFileTest))
fullsuite.addTest(alltests(AuthDigestMiddlewareTest))
fullsuite.addTest(alltests(ContentLengthMiddlewareTest))
+fullsuite.addTest(alltests(CachingMiddlewareTest))
fullsuite.addTest(alltests(BasicAuthMiddlewareTest))
fullsuite.addTest(alltests(NoWriteCallableMiddlewareTest))
+fullsuite.addTest(alltests(RequestLogWSGIFilterTest))
fullsuite.addTest(alltests(GzipWSGIFilterTest))
if __name__ == "__main__":
diff --git a/wsgitools/scgi/forkpool.py b/wsgitools/scgi/forkpool.py
index 88f64be..150ed44 100644
--- a/wsgitools/scgi/forkpool.py
+++ b/wsgitools/scgi/forkpool.py
@@ -5,6 +5,10 @@ It works with multiple processes that are periodically cleaned up to prevent
memory leaks having an impact to the system.
"""
+try:
+ import resource
+except ImportError:
+ resource = None
import socket
import os
import select
@@ -181,14 +185,14 @@ class SCGIServer:
def __init__(self, wsgiapp, port, interface="localhost", error=sys.stderr,
minworkers=2, maxworkers=32, maxrequests=1000, config={},
- reusesocket=None):
+ reusesocket=None, cpulimit=None, timelimit=None):
"""
@param wsgiapp: is the WSGI application to be run.
@type port: int
@param port: is the tcp port to listen on
@type interface: str
@param interface: is the interface to bind to (default: C{"localhost"})
- @param error: is a file-like object beeing passed as C{wsgi.error} in
+ @param error: is a file-like object beeing passed as C{wsgi.errors} in
environ
@type minworkers: int
@param minworkers: is the number of worker processes to spawn
@@ -206,17 +210,31 @@ class SCGIServer:
Instead use given socket as listen socket. The passed socket
must be set up for accepting tcp connections (i.e. C{AF_INET},
C{SOCK_STREAM} with bind and listen called).
+ @type cpulimit: (int, int)
+ @param cpulimit: a pair of soft and hard cpu time limit in seconds.
+ This limit is installed for each worker using RLIMIT_CPU if
+ resource limits are available to this platform. After reaching
+ the soft limit workers will continue to process the current
+ request and then cleanly terminate.
+ @type timelimit: int
+ @param timelimit: The maximum number of wall clock seconds processing
+ a request should take. If this is specified, an alarm timer is
+ installed and the default action is to kill the worker.
"""
assert hasattr(error, "write")
self.wsgiapp = wsgiapp
- self.port = port
- self.interface = interface
+ self.bind_address = (interface, port)
self.minworkers = minworkers
self.maxworkers = maxworkers
self.maxrequests = maxrequests
- self.config = config
- self.error = error
+ self.config = config.copy()
+ self.config["wsgi.errors"] = error
self.reusesocket = reusesocket
+ # cpulimit changes meaning:
+ # master: None or a tuple denoting the limit to be configured.
+ # worker: boolean denoting whether the limit is reached.
+ self.cpulimit = cpulimit
+ self.timelimit = timelimit
self.server = None # becomes a socket
# maps filedescriptors to WorkerStates
self.workers = {}
@@ -240,7 +258,7 @@ class SCGIServer:
if self.reusesocket is None:
self.server = socket.socket()
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- self.server.bind((self.interface, self.port))
+ self.server.bind(self.bind_address)
self.server.listen(5)
else:
self.server = self.reusesocket
@@ -306,6 +324,15 @@ class SCGIServer:
else:
self.running = False
+ def sigxcpuhandler(self, sig=None, stackframe=None):
+ """
+ Signal hanlder function for the SIGXCUP signal. It is sent to a
+ worker when the soft RLIMIT_CPU is crossed.
+ @param sig: ignored for usage with signal.signal
+ @param stackframe: ignored for usage with signal.signal
+ """
+ self.cpulimit = True
+
def spawnworker(self):
"""
internal! spawns a single worker
@@ -321,6 +348,11 @@ class SCGIServer:
worker.sock.close()
del self.workers
+ if self.cpulimit and resource:
+ signal.signal(signal.SIGXCPU, self.sigxcpuhandler)
+ resource.setrlimit(resource.RLIMIT_CPU, self.cpulimit)
+ self.cpulimit = False
+
try:
self.work(worksock)
except socket.error:
@@ -345,8 +377,14 @@ class SCGIServer:
(con, addr) = self.server.accept()
# we cannot handle socket.errors here.
worksock.sendall(b'1') # tell server we're working
+ if self.timelimit:
+ signal.alarm(self.timelimit)
self.process(con)
+ if self.timelimit:
+ signal.alarm(0)
worksock.sendall(b'0') # tell server we've finished
+ if self.cpulimit:
+ break
def process(self, con):
"""
@@ -440,7 +478,6 @@ class SCGIServer:
_convert_environ(environ, multiprocess=True)
sfw = SocketFileWrapper(con, int(environ["CONTENT_LENGTH"]))
environ["wsgi.input"] = sfw
- environ["wsgi.errors"] = self.error
result = self.wsgiapp(environ, start_response)
assert hasattr(result, "__iter__")
@@ -451,9 +488,9 @@ class SCGIServer:
while sent > 0:
sent = result.transfer(con)
else:
- assert response_head[0] is not None
result_iter = iter(result)
for data in result_iter:
+ assert response_head[0] is not None
assert isinstance(data, bytes)
dumbsend(data)
if response_head[0] != True: