SVN: r24299 - trunk/quixote/server

Neil Schemenauer <nascheme-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]> Thu, 20 May 2004 13:05:38 -0400
Newsgroups gmane.comp.web.quixote.cvs
Message-ID <[email protected]>
Author: nascheme
Date: 2004-05-20 13:04:56 -0400 (Thu, 20 May 2004)
New Revision: 24299

Modified:
   trunk/quixote/server/twisted_http.py
Log:
Wrap output in a twisted type producer.  Use twisted reactor instead of
Application.  Thanks to Jason Sibre for the patch.


Modified: trunk/quixote/server/twisted_http.py
===================================================================
--- trunk/quixote/server/twisted_http.py	2004-05-20 15:55:14 UTC (rev 24298)
+++ trunk/quixote/server/twisted_http.py	2004-05-20 17:04:56 UTC (rev 24299)
@@ -13,16 +13,22 @@
 # version 0.2 -- 2003.03.24 11:07 PM
 #   adds missing support for session management, and for
 #   standard Quixote response headers (expires, date)
+#
+# modified 2004/04/10 jsibre
+#   better support for Streams
+#   wraps output (whether Stream or not) into twisted type producer.
+#   modified to use reactor instead of Application (Appication 
+#     has been deprecated)
 
-
 import urllib
-from twisted.internet.app import Application
 from twisted.protocols import http
 from twisted.web import server
 
-import quixote
-quixote.enable_ptl()
-from quixote.publish import Publisher
+# imports for the TWProducer object
+from twisted.spread import pb
+from twisted.python import threadable
+from twisted.internet import abstract
+
 from quixote.http_response import Stream
 
 class QuixoteTWRequest(server.Request):
@@ -30,8 +36,9 @@
     def process(self):
         self.publisher = self.channel.factory.publisher
         environ = self.create_environment()
-        ## this seek is important, it doesn't work without it
-        self.content.seek(0,0)
+        # this seek is important, it doesn't work without it (it doesn't
+        # matter for GETs, but POSTs will not work properly without it.)
+        self.content.seek(0, 0)
         qxrequest = self.publisher.create_request(self.content, environ)
         self.quixote_publish(qxrequest, environ)
         resp = qxrequest.response
@@ -39,8 +46,9 @@
         for hdr, value in resp.generate_headers():
             self.setHeader(hdr, value)
         if resp.body is not None:
-            self.write(resp.body)
-        self.finish()
+            TWProducer(resp.body, self)
+        else:
+            self.finish()
 
 
     def quixote_publish(self, qxrequest, env):
@@ -54,9 +62,6 @@
         # don't write out the output, just set the response body
         # the calling method will do the rest.
         if output:
-            if isinstance(output, Stream):
-                # XXX a Twisted person needs to add support for streams
-                output = ''.join(map(str, output))
             qxrequest.response.set_body(output)
 
         pub._clear_request()
@@ -117,7 +122,110 @@
         return env
 
 
+class TWProducer(pb.Viewable):
+    """
+    A class to represent the transfer of data over the network.
+    
+    JES Note: This has more stuff in it than is minimally neccesary.
+    However, since I'm no twisted guru, I built this by modifing
+    twisted.web.static.FileTransfer.  FileTransfer has stuff in it 
+    that I don't really understand, but know that I probably don't 
+    need. I'm leaving it in under the theory that if anyone ever 
+    needs that stuff (e.g. because they're running with multiple 
+    threads) it'll be MUCH easier for them if I had just left it in
+    than if they have to figure out what needs to be in there.  
+    Furthermore, I notice no performance penalty for leaving it in.
+    """
+    request = None
+    def __init__(self, data, request):
+        self.request = request
+        self.data = ""
+        self.size = 0
+        self.stream = None
+        self.streamIter = None
+    
+        self.outputBufferSize = abstract.FileDescriptor.bufferSize
 
