Permalink
Browse files

Hacks to make Tornado work work with web.py and wsgi applications.

  • Loading branch information...
0 parents commit f69cf16f0be8ac7f9080f7bbcbbbf456be68760b @anandology committed Sep 15, 2009
7 Readme
@@ -0,0 +1,7 @@
+
+# Tornado Hacks
+
+This is an experimental module trying to make Tornado work work with web.py and wsgi applications.
+This module attempts to make use of Tornado's asynchronous features in web.py applications.
+
+
@@ -0,0 +1,88 @@
+"""Tornado chat demo adopted for web.py + tornado.
+
+Authentication has been removed for simplicity.
+
+Start the server and go to http://0.0.0.0:8080/joe to become user joe.
+"""
+import web
+import uuid
+import simplejson
+import logging
+
+from tornadohacks.webpy import asynchronous, async_callback, tornadorun
+
+urls = (
+ '/(\w+)', 'index',
+ '/(\w+)/new', 'new',
+ '/(\w+)/updates', 'update'
+)
+app = web.application(urls, globals())
+
+_globals = {}
+render = web.template.render('templates', globals=_globals)
+_globals['render'] = render
+
+class MessageMixin(object):
+ waiters = []
+ cache = []
+ cache_size = 200
+
+ def wait_for_messages(self, callback, cursor=None):
+ cls = MessageMixin
+ if cursor:
+ index = 0
+ for i in xrange(len(cls.cache)):
+ index = len(cls.cache) - i - 1
+ if cls.cache[index]["id"] == cursor: break
+ recent = cls.cache[index + 1:]
+ if recent:
+ callback(recent)
+ return
+ cls.waiters.append(callback)
+
+ def new_messages(self, messages):
+ cls = MessageMixin
+ logging.info("Sending new message to %r listeners", len(cls.waiters))
+ for callback in cls.waiters:
+ try:
+ callback(messages)
+ except:
+ logging.error("Error in waiter callback", exc_info=True)
+ cls.waiters = []
+ cls.cache.extend(messages)
+ if len(cls.cache) > self.cache_size:
+ cls.cache = cls.cache[-self.cache_size:]
+
+class index:
+ def GET(self, username):
+ return render.index(username, MessageMixin.cache)
+
+class new(MessageMixin):
+ def POST(self, username):
+ i = web.input(body="", next=None)
+ message = {
+ "id": str(uuid.uuid4()),
+ "from": username,
+ "body": i.body,
+ }
+ message["html"] = web.safestr(render.message(message))
+
+ self.new_messages([message])
+ if i.next:
+ web.ctx.status = "303 See Other"
+ web.header("Location", i.next)
+ else:
+ return simplejson.dumps(message)
+
+class update(MessageMixin):
+ @asynchronous
+ def POST(self, username):
+ i = web.input(cursor=None)
+ self.wait_for_messages(async_callback(self.on_new_messages), cursor=i.cursor)
+
+ def on_new_messages(self, messages):
+ web.ctx.response.write(simplejson.dumps(dict(messages=messages)))
+ web.ctx.response.finish()
+
+if __name__ == '__main__':
+ tornadorun(app, 8080)
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2009 FriendFeed
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+body {
+ background: white;
+ margin: 10px;
+}
+
+body,
+input {
+ font-family: sans-serif;
+ font-size: 10pt;
+ color: black;
+}
+
+table {
+ border-collapse: collapse;
+ border: 0;
+}
+
+td {
+ border: 0;
+ padding: 0;
+}
+
+#body {
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+}
+
+#input {
+ margin-top: 0.5em;
+}
+
+#inbox .message {
+ padding-top: 0.25em;
+}
+
+#nav {
+ float: right;
+ z-index: 99;
+}
@@ -0,0 +1,138 @@
+// Copyright 2009 FriendFeed
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+$(document).ready(function() {
+ if (!window.console) window.console = {};
+ if (!window.console.log) window.console.log = function() {};
+
+ $("#messageform").live("submit", function() {
+ newMessage($(this));
+ return false;
+ });
+ $("#messageform").live("keypress", function(e) {
+ if (e.keyCode == 13) {
+ newMessage($(this));
+ return false;
+ }
+ });
+ $("#message").select();
+ updater.poll();
+});
+
+function get_username() {
+ return document.location.pathname.split('/')[1];
+}
+
+function newMessage(form) {
+ var message = form.formToDict();
+ var disabled = form.find("input[type=submit]");
+ disabled.disable();
+ $.postJSON("/" + get_username() + "/new", message, function(response) {
+ updater.showMessage(response);
+ if (message.id) {
+ form.parent().remove();
+ } else {
+ form.find("input[type=text]").val("").select();
+ disabled.enable();
+ }
+ });
+}
+
+function getCookie(name) {
+ var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
+ return r ? r[1] : undefined;
+}
+
+jQuery.postJSON = function(url, args, callback) {
+ $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST",
+ success: function(response) {
+ if (callback) callback(eval("(" + response + ")"));
+ }, error: function(response) {
+ console.log("ERROR:", response)
+ }});
+};
+
+jQuery.fn.formToDict = function() {
+ var fields = this.serializeArray();
+ var json = {}
+ for (var i = 0; i < fields.length; i++) {
+ json[fields[i].name] = fields[i].value;
+ }
+ if (json.next) delete json.next;
+ return json;
+};
+
+jQuery.fn.disable = function() {
+ this.enable(false);
+ return this;
+};
+
+jQuery.fn.enable = function(opt_enable) {
+ if (arguments.length && !opt_enable) {
+ this.attr("disabled", "disabled");
+ } else {
+ this.removeAttr("disabled");
+ }
+ return this;
+};
+
+var updater = {
+ errorSleepTime: 500,
+ cursor: null,
+
+ poll: function() {
+ var args = {};
+ if (updater.cursor) args.cursor = updater.cursor;
+ $.ajax({url: "/" + get_username() + "/updates", type: "POST", dataType: "text",
+ data: $.param(args), success: updater.onSuccess,
+ error: updater.onError});
+ },
+
+ onSuccess: function(response) {
+ try {
+ updater.newMessages(eval("(" + response + ")"));
+ } catch (e) {
+ updater.onError();
+ return;
+ }
+ updater.errorSleepTime = 500;
+ window.setTimeout(updater.poll, 0);
+ },
+
+ onError: function(response) {
+ updater.errorSleepTime *= 2;
+ console.log("Poll error; sleeping for", updater.errorSleepTime, "ms");
+ window.setTimeout(updater.poll, updater.errorSleepTime);
+ },
+
+ newMessages: function(response) {
+ if (!response.messages) return;
+ updater.cursor = response.cursor;
+ var messages = response.messages;
+ updater.cursor = messages[messages.length - 1].id;
+ console.log(messages.length, "new messages, cursor:", updater.cursor);
+ for (var i = 0; i < messages.length; i++) {
+ updater.showMessage(messages[i]);
+ }
+ },
+
+ showMessage: function(message) {
+ var existing = $("#m" + message.id);
+ if (existing.length > 0) return;
+ var node = $(message.html);
+ node.hide();
+ $("#inbox").append(node);
+ node.slideDown();
+ }
+};
@@ -0,0 +1,36 @@
+$def with (username, messages)
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>Tornado Chat Demo</title>
+ <link rel="stylesheet" href="/static/chat.css" type="text/css"/>
+ </head>
+ <body>
+ <div id="nav">
+ <b>$username</b>
+ </div>
+ <div id="body">
+ <div id="inbox">
+ $for message in messages:
+ $:render.message(message)
+ </div>
+ <div id="input">
+ <form action="/$username/new" method="post" id="messageform">
+ <table>
+ <tr>
+ <td><input name="body" id="message" style="width:500px"/></td>
+ <td style="padding-left:5px">
+ <input type="hidden" name="next" value="/$username"/>
+
+ <input type="submit" value="Post"/>
+ </td>
+ </tr>
+ </table>
+ </form>
+ </div>
+ </div>
+ <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js" type="text/javascript"></script>
+ <script src="/static/chat.js" type="text/javascript"></script>
+ </body>
+</html>
@@ -0,0 +1,2 @@
+$def with (message)
+<div class="message" id="m$message["id"]"><b>$message["from"]: </b>$message["body"]</div>
@@ -0,0 +1,3 @@
+"""Hacks to make Tornado work work with web.py and wsgi applications.
+"""
+
@@ -0,0 +1,46 @@
+"""Patch Tornado ioloop to support SocketThreads.
+"""
+import tornado.ioloop
+
+def patch_tornado():
+ """Patch Tornado ioloop to support SocketThreads.
+ """
+ tornado.ioloop.IOLoop._instance = IOLoop()
+
+class SocketThread:
+ """In Tornado http server, the stream of execution are associated with sockets.
+ This class provides thread like interface for those streams of execution.
+ """
+ def __init__(self, fd, parent):
+ self.fd = fd
+ self.parent = parent
+ self.local = None
+
+ def get_local(self):
+ if self.local is not None:
+ return self.local
+ else:
+ return self.parent and self.parent.get_local()
+
+class IOLoop(tornado.ioloop.IOLoop):
+ """IOLoop extension to support SocketThreads."""
+ def __init__(self, impl=None):
+ self.threads = {}
+ self._current_thread = None
+ tornado.ioloop.IOLoop.__init__(self, impl)
+
+ def add_handler(self, fd, handler, events):
+ def xhandler(_fd, _events):
+ self._current_thread = self.threads[_fd]
+ return handler(_fd, _events)
+
+ self.threads[fd] = SocketThread(fd, self._current_thread)
+ tornado.ioloop.IOLoop.add_handler(self, fd, xhandler, events)
+
+ def remove_handler(self, fd):
+ tornado.ioloop.IOLoop.remove_handler(self, fd)
+ del self.threads[fd]
+
+ def get_current_thread(self):
+ return self._current_thread
+
Oops, something went wrong.

0 comments on commit f69cf16

Please sign in to comment.