2015-03-02 08:09:17 +00:00
|
|
|
from __future__ import print_function
|
2015-04-10 03:44:04 +00:00
|
|
|
import re, json, time, random
|
2015-02-11 09:05:11 +00:00
|
|
|
from twisted.python import log
|
2015-10-04 22:49:06 +00:00
|
|
|
from twisted.application import service, internet
|
2015-10-04 19:40:12 +00:00
|
|
|
from twisted.web import server, resource, http
|
2015-02-11 09:05:11 +00:00
|
|
|
|
2015-02-12 02:13:54 +00:00
|
|
|
SECONDS = 1.0
|
2015-03-03 05:22:56 +00:00
|
|
|
MINUTE = 60*SECONDS
|
|
|
|
HOUR = 60*MINUTE
|
2015-05-05 01:13:14 +00:00
|
|
|
DAY = 24*HOUR
|
2015-02-12 02:13:54 +00:00
|
|
|
MB = 1000*1000
|
|
|
|
|
2015-05-05 01:19:40 +00:00
|
|
|
CHANNEL_EXPIRATION_TIME = 3*DAY
|
2015-10-04 22:49:06 +00:00
|
|
|
EXPIRATION_CHECK_PERIOD = 2*HOUR
|
2015-03-03 05:22:56 +00:00
|
|
|
|
2015-03-13 06:07:47 +00:00
|
|
|
class EventsProtocol:
|
|
|
|
def __init__(self, request):
|
|
|
|
self.request = request
|
|
|
|
|
|
|
|
def sendComment(self, comment):
|
|
|
|
# this is ignored by clients, but can keep the connection open in the
|
|
|
|
# face of firewall/NAT timeouts. It also helps unit tests, since
|
|
|
|
# apparently twisted.web.client.Agent doesn't consider the connection
|
|
|
|
# to be established until it sees the first byte of the reponse body.
|
2015-09-28 23:04:54 +00:00
|
|
|
self.request.write(b": " + comment + b"\n\n")
|
2015-03-13 06:07:47 +00:00
|
|
|
|
|
|
|
def sendEvent(self, data, name=None, id=None, retry=None):
|
|
|
|
if name:
|
2015-09-28 23:04:54 +00:00
|
|
|
self.request.write(b"event: " + name.encode("utf-8") + b"\n")
|
2015-03-13 06:07:47 +00:00
|
|
|
# e.g. if name=foo, then the client web page should do:
|
|
|
|
# (new EventSource(url)).addEventListener("foo", handlerfunc)
|
|
|
|
# Note that this basically defaults to "message".
|
2015-09-27 23:55:46 +00:00
|
|
|
self.request.write(b"\n")
|
2015-03-13 06:07:47 +00:00
|
|
|
if id:
|
2015-09-28 23:04:54 +00:00
|
|
|
self.request.write(b"id: " + id.encode("utf-8") + b"\n")
|
2015-09-27 23:55:46 +00:00
|
|
|
self.request.write(b"\n")
|
2015-03-13 06:07:47 +00:00
|
|
|
if retry:
|
2015-09-28 23:04:54 +00:00
|
|
|
self.request.write(b"retry: " + retry + b"\n") # milliseconds
|
2015-09-27 23:55:46 +00:00
|
|
|
self.request.write(b"\n")
|
2015-03-13 06:07:47 +00:00
|
|
|
for line in data.splitlines():
|
2015-09-28 23:04:54 +00:00
|
|
|
self.request.write(b"data: " + line.encode("utf-8") + b"\n")
|
2015-09-27 23:55:46 +00:00
|
|
|
self.request.write(b"\n")
|
2015-03-13 06:07:47 +00:00
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
self.request.finish()
|
|
|
|
|
|
|
|
# note: no versions of IE (including the current IE11) support EventSource
|
|
|
|
|
2015-03-22 18:45:16 +00:00
|
|
|
# relay URLs are:
|
2015-10-06 23:16:41 +00:00
|
|
|
# GET /list -> {channelids: [INT..]}
|
|
|
|
# POST /allocate {side: SIDE} -> {channelid: INT}
|
2015-10-03 19:36:14 +00:00
|
|
|
# these return all messages (base64) for CID= :
|
|
|
|
# POST /CID {side:, phase:, body:} -> {messages: [{phase:, body:}..]}
|
|
|
|
# GET /CID (no-eventsource) -> {messages: [{phase:, body:}..]}
|
|
|
|
# GET /CID (eventsource) -> {phase:, body:}..
|
|
|
|
# POST /CID/deallocate {side: SIDE} -> {status: waiting | deleted}
|
|
|
|
# all JSON responses include a "welcome:{..}" key
|
2015-03-22 18:45:16 +00:00
|
|
|
|
2015-02-11 09:05:11 +00:00
|
|
|
class Channel(resource.Resource):
|
2015-10-06 23:16:41 +00:00
|
|
|
def __init__(self, channelid, relay, db, welcome):
|
2015-02-11 09:05:11 +00:00
|
|
|
resource.Resource.__init__(self)
|
2015-10-06 23:16:41 +00:00
|
|
|
self.channelid = channelid
|
2015-02-11 09:05:11 +00:00
|
|
|
self.relay = relay
|
2015-04-10 16:15:27 +00:00
|
|
|
self.db = db
|
2015-04-21 01:34:13 +00:00
|
|
|
self.welcome = welcome
|
2015-10-03 19:36:14 +00:00
|
|
|
self.event_channels = set() # ep
|
2015-10-06 23:16:41 +00:00
|
|
|
self.putChild(b"deallocate", Deallocator(self.channelid, self.relay))
|
2015-10-03 19:36:14 +00:00
|
|
|
|
|
|
|
def get_messages(self, request):
|
|
|
|
request.setHeader(b"content-type", b"application/json; charset=utf-8")
|
|
|
|
messages = []
|
|
|
|
for row in self.db.execute("SELECT * FROM `messages`"
|
2015-10-06 23:16:41 +00:00
|
|
|
" WHERE `channelid`=?"
|
2015-10-03 19:36:14 +00:00
|
|
|
" ORDER BY `when` ASC",
|
2015-10-06 23:16:41 +00:00
|
|
|
(self.channelid,)).fetchall():
|
2015-10-03 19:36:14 +00:00
|
|
|
messages.append({"phase": row["phase"], "body": row["body"]})
|
|
|
|
data = {"welcome": self.welcome, "messages": messages}
|
|
|
|
return (json.dumps(data)+"\n").encode("utf-8")
|
2015-03-13 06:07:47 +00:00
|
|
|
|
|
|
|
def render_GET(self, request):
|
2015-09-27 23:55:46 +00:00
|
|
|
if b"text/event-stream" not in (request.getHeader(b"accept") or b""):
|
2015-10-03 19:36:14 +00:00
|
|
|
return self.get_messages(request)
|
2015-09-27 23:55:46 +00:00
|
|
|
request.setHeader(b"content-type", b"text/event-stream; charset=utf-8")
|
2015-03-13 06:07:47 +00:00
|
|
|
ep = EventsProtocol(request)
|
2015-04-21 01:34:13 +00:00
|
|
|
ep.sendEvent(json.dumps(self.welcome), name="welcome")
|
2015-10-03 19:36:14 +00:00
|
|
|
self.event_channels.add(ep)
|
|
|
|
request.notifyFinish().addErrback(lambda f:
|
|
|
|
self.event_channels.discard(ep))
|
2015-04-10 17:00:08 +00:00
|
|
|
for row in self.db.execute("SELECT * FROM `messages`"
|
2015-10-06 23:16:41 +00:00
|
|
|
" WHERE `channelid`=?"
|
2015-04-10 17:00:08 +00:00
|
|
|
" ORDER BY `when` ASC",
|
2015-10-06 23:16:41 +00:00
|
|
|
(self.channelid,)).fetchall():
|
2015-10-03 19:36:14 +00:00
|
|
|
data = json.dumps({"phase": row["phase"], "body": row["body"]})
|
|
|
|
ep.sendEvent(data)
|
2015-03-13 06:07:47 +00:00
|
|
|
return server.NOT_DONE_YET
|
|
|
|
|
2015-10-03 19:36:14 +00:00
|
|
|
def broadcast_message(self, phase, body):
|
|
|
|
data = json.dumps({"phase": phase, "body": body})
|
|
|
|
for ep in self.event_channels:
|
|
|
|
ep.sendEvent(data)
|
2015-03-13 06:07:47 +00:00
|
|
|
|
2015-02-11 09:05:11 +00:00
|
|
|
def render_POST(self, request):
|
2015-10-03 19:36:14 +00:00
|
|
|
#data = json.load(request.content, encoding="utf-8")
|
|
|
|
content = request.content.read()
|
|
|
|
data = json.loads(content.decode("utf-8"))
|
|
|
|
|
|
|
|
side = data["side"]
|
|
|
|
phase = data["phase"]
|
|
|
|
if not isinstance(phase, type(u"")):
|
|
|
|
raise TypeError("phase must be string, not %s" % type(phase))
|
|
|
|
body = data["body"]
|
|
|
|
|
|
|
|
self.db.execute("INSERT INTO `messages`"
|
2015-10-06 23:16:41 +00:00
|
|
|
" (`channelid`, `side`, `phase`, `body`, `when`)"
|
2015-10-03 19:36:14 +00:00
|
|
|
" VALUES (?,?,?,?,?)",
|
2015-10-06 23:16:41 +00:00
|
|
|
(self.channelid, side, phase, body, time.time()))
|
2015-10-03 19:36:14 +00:00
|
|
|
self.db.execute("INSERT INTO `allocations`"
|
2015-10-06 23:16:41 +00:00
|
|
|
" (`channelid`, `side`)"
|
2015-10-03 19:36:14 +00:00
|
|
|
" VALUES (?,?)",
|
2015-10-06 23:16:41 +00:00
|
|
|
(self.channelid, side))
|
2015-10-03 19:36:14 +00:00
|
|
|
self.db.commit()
|
|
|
|
self.broadcast_message(phase, body)
|
|
|
|
return self.get_messages(request)
|
|
|
|
|
|
|
|
class Deallocator(resource.Resource):
|
2015-10-06 23:16:41 +00:00
|
|
|
def __init__(self, channelid, relay):
|
|
|
|
self.channelid = channelid
|
2015-10-03 19:36:14 +00:00
|
|
|
self.relay = relay
|
2015-02-11 09:05:11 +00:00
|
|
|
|
2015-10-03 19:36:14 +00:00
|
|
|
def render_POST(self, request):
|
|
|
|
content = request.content.read()
|
|
|
|
data = json.loads(content.decode("utf-8"))
|
|
|
|
side = data["side"]
|
2015-10-06 23:16:41 +00:00
|
|
|
deleted = self.relay.maybe_free_child(self.channelid, side)
|
2015-10-03 19:36:14 +00:00
|
|
|
resp = {"status": "waiting"}
|
|
|
|
if deleted:
|
|
|
|
resp = {"status": "deleted"}
|
|
|
|
return json.dumps(resp).encode("utf-8")
|
2015-02-11 09:05:11 +00:00
|
|
|
|
2015-05-05 01:13:14 +00:00
|
|
|
def get_allocated(db):
|
2015-10-06 23:16:41 +00:00
|
|
|
c = db.execute("SELECT DISTINCT `channelid` FROM `allocations`")
|
|
|
|
return set([row["channelid"] for row in c.fetchall()])
|
2015-05-05 01:13:14 +00:00
|
|
|
|
2015-04-11 03:03:08 +00:00
|
|
|
class Allocator(resource.Resource):
|
2015-05-05 01:13:14 +00:00
|
|
|
def __init__(self, db, welcome):
|
2015-02-11 09:05:11 +00:00
|
|
|
resource.Resource.__init__(self)
|
2015-05-05 01:13:14 +00:00
|
|
|
self.db = db
|
2015-04-21 01:34:13 +00:00
|
|
|
self.welcome = welcome
|
2015-05-05 01:13:14 +00:00
|
|
|
|
2015-10-06 23:16:41 +00:00
|
|
|
def allocate_channelid(self):
|
2015-05-05 01:13:14 +00:00
|
|
|
allocated = get_allocated(self.db)
|
|
|
|
for size in range(1,4): # stick to 1-999 for now
|
|
|
|
available = set()
|
|
|
|
for cid in range(10**(size-1), 10**size):
|
|
|
|
if cid not in allocated:
|
|
|
|
available.add(cid)
|
|
|
|
if available:
|
|
|
|
return random.choice(list(available))
|
|
|
|
# ouch, 999 currently allocated. Try random ones for a while.
|
|
|
|
for tries in range(1000):
|
|
|
|
cid = random.randrange(1000, 1000*1000)
|
|
|
|
if cid not in allocated:
|
|
|
|
return cid
|
2015-10-06 23:16:41 +00:00
|
|
|
raise ValueError("unable to find a free channelid")
|
2015-05-05 01:13:14 +00:00
|
|
|
|
2015-02-11 09:05:11 +00:00
|
|
|
def render_POST(self, request):
|
2015-10-03 19:36:14 +00:00
|
|
|
content = request.content.read()
|
|
|
|
data = json.loads(content.decode("utf-8"))
|
|
|
|
side = data["side"]
|
|
|
|
if not isinstance(side, type(u"")):
|
|
|
|
raise TypeError("side must be string, not '%s'" % type(side))
|
2015-10-06 23:16:41 +00:00
|
|
|
channelid = self.allocate_channelid()
|
2015-05-05 01:13:14 +00:00
|
|
|
self.db.execute("INSERT INTO `allocations` VALUES (?,?)",
|
2015-10-06 23:16:41 +00:00
|
|
|
(channelid, side))
|
2015-05-05 01:13:14 +00:00
|
|
|
self.db.commit()
|
|
|
|
log.msg("allocated #%d, now have %d DB channels" %
|
2015-10-06 23:16:41 +00:00
|
|
|
(channelid, len(get_allocated(self.db))))
|
2015-09-27 23:55:46 +00:00
|
|
|
request.setHeader(b"content-type", b"application/json; charset=utf-8")
|
|
|
|
data = {"welcome": self.welcome,
|
2015-10-06 23:16:41 +00:00
|
|
|
"channelid": channelid}
|
2015-09-27 23:55:46 +00:00
|
|
|
return (json.dumps(data)+"\n").encode("utf-8")
|
2015-02-11 09:05:11 +00:00
|
|
|
|
2015-02-15 02:45:29 +00:00
|
|
|
class ChannelList(resource.Resource):
|
2015-05-05 00:40:57 +00:00
|
|
|
def __init__(self, db, welcome):
|
2015-02-15 02:45:29 +00:00
|
|
|
resource.Resource.__init__(self)
|
2015-05-05 00:40:57 +00:00
|
|
|
self.db = db
|
2015-04-21 01:34:13 +00:00
|
|
|
self.welcome = welcome
|
2015-02-15 02:45:29 +00:00
|
|
|
def render_GET(self, request):
|
2015-10-06 23:16:41 +00:00
|
|
|
c = self.db.execute("SELECT DISTINCT `channelid` FROM `allocations`")
|
|
|
|
allocated = sorted(set([row["channelid"] for row in c.fetchall()]))
|
2015-09-27 23:55:46 +00:00
|
|
|
request.setHeader(b"content-type", b"application/json; charset=utf-8")
|
|
|
|
data = {"welcome": self.welcome,
|
2015-10-06 23:16:41 +00:00
|
|
|
"channelids": allocated}
|
2015-09-27 23:55:46 +00:00
|
|
|
return (json.dumps(data)+"\n").encode("utf-8")
|
2015-02-11 09:05:11 +00:00
|
|
|
|
2015-10-04 22:49:06 +00:00
|
|
|
class Relay(resource.Resource, service.MultiService):
|
2015-04-10 16:15:27 +00:00
|
|
|
def __init__(self, db, welcome):
|
2015-02-11 09:05:11 +00:00
|
|
|
resource.Resource.__init__(self)
|
2015-10-04 22:49:06 +00:00
|
|
|
service.MultiService.__init__(self)
|
2015-04-10 16:15:27 +00:00
|
|
|
self.db = db
|
2015-04-21 01:34:13 +00:00
|
|
|
self.welcome = welcome
|
2015-02-11 09:05:11 +00:00
|
|
|
self.channels = {}
|
2015-10-04 22:49:06 +00:00
|
|
|
t = internet.TimerService(EXPIRATION_CHECK_PERIOD,
|
|
|
|
self.prune_old_channels)
|
|
|
|
t.setServiceParent(self)
|
|
|
|
|
2015-02-11 09:05:11 +00:00
|
|
|
|
|
|
|
def getChild(self, path, request):
|
2015-09-27 23:55:46 +00:00
|
|
|
if path == b"allocate":
|
2015-05-05 01:13:14 +00:00
|
|
|
return Allocator(self.db, self.welcome)
|
2015-09-27 23:55:46 +00:00
|
|
|
if path == b"list":
|
2015-05-05 00:40:57 +00:00
|
|
|
return ChannelList(self.db, self.welcome)
|
2015-09-27 23:55:46 +00:00
|
|
|
if not re.search(br'^\d+$', path):
|
2015-02-11 09:05:11 +00:00
|
|
|
return resource.ErrorPage(http.BAD_REQUEST,
|
|
|
|
"invalid channel id",
|
|
|
|
"invalid channel id")
|
2015-10-06 23:16:41 +00:00
|
|
|
channelid = int(path)
|
|
|
|
if not channelid in self.channels:
|
|
|
|
log.msg("spawning #%d" % channelid)
|
|
|
|
self.channels[channelid] = Channel(channelid, self, self.db,
|
|
|
|
self.welcome)
|
|
|
|
return self.channels[channelid]
|
2015-02-11 09:05:11 +00:00
|
|
|
|
2015-10-06 23:16:41 +00:00
|
|
|
def maybe_free_child(self, channelid, side):
|
2015-05-05 01:13:14 +00:00
|
|
|
self.db.execute("DELETE FROM `allocations`"
|
2015-10-06 23:16:41 +00:00
|
|
|
" WHERE `channelid`=? AND `side`=?",
|
|
|
|
(channelid, side))
|
2015-05-05 01:13:14 +00:00
|
|
|
self.db.commit()
|
|
|
|
remaining = self.db.execute("SELECT COUNT(*) FROM `allocations`"
|
2015-10-06 23:16:41 +00:00
|
|
|
" WHERE `channelid`=?",
|
|
|
|
(channelid,)).fetchone()[0]
|
2015-05-05 01:13:14 +00:00
|
|
|
if remaining:
|
|
|
|
return False
|
2015-10-06 23:16:41 +00:00
|
|
|
self.free_child(channelid)
|
2015-05-05 01:13:14 +00:00
|
|
|
return True
|
|
|
|
|
2015-10-06 23:16:41 +00:00
|
|
|
def free_child(self, channelid):
|
|
|
|
self.db.execute("DELETE FROM `allocations` WHERE `channelid`=?",
|
|
|
|
(channelid,))
|
|
|
|
self.db.execute("DELETE FROM `messages` WHERE `channelid`=?",
|
|
|
|
(channelid,))
|
2015-04-10 17:00:08 +00:00
|
|
|
self.db.commit()
|
2015-10-06 23:16:41 +00:00
|
|
|
if channelid in self.channels:
|
|
|
|
self.channels.pop(channelid)
|
2015-05-05 01:13:14 +00:00
|
|
|
log.msg("freed+killed #%d, now have %d DB channels, %d live" %
|
2015-10-06 23:16:41 +00:00
|
|
|
(channelid, len(get_allocated(self.db)), len(self.channels)))
|
2015-05-05 01:13:14 +00:00
|
|
|
|
|
|
|
def prune_old_channels(self):
|
2015-05-05 01:19:40 +00:00
|
|
|
old = time.time() - CHANNEL_EXPIRATION_TIME
|
2015-10-06 23:16:41 +00:00
|
|
|
for channelid in get_allocated(self.db):
|
2015-05-05 01:13:14 +00:00
|
|
|
c = self.db.execute("SELECT `when` FROM `messages`"
|
2015-10-06 23:16:41 +00:00
|
|
|
" WHERE `channelid`=?"
|
|
|
|
" ORDER BY `when` DESC LIMIT 1", (channelid,))
|
2015-05-05 01:13:14 +00:00
|
|
|
rows = c.fetchall()
|
|
|
|
if not rows or (rows[0]["when"] < old):
|
2015-10-06 23:16:41 +00:00
|
|
|
log.msg("expiring %d" % channelid)
|
|
|
|
self.free_child(channelid)
|
2015-02-11 09:05:11 +00:00
|
|
|
|