Back to index

moin  1.9.0~rc2
serving.py
Go to the documentation of this file.
00001 # -*- coding: utf-8 -*-
00002 """
00003     werkzeug.serving
00004     ~~~~~~~~~~~~~~~~
00005 
00006     There are many ways to serve a WSGI application.  While you're developing
00007     it you usually don't want a full blown webserver like Apache but a simple
00008     standalone one.  From Python 2.5 onwards there is the `wsgiref`_ server in
00009     the standard library.  If you're using older versions of Python you can
00010     download the package from the cheeseshop.
00011 
00012     However there are some caveats. Sourcecode won't reload itself when
00013     changed and each time you kill the server using ``^C`` you get an
00014     `KeyboardInterrupt` error.  While the latter is easy to solve the first
00015     one can be a pain in the ass in some situations.
00016 
00017     Because of that Werkzeug ships a small wrapper over `wsgiref` that spawns
00018     the WSGI application in a subprocess and automatically reloads the
00019     application if a module was changed.
00020 
00021     The easiest way is creating a small ``start-myproject.py`` that runs the
00022     application::
00023 
00024         #!/usr/bin/env python
00025         # -*- coding: utf-8 -*-
00026         from myproject import make_app
00027         from werkzeug import run_simple
00028 
00029         app = make_app(...)
00030         run_simple('localhost', 8080, app, use_reloader=True)
00031 
00032     You can also pass it a `extra_files` keyword argument with a list of
00033     additional files (like configuration files) you want to observe.
00034 
00035     For bigger applications you should consider using `werkzeug.script`
00036     instead of a simple start file.
00037 
00038     .. _wsgiref: http://cheeseshop.python.org/pypi/wsgiref
00039 
00040 
00041     :copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
00042     :license: BSD, see LICENSE for more details.
00043 """
00044 import os
00045 import socket
00046 import sys
00047 import time
00048 import thread
00049 import subprocess
00050 from urllib import unquote
00051 from urlparse import urlparse
00052 from itertools import chain
00053 from SocketServer import ThreadingMixIn, ForkingMixIn
00054 from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
00055 
00056 from werkzeug import __version__ as version
00057 from werkzeug._internal import _log
00058 from werkzeug.utils import responder
00059 from werkzeug.exceptions import InternalServerError
00060 
00061 
00062 class BaseRequestHandler(BaseHTTPRequestHandler, object):
00063     server_version = 'Werkzeug/' + version
00064 
00065     def make_environ(self):
00066         path_info, query = urlparse(self.path)[2::2]
00067         environ = {
00068             'wsgi.version':         (1, 0),
00069             'wsgi.url_scheme':      'http',
00070             'wsgi.input':           self.rfile,
00071             'wsgi.errors':          sys.stderr,
00072             'wsgi.multithread':     self.server.multithread,
00073             'wsgi.multiprocess':    self.server.multiprocess,
00074             'wsgi.run_once':        False,
00075             'REQUEST_METHOD':       self.command,
00076             'SCRIPT_NAME':          '',
00077             'PATH_INFO':            unquote(path_info),
00078             'QUERY_STRING':         query,
00079             'CONTENT_TYPE':         self.headers.get('Content-Type', ''),
00080             'CONTENT_LENGTH':       self.headers.get('Content-Length', ''),
00081             'REMOTE_ADDR':          self.client_address[0],
00082             'REMOTE_PORT':          self.client_address[1],
00083             'SERVER_NAME':          self.server.server_address[0],
00084             'SERVER_PORT':          str(self.server.server_address[1]),
00085             'SERVER_PROTOCOL':      self.request_version
00086         }
00087 
00088         for key, value in self.headers.items():
00089             key = 'HTTP_' + key.upper().replace('-', '_')
00090             if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'):
00091                 environ[key] = value
00092 
00093         return environ
00094 
00095     def run_wsgi(self):
00096         app = self.server.app
00097         environ = self.make_environ()
00098         headers_set = []
00099         headers_sent = []
00100 
00101         def write(data):
00102             assert headers_set, 'write() before start_response'
00103             if not headers_sent:
00104                 status, response_headers = headers_sent[:] = headers_set
00105                 code, msg = status.split(None, 1)
00106                 self.send_response(int(code), msg)
00107                 header_keys = set()
00108                 for key, value in response_headers:
00109                     self.send_header(key, value)
00110                     key = key.lower()
00111                     header_keys.add(key)
00112                 if 'content-length' not in header_keys:
00113                     self.close_connection = True
00114                     self.send_header('Connection', 'close')
00115                 if 'server' not in header_keys:
00116                     self.send_header('Server', self.version_string())
00117                 if 'date' not in header_keys:
00118                     self.send_header('Date', self.date_time_string())
00119                 self.end_headers()
00120 
00121             assert type(data) is str, 'applications must write bytes'
00122             self.wfile.write(data)
00123             self.wfile.flush()
00124 
00125         def start_response(status, response_headers, exc_info=None):
00126             if exc_info:
00127                 try:
00128                     if headers_sent:
00129                         raise exc_info[0], exc_info[1], exc_info[2]
00130                 finally:
00131                     exc_info = None
00132             elif headers_set:
00133                 raise AssertionError('Headers already set')
00134             headers_set[:] = [status, response_headers]
00135             return write
00136 
00137         def execute(app):
00138             application_iter = app(environ, start_response)
00139             try:
00140                 for data in application_iter:
00141                     write(data)
00142                 # make sure the headers are sent
00143                 if not headers_sent:
00144                     write('')
00145             finally:
00146                 if hasattr(application_iter, 'close'):
00147                     application_iter.close()
00148                 application_iter = None
00149 
00150         try:
00151             execute(app)
00152         except (socket.error, socket.timeout), e:
00153             self.connection_dropped(e, environ)
00154         except:
00155             if self.server.passthrough_errors:
00156                 raise
00157             from werkzeug.debug.tbtools import get_current_traceback
00158             traceback = get_current_traceback(ignore_system_exceptions=True)
00159             try:
00160                 # if we haven't yet sent the headers but they are set
00161                 # we roll back to be able to set them again.
00162                 if not headers_sent:
00163                     del headers_set[:]
00164                 execute(InternalServerError())
00165             except:
00166                 pass
00167             self.server.log('error', 'Error on request:\n%s',
00168                             traceback.plaintext)
00169 
00170     def connection_dropped(self, error, environ):
00171         """Called if the connection was closed by the client.  By default
00172         nothing happens.
00173         """
00174 
00175     def handle_one_request(self):
00176         """Handle a single HTTP request."""
00177         self.raw_requestline = self.rfile.readline()
00178         if not self.raw_requestline:
00179             self.close_connection = 1
00180         elif self.parse_request():
00181             return self.run_wsgi()
00182 
00183     def send_response(self, code, message=None):
00184         """Send the response header and log the response code."""
00185         self.log_request(code)
00186         if message is None:
00187             message = code in self.responses and self.responses[code][0] or ''
00188         if self.request_version != 'HTTP/0.9':
00189             self.wfile.write("%s %d %s\r\n" %
00190                              (self.protocol_version, code, message))
00191 
00192     def version_string(self):
00193         return BaseHTTPRequestHandler.version_string(self).strip()
00194 
00195     def address_string(self):
00196         return self.client_address[0]
00197 
00198 
00199 class BaseWSGIServer(HTTPServer):
00200     multithread = False
00201     multiprocess = False
00202 
00203     def __init__(self, host, port, app, handler=None,
00204                  passthrough_errors=False):
00205         if handler is None:
00206             handler = BaseRequestHandler
00207         HTTPServer.__init__(self, (host, int(port)), handler)
00208         self.app = app
00209         self.passthrough_errors = passthrough_errors
00210 
00211     def log(self, type, message, *args):
00212         _log(type, message, *args)
00213 
00214     def serve_forever(self):
00215         try:
00216             HTTPServer.serve_forever(self)
00217         except KeyboardInterrupt:
00218             pass
00219 
00220     def handle_error(self, request, client_address):
00221         if self.passthrough_errors:
00222             raise
00223         else:
00224             return HTTPServer.handle_error(self, request, client_address)
00225 
00226 
00227 class ThreadedWSGIServer(ThreadingMixIn, BaseWSGIServer):
00228     multithread = True
00229 
00230 
00231 class ForkingWSGIServer(ForkingMixIn, BaseWSGIServer):
00232     multiprocess = True
00233 
00234     def __init__(self, host, port, app, processes=40, handler=None,
00235                  passthrough_errors=False):
00236         BaseWSGIServer.__init__(self, host, port, app, handler,
00237                                 passthrough_errors)
00238         self.max_children = processes
00239 
00240 
00241 def make_server(host, port, app=None, threaded=False, processes=1,
00242                 request_handler=None, passthrough_errors=False):
00243     """Create a new server instance that is either threaded, or forks
00244     or just processes one request after another.
00245     """
00246     if threaded and processes > 1:
00247         raise ValueError("cannot have a multithreaded and "
00248                          "multi process server.")
00249     elif threaded:
00250         return ThreadedWSGIServer(host, port, app, request_handler,
00251                                   passthrough_errors)
00252     elif processes > 1:
00253         return ForkingWSGIServer(host, port, app, processes, request_handler,
00254                                  passthrough_errors)
00255     else:
00256         return BaseWSGIServer(host, port, app, request_handler,
00257                               passthrough_errors)
00258 
00259 
00260 def reloader_loop(extra_files=None, interval=1):
00261     """When this function is run from the main thread, it will force other
00262     threads to exit when any modules currently loaded change.
00263 
00264     Copyright notice.  This function is based on the autoreload.py from
00265     the CherryPy trac which originated from WSGIKit which is now dead.
00266 
00267     :param extra_files: a list of additional files it should watch.
00268     """
00269     def iter_module_files():
00270         for module in sys.modules.values():
00271             filename = getattr(module, '__file__', None)
00272             if filename:
00273                 while not os.path.isfile(filename):
00274                     filename = os.path.dirname(filename)
00275                     if not filename:
00276                         break
00277                 else:
00278                     if filename[-4:] in ('.pyc', '.pyo'):
00279                         filename = filename[:-1]
00280                     yield filename
00281 
00282     mtimes = {}
00283     while 1:
00284         for filename in chain(iter_module_files(), extra_files or ()):
00285             try:
00286                 mtime = os.stat(filename).st_mtime
00287             except OSError:
00288                 continue
00289 
00290             old_time = mtimes.get(filename)
00291             if old_time is None:
00292                 mtimes[filename] = mtime
00293                 continue
00294             elif mtime > old_time:
00295                 _log('info', ' * Detected change in %r, reloading' % filename)
00296                 sys.exit(3)
00297         time.sleep(interval)
00298 
00299 
00300 def restart_with_reloader():
00301     """Spawn a new Python interpreter with the same arguments as this one,
00302     but running the reloader thread.
00303     """
00304     while 1:
00305         _log('info', ' * Restarting with reloader...')
00306         args = [sys.executable] + sys.argv
00307         new_environ = os.environ.copy()
00308         new_environ['WERKZEUG_RUN_MAIN'] = 'true'
00309         exit_code = subprocess.call(args, env=new_environ)
00310         if exit_code != 3:
00311             return exit_code
00312 
00313 
00314 def run_with_reloader(main_func, extra_files=None, interval=1):
00315     """Run the given function in an independent python interpreter."""
00316     if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
00317         thread.start_new_thread(main_func, ())
00318         try:
00319             reloader_loop(extra_files, interval)
00320         except KeyboardInterrupt:
00321             return
00322     try:
00323         sys.exit(restart_with_reloader())
00324     except KeyboardInterrupt:
00325         pass
00326 
00327 
00328 def run_simple(hostname, port, application, use_reloader=False,
00329                use_debugger=False, use_evalex=True,
00330                extra_files=None, reloader_interval=1, threaded=False,
00331                processes=1, request_handler=None, static_files=None,
00332                passthrough_errors=False):
00333     """Start an application using wsgiref and with an optional reloader.  This
00334     wraps `wsgiref` to fix the wrong default reporting of the multithreaded
00335     WSGI variable and adds optional multithreading and fork support.
00336 
00337     .. versionadded:: 0.5
00338        `static_files` was added to simplify serving of static files as well
00339        as `passthrough_errors`.
00340 
00341     :param hostname: The host for the application.  eg: ``'localhost'``
00342     :param port: The port for the server.  eg: ``8080``
00343     :param application: the WSGI application to execute
00344     :param use_reloader: should the server automatically restart the python
00345                          process if modules were changed?
00346     :param use_debugger: should the werkzeug debugging system be used?
00347     :param use_evalex: should the exception evaluation feature be enabled?
00348     :param extra_files: a list of files the reloader should watch
00349                         additionally to the modules.  For example configuration
00350                         files.
00351     :param reloader_interval: the interval for the reloader in seconds.
00352     :param threaded: should the process handle each request in a separate
00353                      thread?
00354     :param processes: number of processes to spawn.
00355     :param request_handler: optional parameter that can be used to replace
00356                             the default one.  You can use this to replace it
00357                             with a different
00358                             :class:`~BaseHTTPServer.BaseHTTPRequestHandler`
00359                             subclass.
00360     :param static_files: a dict of paths for static files.  This works exactly
00361                          like :class:`SharedDataMiddleware`, it's actually
00362                          just wrapping the application in that middleware before
00363                          serving.
00364     :param passthrough_errors: set this to `True` to disable the error catching.
00365                                This means that the server will die on errors but
00366                                it can be useful to hook debuggers in (pdb etc.)
00367     """
00368     if use_debugger:
00369         from werkzeug.debug import DebuggedApplication
00370         application = DebuggedApplication(application, use_evalex)
00371     if static_files:
00372         from werkzeug.utils import SharedDataMiddleware
00373         application = SharedDataMiddleware(application, static_files)
00374 
00375     def inner():
00376         make_server(hostname, port, application, threaded,
00377                     processes, request_handler,
00378                     passthrough_errors).serve_forever()
00379 
00380     if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
00381         display_hostname = hostname or '127.0.0.1'
00382         _log('info', ' * Running on http://%s:%d/', display_hostname, port)
00383     if use_reloader:
00384         # Create and destroy a socket so that any exceptions are raised before
00385         # we spawn a separate Python interpreter and lose this ability.
00386         test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
00387         test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
00388         test_socket.bind((hostname, port))
00389         test_socket.close()
00390         run_with_reloader(inner, extra_files, reloader_interval)
00391     else:
00392         inner()