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

Changeset 1530

Show
Ignore:
Timestamp:
12/15/06 17:09:19
Author:
fumanchu
Message:

Blurg. Horrible late rewrite of WSGI stack due to missing close calls (one test fails still: IR with HTTP/1.0).

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/cherrypy/_cpwsgi.py

    r1460 r1530  
    77from cherrypy import _cperror, wsgiserver 
    88from cherrypy.lib import http as _http 
     9 
     10 
     11#                            Internal Redirect                            # 
    912 
    1013 
     
    3740     
    3841    def __call__(self, environ, start_response): 
    39         redirections = [] 
    40          
    41         env = environ.copy() 
    42         path = env.get('PATH_INFO', '') 
    43         qs = env.get('QUERY_STRING', '') 
    44          
     42        return IRResponse(self.nextapp, environ, start_response, self.recursive) 
     43 
     44 
     45class IRResponse(object): 
     46     
     47    def __init__(self, nextapp, environ, start_response, recursive): 
     48        self.redirections = [] 
     49        self.recursive = recursive 
     50        self.environ = environ.copy() 
     51        self.nextapp = nextapp 
     52        self.start_response = start_response 
     53        self.setapp() 
     54     
     55    def setapp(self): 
    4556        while True: 
    4657            try: 
    47                 for chunk in self.nextapp(env, start_response): 
    48                     yield chunk 
     58                self.response = self.nextapp(self.environ, self.start_response) 
     59                self.iter_response = iter(self.response) 
    4960                break 
    5061            except _cherrypy.InternalRedirect, ir: 
    51                 if not self.recursive: 
    52                     if ir.path in redirections: 
    53                         raise RuntimeError("InternalRedirector visited the " 
    54                                            "same URL twice: %r" % ir.path) 
    55                     else: 
    56                         # Add the *previous* path_info + qs to redirections. 
    57                         if qs: 
    58                             qs = "?" + qs 
    59                         redirections.append(env.get('SCRIPT_NAME', '') + path + qs) 
    60                  
    61                 # Munge environment and try again. 
    62                 env['REQUEST_METHOD'] = "GET" 
    63                 env['PATH_INFO'] = path = ir.path 
    64                 env['QUERY_STRING'] = qs = ir.query_string 
    65                 env['wsgi.input'] = _StringIO.StringIO() 
    66  
    67  
    68 class CPWSGIApp(object): 
    69     """A WSGI application object for a CherryPy Application. 
    70      
    71     pipeline: a list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a 
    72         constructor that takes an initial, positional 'nextapp' argument, 
    73         plus optional keyword arguments, and returns a WSGI application 
    74         (that takes environ and start_response arguments). The 'name' can 
    75         be any you choose, and will correspond to keys in self.config. 
    76      
    77     head: rather than nest all apps in the pipeline on each call, it's only 
    78         done the first time, and the result is memoized into self.head. Set 
    79         this to None again if you change self.pipeline after calling self. 
    80      
    81     config: a dict whose keys match names listed in the pipeline. Each 
    82         value is a further dict which will be passed to the corresponding 
    83         named WSGI callable (from the pipeline) as keyword arguments. 
    84     """ 
    85      
    86     pipeline = [('iredir', InternalRedirector)] 
    87     head = None 
    88     config = {} 
     62                self.setenv(ir) 
     63     
     64    def setenv(self, ir): 
     65        env = self.environ 
     66        if not self.recursive: 
     67            if ir.path in self.redirections: 
     68                raise RuntimeError("InternalRedirector visited the " 
     69                                   "same URL twice: %r" % ir.path) 
     70            else: 
     71                # Add the *previous* path_info + qs to redirections. 
     72                sn = env.get('SCRIPT_NAME', '') 
     73                path = env.get('PATH_INFO', '') 
     74                qs = env.get('QUERY_STRING', '') 
     75                if qs: 
     76                    qs = "?" + qs 
     77                self.redirections.append(sn + path + qs) 
     78         
     79        # Munge environment and try again. 
     80        env['REQUEST_METHOD'] = "GET" 
     81        env['PATH_INFO'] = ir.path 
     82        env['QUERY_STRING'] = ir.query_string 
     83        env['wsgi.input'] = _StringIO.StringIO() 
     84     
     85    def close(self): 
     86        if hasattr(self.response, "close"): 
     87            self.response.close() 
     88     
     89    def __iter__(self): 
     90        return self 
     91     
     92    def next(self): 
     93        while True: 
     94            try: 
     95                return self.iter_response.next() 
     96            except _cherrypy.InternalRedirect, ir: 
     97                self.setenv(ir) 
     98                self.setapp() 
     99 
     100 
     101 
     102#                           WSGI-to-CP Adapter                           # 
     103 
     104 
     105class AppResponse(object): 
    89106     
    90107    throws = (KeyboardInterrupt, SystemExit, _cherrypy.InternalRedirect) 
     108    request = None 
     109     
     110    def __init__(self, environ, start_response, cpapp): 
     111        try: 
     112            self.request = self.get_engine_request(environ, cpapp) 
     113             
     114            meth = environ['REQUEST_METHOD'] 
     115            path = environ.get('SCRIPT_NAME', '') + environ.get('PATH_INFO', '') 
     116            qs = environ.get('QUERY_STRING', '') 
     117            rproto = environ.get('SERVER_PROTOCOL') 
     118            headers = self.translate_headers(environ) 
     119            rfile = environ['wsgi.input'] 
     120             
     121            response = self.request.run(meth, path, qs, rproto, headers, rfile) 
     122            s, h, b = response.status, response.header_list, response.body 
     123            exc = None 
     124        except self.throws: 
     125            self.close() 
     126            raise 
     127        except: 
     128            if getattr(self.request, "throw_errors", False): 
     129                self.close() 
     130                raise 
     131             
     132            tb = _cperror.format_exc() 
     133            _cherrypy.log(tb) 
     134            if not getattr(self.request, "show_tracebacks", True): 
     135                tb = "" 
     136            s, h, b = _cperror.bare_error(tb) 
     137            exc = _sys.exc_info() 
     138         
     139        self.iter_response = iter(b) 
     140         
     141        try: 
     142            start_response(s, h, exc) 
     143        except self.throws: 
     144            self.close() 
     145            raise 
     146        except: 
     147            if getattr(self.request, "throw_errors", False): 
     148                self.close() 
     149                raise 
     150             
     151            _cherrypy.log(traceback=True) 
     152            self.close() 
     153             
     154            # CherryPy test suite expects bare_error body to be output, 
     155            # so don't call start_response (which, according to PEP 333, 
     156            # may raise its own error at that point). 
     157            s, h, b = _cperror.bare_error() 
     158            self.iter_response = iter(b) 
     159     
     160    def __iter__(self): 
     161        return self 
     162     
     163    def next(self): 
     164        try: 
     165            chunk = self.iter_response.next() 
     166            # WSGI requires all data to be of type "str". This coercion should 
     167            # not take any time at all if chunk is already of type "str". 
     168            # If it's unicode, it could be a big performance hit (x ~500). 
     169            if not isinstance(chunk, str): 
     170                chunk = chunk.encode("ISO-8859-1") 
     171            return chunk 
     172        except self.throws: 
     173            raise 
     174        except StopIteration: 
     175            raise 
     176        except: 
     177            if getattr(self.request, "throw_errors", False): 
     178                raise 
     179             
     180            _cherrypy.log(traceback=True) 
     181             
     182            # CherryPy test suite expects bare_error body to be output, 
     183            # so don't call start_response (which, according to PEP 333, 
     184            # may raise its own error at that point). 
     185            s, h, b = _cperror.bare_error() 
     186            self.iter_response = iter([]) 
     187            return "".join(b) 
     188     
     189    def close(self): 
     190        if hasattr(self.request, "close"): 
     191            try: 
     192                self.request.close() 
     193            except: 
     194                _cherrypy.log(traceback=True) 
     195     
     196    def get_engine_request(self, environ, cpapp): 
     197        """Return a Request object from the CherryPy Engine using environ.""" 
     198        env = environ.get 
     199         
     200        local = _http.Host('', int(env('SERVER_PORT', 80)), 
     201                           env('SERVER_NAME', '')) 
     202        remote = _http.Host(env('REMOTE_ADDR', ''), 
     203                            int(env('REMOTE_PORT', -1)), 
     204                            env('REMOTE_HOST', '')) 
     205        scheme = env('wsgi.url_scheme') 
     206        sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1") 
     207        request = _cherrypy.engine.request(local, remote, scheme, sproto) 
     208         
     209        # LOGON_USER is served by IIS, and is the name of the 
     210        # user after having been mapped to a local account. 
     211        # Both IIS and Apache set REMOTE_USER, when possible. 
     212        request.login = env('LOGON_USER') or env('REMOTE_USER') or None 
     213        request.multithread = environ['wsgi.multithread'] 
     214        request.multiprocess = environ['wsgi.multiprocess'] 
     215        request.wsgi_environ = environ 
     216        request.app = cpapp 
     217        request.prev = env('cherrypy.request') 
     218        environ['cherrypy.request'] = request 
     219        return request 
    91220     
    92221    headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization', 
     
    107236                translatedHeader = cgiName[5:].replace("_", "-") 
    108237                yield translatedHeader, environ[cgiName] 
     238 
     239 
     240class CPWSGIApp(object): 
     241    """A WSGI application object for a CherryPy Application. 
     242     
     243    pipeline: a list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a 
     244        constructor that takes an initial, positional 'nextapp' argument, 
     245        plus optional keyword arguments, and returns a WSGI application 
     246        (that takes environ and start_response arguments). The 'name' can 
     247        be any you choose, and will correspond to keys in self.config. 
     248     
     249    head: rather than nest all apps in the pipeline on each call, it's only 
     250        done the first time, and the result is memoized into self.head. Set 
     251        this to None again if you change self.pipeline after calling self. 
     252     
     253    config: a dict whose keys match names listed in the pipeline. Each 
     254        value is a further dict which will be passed to the corresponding 
     255        named WSGI callable (from the pipeline) as keyword arguments. 
     256    """ 
     257     
     258    pipeline = [('iredir', InternalRedirector)] 
     259    head = None 
     260    config = {} 
    109261     
    110262    def __init__(self, cpapp, pipeline=None): 
     
    115267        self.config = self.config.copy() 
    116268     
    117     def get_request(self, environ): 
    118         env = environ.get 
    119          
    120         local = _http.Host('', int(env('SERVER_PORT', 80)), 
    121                            env('SERVER_NAME', '')) 
    122         remote = _http.Host(env('REMOTE_ADDR', ''), 
    123                             int(env('REMOTE_PORT', -1)), 
    124                             env('REMOTE_HOST', '')) 
    125         scheme = env('wsgi.url_scheme') 
    126         sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1") 
    127         request = _cherrypy.engine.request(local, remote, scheme, sproto) 
    128          
    129         # LOGON_USER is served by IIS, and is the name of the 
    130         # user after having been mapped to a local account. 
    131         # Both IIS and Apache set REMOTE_USER, when possible. 
    132         request.login = env('LOGON_USER') or env('REMOTE_USER') or None 
    133         request.multithread = environ['wsgi.multithread'] 
    134         request.multiprocess = environ['wsgi.multiprocess'] 
    135         request.wsgi_environ = environ 
    136         request.app = self.cpapp 
    137         request.prev = env('cherrypy.request') 
    138         environ['cherrypy.request'] = request 
    139         return request 
     269    response_class = AppResponse 
    140270     
    141271    def tail(self, environ, start_response): 
     
    145275        so that any WSGI middleware in self.pipeline can run first. 
    146276        """ 
    147         request = None 
    148         try: 
    149             request = self.get_request(environ) 
    150              
    151             meth = environ['REQUEST_METHOD'] 
    152             path = environ.get('SCRIPT_NAME', '') + environ.get('PATH_INFO', '') 
    153             qs = environ.get('QUERY_STRING', '') 
    154             rproto = environ.get('SERVER_PROTOCOL') 
    155             headers = self.translate_headers(environ) 
    156             rfile = environ['wsgi.input'] 
    157              
    158             response = request.run(meth, path, qs, rproto, headers, rfile) 
    159             s, h, b = response.status, response.header_list, response.body 
    160             exc = None 
    161         except self.throws, ex: 
    162             self._close_req(request) 
    163             raise ex 
    164         except: 
    165             if request and request.throw_errors: 
    166                 raise 
    167              
    168             tb = _cperror.format_exc() 
    169             _cherrypy.log(tb) 
    170             if request and not request.show_tracebacks: 
    171                 tb = "" 
    172             s, h, b = _cperror.bare_error(tb) 
    173              
    174             exc = _sys.exc_info() 
    175          
    176         try: 
    177             start_response(s, h, exc) 
    178             for chunk in b: 
    179                 # WSGI requires all data to be of type "str". This coercion should 
    180                 # not take any time at all if chunk is already of type "str". 
    181                 # If it's unicode, it could be a big performance hit (x ~500). 
    182                 if not isinstance(chunk, str): 
    183                     chunk = chunk.encode("ISO-8859-1") 
    184                 yield chunk 
    185             self._close_req(request) 
    186         except self.throws, ex: 
    187             self._close_req(request) 
    188             raise ex 
    189         except: 
    190             _cherrypy.log(traceback=True) 
    191             self._close_req(request) 
    192              
    193             # CherryPy test suite expects bare_error body to be output, 
    194             # so don't call start_response (which, according to PEP 333, 
    195             # may raise its own error at that point). 
    196             s, h, b = _cperror.bare_error() 
    197             for chunk in b: 
    198                 if not isinstance(chunk, str): 
    199                     chunk = chunk.encode("ISO-8859-1") 
    200                 yield chunk 
    201      
    202     def _close_req(self, request): 
    203         if hasattr(request, "close"): 
    204             try: 
    205                 request.close() 
    206             except: 
    207                 _cherrypy.log(traceback=True) 
     277        return self.response_class(environ, start_response, self.cpapp) 
    208278     
    209279    def __call__(self, environ, start_response): 
     
    211281        if head is None: 
    212282            # Create and nest the WSGI apps in our pipeline (in reverse order). 
     283            # Then memoize the result in self.head. 
    213284            head = self.tail 
    214285            for name, callable in self.pipeline[::-1]: 
  • trunk/cherrypy/test/test_tools.py

    r1432 r1530  
    159159        def err_in_onstart(self): 
    160160            return "success!" 
     161         
     162        def stream(self): 
     163            for x in xrange(100000000): 
     164                yield str(x) 
     165        stream._cp_config = {'response.stream': True} 
    161166     
    162167     
     
    236241        self.getPage("/demo/userid") 
    237242        self.assertBody("Welcome!") 
     243         
     244        # Test that on_end_request is called even if the client drops. 
     245        self.persistent = True 
     246        try: 
     247            conn = self.HTTP_CONN 
     248            conn.putrequest("GET", "/demo/stream", skip_host=True) 
     249            conn.putheader("Host", self.HOST) 
     250            conn.endheaders() 
     251            # Skip the rest of the request and close the conn. This will 
     252            # cause the server's active socket to error, which *should* 
     253            # result in the request being aborted, and request.close being 
     254            # called all the way up the stack (including WSGI middleware), 
     255            # eventually calling our on_end_request hook. 
     256        finally: 
     257            self.persistent = False 
     258        time.sleep(0.1) 
     259        # Test that the on_end_request hooks was called. 
     260        self.getPage("/demo/ended/9") 
     261        self.assertBody("True") 
    238262     
    239263    def testGuaranteedHooks(self): 
  • trunk/cherrypy/wsgiserver.py

    r1528 r1530  
    280280        response = self.wsgi_app(self.environ, self.start_response) 
    281281        try: 
    282             for line in response: 
     282            for chunk in response: 
    283283                if not self.sent_headers: 
    284284                    self.sent_headers = True 
    285285                    self.send_headers() 
    286286                if self.chunked_write: 
    287                     wfile.write(hex(len(line))[2:]) 
     287                    wfile.write(hex(len(chunk))[2:]) 
    288288                    wfile.write("\r\n") 
    289                     wfile.write(line
     289                    wfile.write(chunk
    290290                    wfile.write("\r\n") 
    291291                else: 
    292                     wfile.write(line
     292                    wfile.write(chunk
    293293                wfile.flush() 
    294294        finally: 
     
    669669                # Despite the socket module docs, using '' does not 
    670670                # allow AI_PASSIVE to work. Passing None instead 
    671                 # returns '0.0.0.0' like we want. 
     671                # returns '0.0.0.0' like we want. In other words: 
     672                #     host    AI_PASSIVE     result 
     673                #      ''         Y         192.168.x.y 
     674                #      ''         N         192.168.x.y 
     675                #     None        Y         0.0.0.0 
     676                #     None        N         127.0.0.1 
    672677                host = None 
    673678                flags = socket.AI_PASSIVE 

Hosted by WebFaction

Log in as guest/cpguest to create tickets