1#!/usr/bin/env python 2# 3# Copyright 2009 Facebook 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); you may 6# not use this file except in compliance with the License. You may obtain 7# a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14# License for the specific language governing permissions and limitations 15# under the License. 16 17import logging 18import tornado.escape 19import tornado.ioloop 20import tornado.web 21import os.path 22import uuid 23 24from tornado.concurrent import Future 25from tornado import gen 26from tornado.options import define, options, parse_command_line 27 28define("port", default=8888, help="run on the given port", type=int) 29define("debug", default=False, help="run in debug mode") 30 31 32class MessageBuffer(object): 33 def __init__(self): 34 self.waiters = set() 35 self.cache = [] 36 self.cache_size = 200 37 38 def wait_for_messages(self, cursor=None): 39 # Construct a Future to return to our caller. This allows 40 # wait_for_messages to be yielded from a coroutine even though 41 # it is not a coroutine itself. We will set the result of the 42 # Future when results are available. 43 result_future = Future() 44 if cursor: 45 new_count = 0 46 for msg in reversed(self.cache): 47 if msg["id"] == cursor: 48 break 49 new_count += 1 50 if new_count: 51 result_future.set_result(self.cache[-new_count:]) 52 return result_future 53 self.waiters.add(result_future) 54 return result_future 55 56 def cancel_wait(self, future): 57 self.waiters.remove(future) 58 # Set an empty result to unblock any coroutines waiting. 59 future.set_result([]) 60 61 def new_messages(self, messages): 62 logging.info("Sending new message to %r listeners", len(self.waiters)) 63 for future in self.waiters: 64 future.set_result(messages) 65 self.waiters = set() 66 self.cache.extend(messages) 67 if len(self.cache) > self.cache_size: 68 self.cache = self.cache[-self.cache_size:] 69 70 71# Making this a non-singleton is left as an exercise for the reader. 72global_message_buffer = MessageBuffer() 73 74 75class MainHandler(tornado.web.RequestHandler): 76 def get(self): 77 self.render("index.html", messages=global_message_buffer.cache) 78 79 80class MessageNewHandler(tornado.web.RequestHandler): 81 def post(self): 82 message = { 83 "id": str(uuid.uuid4()), 84 "body": self.get_argument("body"), 85 } 86 # to_basestring is necessary for Python 3's json encoder, 87 # which doesn't accept byte strings. 88 message["html"] = tornado.escape.to_basestring( 89 self.render_string("message.html", message=message)) 90 if self.get_argument("next", None): 91 self.redirect(self.get_argument("next")) 92 else: 93 self.write(message) 94 global_message_buffer.new_messages([message]) 95 96 97class MessageUpdatesHandler(tornado.web.RequestHandler): 98 @gen.coroutine 99 def post(self): 100 cursor = self.get_argument("cursor", None) 101 # Save the future returned by wait_for_messages so we can cancel 102 # it in wait_for_messages 103 self.future = global_message_buffer.wait_for_messages(cursor=cursor) 104 messages = yield self.future 105 if self.request.connection.stream.closed(): 106 return 107 self.write(dict(messages=messages)) 108 109 def on_connection_close(self): 110 global_message_buffer.cancel_wait(self.future) 111 112 113def main(): 114 parse_command_line() 115 app = tornado.web.Application( 116 [ 117 (r"/", MainHandler), 118 (r"/a/message/new", MessageNewHandler), 119 (r"/a/message/updates", MessageUpdatesHandler), 120 ], 121 cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__", 122 template_path=os.path.join(os.path.dirname(__file__), "templates"), 123 static_path=os.path.join(os.path.dirname(__file__), "static"), 124 xsrf_cookies=True, 125 debug=options.debug, 126 ) 127 app.listen(options.port) 128 tornado.ioloop.IOLoop.current().start() 129 130 131if __name__ == "__main__": 132 main() 133