From 936c98da86ec41255dacd7975a6115055a7c583a Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Mon, 5 Jan 2015 19:27:04 +0100 Subject: [PATCH] Buttonserver (Telnet Buttons!) --- extra/buttonsrv.py | 293 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100755 extra/buttonsrv.py diff --git a/extra/buttonsrv.py b/extra/buttonsrv.py new file mode 100755 index 0000000..6b9b59c --- /dev/null +++ b/extra/buttonsrv.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python2 +from __future__ import print_function + +# Seopardy Network Button Server +# +# This button server was written as a replacement for the "usual" button +# hardware used with this game. It opens a port where users can telnet to +# and allows them to use the enter key of their laptop as a button. +# +# Disclaimer: This code was written on the 31c3 while being sleep-deprived +# +# Workflow: +# 1. Configure seopardy to use the Unix player input +# 2. Start seopardy with questions +# 3. Start button server with -u +# 4. Tell players to telnet/netcat to your machine (default port 9999) +# 5. Assign player numbers to all connection participants +# 6. Start game + +import SocketServer +import threading +import select +import Queue +import sys +import argparse +import socket + +class PlayerHandler(SocketServer.BaseRequestHandler): + """ Handle a player connection, used by SocketServer """ + banner = " ____ _ \n" + \ + "/ ___| ___ ___ _ __ __ _ _ __ __| |_ _ \n" + \ + "\___ \ / _ \/ _ \| '_ \ / _` | '__/ _` | | | |\n" + \ + " ___) | __/ (_) | |_) | (_| | | | (_| | |_| |\n" + \ + "|____/ \___|\___/| .__/ \__,_|_| \__,_|\__, |\n" + \ + " Network Button |_| Server v0.1 |___/ \n" + + connQueue = None + regOpen = True + usock = None + + def handle(self): + self.player = None + self.running = True + try: + self.handle_client() + except: + self.running = False + raise + + self.running = False + + def handle_client(self): + # self.request is the TCP socket connected to the client + self.request.send(self.banner + "\n") + if not self.regOpen: + self.request.send("Currently no new players are accepted. Goodbye.\n") + return + + print("?? Putting new client into queue", ":".join(map(str, self.client_address))) + self.connQueue.put(self) + self.request.send("Requesting a player socket for you\n") + + # waiting for a player number... + while not self.player: + if not self.regOpen: + # management has closed the registration. be polite and say goodbye + self.request.send("Currently no new players are accepted. Goodbye.\n") + return + if not self.running: + # management has said "discard" + self.request.send("Request denied. Goodbye.\n") + return + + if len(select.select([self.request.fileno()], [], [], 0.5)[0]) > 0: + if self.request.recv(1024) == '': + print("!! Client %s was tired of waiting" % (":".join(map(str, cli.client_address)),)) + self.running = False + return + + self.request.send("You are player %s!\n" % self.player) + + while self.running: + # everytime we receive a UDP packet we interpret it as a button press + data = self.request.recv(1024) + if data == '': + print("!! Client %s closed the connection" % (":".join(map(str, cli.client_address)),)) + return + + if self.running and self.usock: + # if we are still connected && the board is still there, send a button press + print("Button press from player %s (%s)" % (self.player, self.client_address[0])) + self.request.sendall("Button press recognized... ") + + self.usock.send(str(self.player)) + + +class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): + pass + +class ButtonServer(threading.Thread): + """ Thread that runs the button telnet server """ + def __init__(self, port): + threading.Thread.__init__(self) + + self._port = port + SocketServer.TCPServer.allow_reuse_address = True + self.server = ThreadedTCPServer(("", self._port), PlayerHandler) + + self.daemon = True + + self._acceptClients = True + + def isOpen(self): + return self._acceptClients + + def run(self): + self.server.serve_forever() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-u", "--unix-socket", required=True, help="Path to unix domain socket with seopardy on the other side") + parser.add_argument("-p", "--port", type=int, default=9999, help="Port") + + args = parser.parse_args() + + # queue for new player connections + connQueue = Queue.Queue() + PlayerHandler.connQueue = connQueue + + # connect to unix domain socket + usock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + usock.connect(args.unix_socket) + except socket.error as e: + print("Error: Could not connect to seopardy unix domain socket '%s': %s" % (args.unix_socket, str(e))) + print("Exiting") + sys.exit(1) + PlayerHandler.usock = usock + + # start telnet button server + ThreadedTCPServer.daemon_threads = True + server = ButtonServer(args.port) + server.start() + + buttonsOpen = False + lastPlayer = None + clients = {} + + print(PlayerHandler.banner) + print("Disclaimer: Very hacky software!") + print() + print("Seopardy UNIX domain socket:", args.unix_socket) + print("Server running on port:", args.port) + + helpText = "You are on the management console.\n\n" + \ + "Press c to close registration and k to kill a player's connection\n" + \ + "Options are: (c)lose registration, re(o)pen registration, (k)ill player, show (s)tatus\n" + print(helpText) + + while True: + (readSocks, _, _) = select.select([sys.stdin, usock], [], [], 0.5) + + # clean up players + for playerNo, player in clients.items(): + if not player.running: + print("-- Player %s (%s) disconnected" % (playerNo, ":".join(map(str, player.client_address)))) + del(clients[playerNo]) + + if len(readSocks) > 0: + if sys.stdin in readSocks: + # handle data coming from stdin + + cmd = sys.stdin.readline().strip() + if cmd == "c": + # closing registration: new connections to the server will be discarded + print(">> Closing registration") + PlayerHandler.regOpen = False + elif cmd == "o": + # reopen registration + if not PlayerHandler.regOpen: + print(">> Reopening registration") + PlayerHandler.regOpen = True + else: + print("!! Registration still open") + elif cmd == "k": + # disconnect a player + inp = raw_input("Which player should I kill? ") + playerNo = None + try: + playerNo = int(inp) + except ValueError: + pass + + if playerNo and playerNo in clients.keys(): + clients[playerNo].running = False + clients[playerNo].request.close() + del(clients[playerNo]) + print("++ Player %s killed" % playerNo) + else: + print("!! Player %s not found" % playerNo) + elif cmd == "s": + print("Unix socket:", args.unix_socket) + print("TCP Port:", args.port) + print("Clients: %d connected" % len(clients)) + for playerNo, player in clients.items(): + print(" -- Player %s (%s)" % (playerNo, ":".join(map(str, player.client_address)))) + print() + elif cmd in ("h", "?", "help"): + print(helpText) + else: + print("!! Unknown command...") + elif usock in readSocks: + # handle data coming from the seopardy board + + cmd = usock.recv(1) + if cmd == '': + # FIXME: Can we reconnect? + print("!! Seopardy quitted its socket") + PlayerHandler.usock = None + for playerNo, player in clients.items(): + if player.running: + clients[playerNo].request.send("\nThe board has quit and so do we. Goodbye player %s! :)\n" % playerNo) + clients[playerNo].request.close() + del(clients[playerNo]) + sys.exit(1) + if cmd == "T": + # T tells us which player's turn it is + player = usock.recv(1) + playerNo = None + try: + playerNo = int(player) + except ValueError: + pass + + if playerNo and playerNo in clients.keys(): + lastPlayer = playerNo + clients[playerNo].request.send("Your turn! Please ask a question!\n") + else: + print("!! Unknown player: '%s' (not connected?)" % playerNo) + elif cmd == "C": + # buttons are currently closed + print("-- Buttons are closed") + if buttonsOpen: + buttonsOpen = False + for playerNo, client in clients.iteritems(): + if playerNo != lastPlayer: + client.request.send("Buttons are closed...\n") + elif cmd == "O": + # buttons are open again! + print("-- Buttons are open") + lastPlayer = None + if not buttonsOpen: + buttonsOpen = True + for client in clients.values(): + client.request.send("Buttons are open!\n") + else: + # if nothing else to do, check if we have a new client in the queue + if not connQueue.empty(): + # new client awaits for us to assign them a player number + cli = connQueue.get() + if cli.running: + cliHandled = False + while not cliHandled and cli.running: + print("New client, connecting from", ":".join(map(str, cli.client_address))) + print("Options are: (d)iscard, or assigning a player number from 1 to 9") + cmd = sys.stdin.readline().strip() + + if cmd == 'd': + print("++ Discarding...") + cli.running = False + cliHandled = True + elif ord(cmd[0]) > ord("0") and ord(cmd[0]) <= ord("9"): + playerNo = ord(cmd[0]) - ord("0") + assign = True + if playerNo in clients.keys(): + # if player number is taken, allow user to kill them + inp = raw_input("Player %s is already present. Kill them? (y/n) " % playerNo) + if inp.strip().lower() == "y": + print("Killing old player %s for your convenience." % playerNo) + clients[playerNo].running = False + clients[playerNo].request.close() + del(clients[playerNo]) + else: + print("Not assigning plaser number %s" % playerNo) + assign = False + + if assign: + cli.player = playerNo + clients[playerNo] = cli + cli.running = True + cliHandled = True + print("Player", playerNo, "connected")