+        if isinstance(data, Stream):    # data could be a Stream
+            self.stream = data
+            self.streamIter = iter(data)
+            self.size = data.length
+        elif data:                      # data could be a string
+            self.data = data
+            self.size = len(data)
+        else:                           # data could be None
+            # We'll just leave self.data as ""
+            pass
+
+        request.registerProducer(self, 0)
+
+
+    def resumeProducing(self):
+        """ 
+        This is twisted's version of a producer's '.more()', or
+        an iterator's '.next()'.  That is, this function is 
+        responsible for returning some content.
+        """
+        if not self.request:
+            return
+
+        if self.stream:
+            # If we were provided a Stream, let's grab some data
+            # and push it into our data buffer
+            
+            buffer = [self.data]
+            bytesInBuffer = len(buffer[-1])
+            while bytesInBuffer < self.outputBufferSize:
+                try:
+                    buffer.append(self.streamIter.next())
+                    bytesInBuffer += len(buffer[-1])
+                except StopIteration:
+                    # We've exhausted the Stream, time to clean up.
+                    self.stream = None
+                    self.streamIter = None
+                    break
+            self.data = "".join(buffer)
+            
+        if self.data:
+            chunkSize = min(self.outputBufferSize, len(self.data))
+            data, self.data = self.data[:chunkSize], self.data[chunkSize:]
+        else:
+            data = ""
+            
+        if data:
+            self.request.write(data)
+        
+        if not self.data:
+            self.request.unregisterProducer()
+            self.request.finish()
+            self.request = None
+
+    def pauseProducing(self):
+        pass
+
+    def stopProducing(self):
+        self.data    = ""
+        self.request = None
+        self.stream  = None
+        self.streamIter = None
+
+    # Remotely relay producer interface.
+
+    def view_resumeProducing(self, issuer):
+        self.resumeProducing()
+
+    def view_pauseProducing(self, issuer):
+        self.pauseProducing()
+
+    def view_stopProducing(self, issuer):
+        self.stopProducing()
+
+    synchronized = ['resumeProducing', 'stopProducing']
+
+threadable.synchronize(TWProducer)
+
+
+
 class QuixoteFactory (http.HTTPFactory):
 
     def __init__(self, publisher):
@@ -125,24 +233,43 @@
         http.HTTPFactory.__init__(self, None)
 
     def buildProtocol (self, addr):
-        h = http.HTTPChannel()
-        h.requestFactory = QuixoteTWRequest
-        h.factory = self
-        return h
+        p = http.HTTPFactory.buildProtocol(self, addr)
+        p.requestFactory = QuixoteTWRequest
+        return p
 
 
-def Server (namespace, port):
-    app = Application('Quixote')
+def Server (namespace, http_port):
+    from twisted.internet import reactor
+    from quixote.publish import Publisher
+
+    #  If you want SSL, make sure you have OpenSSL,
+    #  uncomment the follownig, and uncomment the 
+    #  listenSSL() call below.
+    
+    ##from OpenSSL import SSL
+    ##class ServerContextFactory:
+    ##    def getContext(self):
+    ##        ctx = SSL.Context(SSL.SSLv23_METHOD)
+    ##        ctx.use_certificate_file('/path/to/pem/encoded/ssl_cert_file')
+    ##        ctx.use_privatekey_file('/path/to/pem/encoded/ssl_key_file')
+    ##        return ctx
+
     publisher = Publisher(namespace)
     ##publisher.setup_logs()
     qf = QuixoteFactory(publisher)
-    app.listenTCP(port, qf)
-    return app
 
+    reactor.listenTCP(http_port, qf)
+    ##reactor.listenSSL(http_port, qf, ServerContextFactory())
+
+    return reactor
+
+
 def run (namespace, port):
     app = Server(namespace, port)
-    app.run(save=0)
+    app.run()
 
 
 if __name__ == '__main__':
+    from quixote import enable_ptl
+    enable_ptl()
     run('quixote.demo', 8080)