Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/branches/cherrypy-2.1/cherrypy/_cpwsgiserver.py

Revision 744 (checked in by rdelon, 3 years ago)

Preparing for 2.1.0 release

Line 
1 """
2 Copyright (c) 2005, CherryPy Team (team@cherrypy.org)
3 All rights reserved.
4
5 Redistribution and use in source and binary forms, with or without modification,
6 are permitted provided that the following conditions are met:
7
8     * Redistributions of source code must retain the above copyright notice,
9       this list of conditions and the following disclaimer.
10     * Redistributions in binary form must reproduce the above copyright notice,
11       this list of conditions and the following disclaimer in the documentation
12       and/or other materials provided with the distribution.
13     * Neither the name of the CherryPy Team nor the names of its contributors
14       may be used to endorse or promote products derived from this software
15       without specific prior written permission.
16
17 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
21 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 """
28
29 """
30 A high-speed, production ready, thread pooled, generic WSGI server.
31 """
32
33 import socket
34 import threading
35 import Queue
36 import mimetools # todo: use email
37 import sys
38 import time
39 import traceback
40
41 weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
42 monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
43                    'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
44
45 class MaxSizeExceeded(Exception):
46     pass
47
48 class SizeCheckWrapper(object):
49     """ Wrapper around the rfile object. For each data reading method,
50         it reads the data but it checks that the size of the data doesn't
51         exceed a certain limit
52     """
53     def __init__(self, rfile, maxlen):
54         self.rfile = rfile
55         self.maxlen = maxlen
56         self.bytes_read = 0
57     def _check_length(self):
58         if self.maxlen and self.bytes_read > self.maxlen:
59             raise MaxSizeExceeded()
60     def read(self, size = None):
61         data = self.rfile.read(size)
62         self.bytes_read += len(data)
63         self._check_length()
64         return data
65     def readline(self, size = None):
66         if size is not None:
67             data = self.rfile.readline(size)
68             self.bytes_read += len(data)
69             self._check_length()
70             return data
71
72         # User didn't specify a size ...
73         # We read the line in chunks to make sure it's not a 100MB line !
74         res = []
75         while True:
76             data = self.rfile.readline(256)
77             self.bytes_read += len(data)
78             self._check_length()
79             res.append(data)
80             if len(data) < 256:
81                 return ''.join(res)
82     def close(self):
83         self.rfile.close()
84
85     def __iter__(self):
86         return self.rfile
87
88     def next(self):
89         data = self.rfile.next()
90         self.bytes_read += len(data)
91         self._check_length()
92 ##      Normally the next method must raise StopIteration when it
93 ##      fails but CP expects MaxSizeExceeded
94 ##        try:
95 ##            self._check_length()
96 ##        except:
97 ##            raise StopIteration()
98         return data
99
100 class HTTPRequest(object):
101     def __init__(self, socket, addr, server):
102         self.socket = socket
103         self.addr = addr
104         self.server = server
105         self.environ = {}
106         self.ready = False
107         self.started_response = False
108         self.status = ""
109         self.outheaders = []
110         self.outheaderkeys = []
111         self.rfile = self.socket.makefile("r", self.server.bufsize)
112         if self.server.config:
113             mhs = self.server.config.get(
114                 'server.maxRequestHeaderSize',
115                 500 * 1024) # 500KB by default
116             self.rfile = SizeCheckWrapper(self.rfile, mhs)
117         self.wfile = self.socket.makefile("w", self.server.bufsize)
118         self.sent_headers = False
119    
120     def parse_request(self):
121         self.sent_headers = False
122         self.environ = {}
123         self.environ["wsgi.version"] = (1,0)
124         self.environ["wsgi.url_scheme"] = "http"
125         self.environ["wsgi.input"] = self.rfile
126         self.environ["wsgi.errors"] = self.server.stderr
127         self.environ["wsgi.multithread"] = True
128         self.environ["wsgi.multiprocess"] = False
129         self.environ["wsgi.run_once"] = False
130         request_line = self.rfile.readline()
131         if not request_line:
132             self.ready = False
133             return
134         method,path,version = request_line.strip().split(" ", 2)
135         if "?" in path:
136             path, qs = path.split("?", 1)
137         else:
138             qs = ""
139         self.environ["REQUEST_METHOD"] = method
140         if path == "*":
141             self.environ["SCRIPT_NAME"] = path
142         else:
143             self.environ["SCRIPT_NAME"] = path[1:]
144         self.environ["PATH_INFO"] = ""
145         self.environ["QUERY_STRING"] = qs
146         self.environ["SERVER_PROTOCOL"] = version
147         self.environ["SERVER_NAME"] = self.server.server_name
148         self.environ["SERVER_PORT"] = str(self.server.bind_addr[1])
149         # optional values
150         self.environ["REMOTE_HOST"] = self.addr[0]
151         self.environ["REMOTE_ADDR"] = self.addr[0]
152         self.environ["REMOTE_PORT"] = str(self.addr[1])
153         # then all the http headers
154         headers = mimetools.Message(self.rfile)
155         self.environ["CONTENT_TYPE"] = headers.getheader("Content-type", "")
156         self.environ["CONTENT_LENGTH"] = headers.getheader("Content-length", "")
157         for (k, v) in headers.items():
158             envname = "HTTP_" + k.upper().replace("-","_")
159             self.environ[envname] = v
160         self.ready = True
161
162         # Request header is parsed
163         # We prepare the SizeCheckWrapper for the request body
164         if self.server.config:
165             mbs = self.server.config.get(
166                 'server.maxRequestBodySize',
167                 100 * 1024 * 1024, # 100MB by default
168                 path = path)
169             self.rfile.bytes_read = 0
170             self.rfile.maxlen = mbs
171    
172     def start_response(self, status, headers, exc_info = None):
173         if self.started_response:
174             if not exc_info:
175                 assert False, "Already started response"
176             else:
177                 try:
178                     raise exc_info[0], exc_info[1], exc_info[2]
179                 finally:
180                     exc_info = None
181         self.started_response = True
182         self.status = status
183         self.outheaders = headers
184         self.outheaderkeys = [key.lower() for (key,value) in self.outheaders]
185         return self.write
186    
187     def write(self, d):
188         if not self.sent_headers:
189             self.sent_headers = True
190             self.send_headers()
191         self.wfile.write(d)
192         self.wfile.flush()
193    
194     def send_headers(self):
195         if "content-length" not in self.outheaderkeys:
196             self.close_at_end = True
197         if "date" not in self.outheaderkeys:
198             # HTTP 1.1 mandates date output in RFC 1123 format.
199             year, month, day, hh, mm, ss, wd, y, z = time.gmtime()
200             dt = ("%s, %02d %3s %4d %02d:%02d:%02d GMT" %
201                   (weekdayname[wd], day, monthname[month], year, hh, mm, ss))
202             self.outheaders.append(("Date", dt))
203         if "server" not in self.outheaderkeys:
204             self.outheaders.append(("Server", self.server.version))
205         if (self.environ["SERVER_PROTOCOL"] == "HTTP/1.1"
206             and "connection" not in self.outheaderkeys):
207             self.outheaders.append(("Connection", "close"))
208         self.wfile.write(self.environ["SERVER_PROTOCOL"] + " " + self.status + "\r\n")
209         for (k,v) in self.outheaders:
210             self.wfile.write(k + ": " + v + "\r\n")
211         self.wfile.write("\r\n")
212         self.wfile.flush()
213    
214     def terminate(self):
215         if self.ready and not self.sent_headers and not self.server.interrupt:
216             self.sent_headers = True
217             self.send_headers()
218         self.rfile.close()
219         self.wfile.close()
220         self.socket.close()
221
222
223 _SHUTDOWNREQUEST = None
224
225 class WorkerThread(threading.Thread):
226    
227     def __init__(self, server):
228         self.ready = False
229         self.server = server
230         threading.Thread.__init__(self)
231    
232     def run(self):
233         try:
234             self.ready = True
235             while True:
236                 request = self.server.requests.get()
237                 if request is _SHUTDOWNREQUEST:
238                     return
239                
240                 try:
241                     try:
242                         request.parse_request()
243                         if request.ready:
244                             response = self.server.wsgi_app(request.environ,
245                                                             request.start_response)
246                             for line in response:
247                                 request.write(line)
248                     except socket.error, e:
249                         errno = e.args[0]
250                         if errno in (32, 104, 10053, 10054):
251                             # Client probably closed the connection before the
252                             # response was sent.
253                             pass
254                         else:
255                             raise
256                     except MaxSizeExceeded:
257                         str = "Request Entity Too Large"
258                         proto = request.environ.get("SERVER_PROTOCOL", "HTTP/1.0")
259                         request.wfile.write("%s 413 %s\r\n" % (proto, str))
260                         request.wfile.write("Content-Length: %s\r\n\r\n" % len(str))
261                         request.wfile.write(str)
262                         request.wfile.flush()
263                     except (KeyboardInterrupt, SystemExit), exc:
264                         self.server.interrupt = exc
265                     except:
266                         traceback.print_exc()
267                 finally:
268                     request.terminate()
269         except (KeyboardInterrupt, SystemExit), exc:
270             self.server.interrupt = exc
271
272
273 class CherryPyWSGIServer(object):
274    
275     version = "CherryPy/2.1.0"
276     ready = False
277     interrupt = None
278    
279     def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
280                  stderr=sys.stderr, bufsize=-1, max=-1,
281                  config = None):
282         '''
283         be careful w/ max
284         '''
285         self.requests = Queue.Queue(max)
286         self.wsgi_app = wsgi_app
287         self.bind_addr = bind_addr
288         self.numthreads = numthreads or 1
289         self.config = config
290         if server_name:
291             self.server_name = server_name
292         else:
293             self.server_name = socket.gethostname()
294         self.stderr = stderr
295         self.bufsize = bufsize
296         self._workerThreads = []
297    
298     def start(self):
299         '''
300         run the server forever
301         '''
302         # We don't have to trap KeyboardInterrupt or SystemExit here,
303         # because cherrpy.server already does so, calling self.stop() for us.
304         # If you're using this server with another framework, you should
305         # trap those exceptions in whatever code block calls start().
306         self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
307         self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
308         self.socket.bind(self.bind_addr)
309         # Timeout so KeyboardInterrupt can be caught on Win32
310         self.socket.settimeout(1)
311         self.socket.listen(5)
312        
313         # Create worker threads
314         for i in xrange(self.numthreads):
315             self._workerThreads.append(WorkerThread(self))
316         for worker in self._workerThreads:
317             worker.start()
318         for worker in self._workerThreads:
319             while not worker.ready:
320                 time.sleep(.1)
321        
322         self.ready = True
323         while self.ready:
324             self.tick()
325             if self.interrupt:
326                 raise self.interrupt
327    
328     def tick(self):
329         try:
330             s, addr = self.socket.accept()
331             if hasattr(s, 'setblocking'):
332                 s.setblocking(1)
333             request = HTTPRequest(s, addr, self)
334             self.requests.put(request)
335             # optimized version follows
336             #self.requests.put(HTTPRequest(*self.socket.accept()))
337         except socket.timeout:
338             # The only reason for the timeout in start() is so we can
339             # notice keyboard interrupts on Win32, which don't interrupt
340             # accept() by default
341             return
342    
343     def stop(self):
344         """Gracefully shutdown a server that is serving forever."""
345         self.ready = False
346         self.socket.close()
347        
348         # Must shut down threads here so the code that calls
349         # this method can know when all threads are stopped.
350         for worker in self._workerThreads:
351             self.requests.put(_SHUTDOWNREQUEST)
352        
353         # Don't join currentThread (when stop is called inside a request).
354         current = threading.currentThread()
355         for worker in self._workerThreads:
356             if worker is not current and worker.isAlive:
357                 worker.join()
358        
359         self._workerThreads = []
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets