#!/usr/bin/python import base64, json, mod_python, socket, struct, sys import psycopg2, random PROTOCOL_VERSION = 0 PRX_DEVICE_GONE = 3 PRX_GET_MSGS = 4 # try: # from mod_python import apache # apacheAvailable = True # except ImportError: # apacheAvailable = False # Joining a game. Basic idea is you have stuff to match on (room, # number in game, language) and when somebody wants to join you add to # an existing matching game if there's space otherwise create a new # one. Problems are the unreliablity of transport: if you give a space # and the device doesn't get the message you can't hold it forever. So # device provides a seed that holds the space. If it asks again for a # space with the same seed it gets the same space. If it never asks # again (app deleted, say), the space needs eventually to be given to # somebody else. I think that's done by adding a timestamp array and # treating the space as available if TIME has expired. Need to think # about this: what if app fails to ACK for TIME, then returns with # seed to find it given away. Let's do a 30 minute reservation for # now? [Note: much of this is PENDING] def join(req, devID, room, seed, hid = 0, lang = 1, nInGame = 2, nHere = 1, inviteID = None): assert hid <= 4 seed = int(seed) assert seed != 0 nInGame = int(nInGame) nHere = int(nHere) assert nHere <= nInGame assert nInGame <= 4 devID = int(devID, 16) connname = None logs = [] # for debugging # logs.append('vers: ' + platform.python_version()) con = psycopg2.connect(database='xwgames') cur = con.cursor() # cur.execute('LOCK TABLE games IN ACCESS EXCLUSIVE MODE') # First see if there's a game with a space for me. Must match on # room, lang and size. Must have room OR must have already given a # spot for a seed equal to mine, in which case I get it # back. Assumption is I didn't ack in time. query = "SELECT connname, seeds, nperdevice FROM games " query += "WHERE lang = %s AND nTotal = %s AND room = %s " query += "AND (njoined + %s <= ntotal OR %s = ANY(seeds)) " query += "LIMIT 1" cur.execute( query, (lang, nInGame, room, nHere, seed)) for row in cur: (connname, seeds, nperdevice) = row print('found', connname, seeds, nperdevice) break # should be only one! # If we've found a match, we either need to UPDATE or, if the # seeds match, remind the caller of where he belongs. If a hid's # been specified, we honor it by updating if the slot's available; # otherwise a new game has to be created. if connname: if seed in seeds and nHere == nperdevice[seeds.index(seed)]: hid = seeds.index(seed) + 1 print('resusing seed case; outta here!') else: if hid == 0: # Any gaps? Assign it if None in seeds: hid = seeds.index(None) + 1 else: hid = len(seeds) + 1 print('set hid to', hid, 'based on ', seeds) else: print('hid already', hid) query = "UPDATE games SET njoined = njoined + %s, " query += "devids[%d] = %%s, " % hid query += "seeds[%d] = %%s, " % hid query += "jtimes[%d] = 'now', " % hid query += "nperdevice[%d] = %%s " % hid query += "WHERE connname = %s " print(query) params = (nHere, devID, seed, nHere, connname) cur.execute(query, params) # If nothing was found, add a new game and add me. Honor my hid # preference if specified if not connname: # This requires python3, which likely requires mod_wsgi # ts = datetime.datetime.utcnow().timestamp() # connname = '%s:%d:1' % (xwconfig.k_HOSTNAME, int(ts * 1000)) connname = '%s:%d:1' % (xwconfig.k_HOSTNAME, random.randint(0, 10000000000)) useHid = hid == 0 and 1 or hid print('not found case; inserting using hid:', useHid) query = "INSERT INTO games (connname, room, lang, ntotal, njoined, " + \ "devids[%d], seeds[%d], jtimes[%d], nperdevice[%d]) " % (4 * (useHid,)) query += "VALUES (%s, %s, %s, %s, %s, %s, %s, 'now', %s) " query += "RETURNING connname, array_length(seeds,1); " cur.execute(query, (connname, room, lang, nInGame, nHere, devID, seed, nHere)) for row in cur: connname, gothid = row break if hid == 0: hid = gothid con.commit() con.close() result = {'connname': connname, 'hid' : hid, 'log' : ':'.join(logs)} return json.dumps(result) def kill(req, params): print(params) params = json.loads(params) count = len(params) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('127.0.0.1', 10998)) header = struct.Struct('!BBh') strLens = 0 for ii in range(count): strLens += len(params[ii]['relayID']) + 1 size = header.size + (2*count) + strLens sock.send(struct.Struct('!h').pack(size)) sock.send(header.pack(PROTOCOL_VERSION, PRX_DEVICE_GONE, count)) for ii in range(count): elem = params[ii] asBytes = bytes(elem['relayID']) sock.send(struct.Struct('!H%dsc' % (len(asBytes))).pack(elem['seed'], asBytes, '\n')) sock.close() result = {'err': 0} return json.dumps(result) # winds up in handle_udp_packet() in xwrelay.cpp def post(req, params): err = 'none' params = json.loads(params) data = params['data'] timeoutSecs = 'timeoutSecs' in params and params['timeoutSecs'] or 1.0 binData = [base64.b64decode(datum) for datum in data] udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udpSock.settimeout(float(timeoutSecs)) # seconds addr = ("127.0.0.1", 10997) for binDatum in binData: udpSock.sendto(binDatum, addr) responses = [] while True: try: data, server = udpSock.recvfrom(1024) responses.append(base64.b64encode(data)) except socket.timeout: #If data is not received back from server, print it has timed out err = 'timeout' break result = {'err' : err, 'data' : responses} return json.dumps(result) def query(req, params): print('params', params) params = json.loads(params) ids = params['ids'] # timeoutSecs = 'timeoutSecs' in params and float(params['timeoutSecs']) or 2.0 idsLen = 0 for id in ids: idsLen += len(id) tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # tcpSock.settimeout(timeoutSecs) tcpSock.connect(('127.0.0.1', 10998)) lenShort = 2 + idsLen + len(ids) + 2 print(lenShort, PROTOCOL_VERSION, PRX_GET_MSGS, len(ids)) header = struct.Struct('!hBBh') assert header.size == 6 tcpSock.send(header.pack(lenShort, PROTOCOL_VERSION, PRX_GET_MSGS, len(ids))) for id in ids: tcpSock.send(id + '\n') result = {'ids':ids} try: msgsLists = {} shortUnpacker = struct.Struct('!H') resLen, = shortUnpacker.unpack(tcpSock.recv(shortUnpacker.size)) # not getting all bytes nameCount, = shortUnpacker.unpack(tcpSock.recv(shortUnpacker.size)) resLen -= shortUnpacker.size print('resLen:', resLen, 'nameCount:', nameCount) if nameCount == len(ids) and resLen > 0: print('nameCount', nameCount) for ii in range(nameCount): perGame = [] countsThisGame, = shortUnpacker.unpack(tcpSock.recv(shortUnpacker.size)) # problem print('countsThisGame:', countsThisGame) for jj in range(countsThisGame): msgLen, = shortUnpacker.unpack(tcpSock.recv(shortUnpacker.size)) print('msgLen:', msgLen) msgs = [] if msgLen > 0: msg = tcpSock.recv(msgLen) print('msg len:', len(msg)) msg = base64.b64encode(msg) msgs.append(msg) perGame.append(msgs) msgsLists[ids[ii]] = perGame result['msgs'] = msgsLists except: # Anything but a timeout should mean we abort/send nothing result['err'] = 'hit exception' None return json.dumps(result) def main(): result = None if len(sys.argv) > 1: cmd = sys.argv[1] args = sys.argv[2:] if cmd == 'query' and len(args) > 0: result = query(None, json.dumps({'ids':args})) elif cmd == 'post': # Params = { 'data' : 'V2VkIE9jdCAxOCAwNjowNDo0OCBQRFQgMjAxNwo=' } # params = json.dumps(params) # print(post(None, params)) pass elif cmd == 'join': if len(args) == 6: result = join(None, 1, args[0], int(args[1]), int(args[2]), int(args[3]), int(args[4]), int(args[5])) elif cmd == 'kill': result = kill( None, json.dumps([{'relayID': args[0], 'seed':int(args[1])}]) ) if result: print '->', result else: print 'USAGE: query [connname/hid]*' print ' join ' print ' query [connname/hid]*' # print ' post ' print ' kill ' print ' join ' ############################################################################## if __name__ == '__main__': main()