From c9e197c9021661821013feae7565a63bbe0d3868 Mon Sep 17 00:00:00 2001 From: Dennis Schridde Date: Sun, 23 Dec 2007 18:40:10 +0000 Subject: [PATCH] Improved MasterServer by Gerhard Schaden, patch #893. Depends on Python 2.5, but we should have that on the server very soon. Should effectively help fight ghostgames and also be easily extensible in the future. (Gerhard talked about a web-interface with admin functions. :) ) git-svn-id: svn+ssh://svn.gna.org/svn/warzone/trunk@3139 4a71c877-e1ca-e34f-864e-861f7616d084 --- tools/masterserver/wzmasterserver.py | 260 ++++++++++++++++----------- tools/masterserver/wztest.py | 107 +++++++++++ 2 files changed, 264 insertions(+), 103 deletions(-) create mode 100644 tools/masterserver/wztest.py diff --git a/tools/masterserver/wzmasterserver.py b/tools/masterserver/wzmasterserver.py index 628318c27..0562d00ad 100644 --- a/tools/masterserver/wzmasterserver.py +++ b/tools/masterserver/wzmasterserver.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2.5 # # This file is part of the Warzone 2100 Resurrection Project. # Copyright (c) 2007 Warzone 2100 Resurrection Project @@ -15,13 +15,14 @@ # -------------------------------------------------------------------------- # MasterServer v0.1 by Gerard Krol (gerard_) and Tim Perrei (Kamaze) # v1.0 by Freddie Witherden (EvilGuru) +# v2.0 by Gerhard Schaden (gschaden) # -------------------------------------------------------------------------- # ################################################################################ # -__author__ = "Gerhard Krol, Tim Perrei, Freddie Witherden" -__version__ = "1.0" +__author__ = "Gerhard Krol, Tim Perrei, Freddie Witherden, Gerhard Schaden" +__version__ = "2.0" __bpydoc__ = """\ This script runs a Warzone 2100 2.x masterserver """ @@ -30,12 +31,17 @@ This script runs a Warzone 2100 2.x masterserver ################################################################################ # Get the things we need. +#import the new with statement, has to be the first expression +from __future__ import with_statement + import sys import SocketServer -import thread import struct import socket -import time +from threading import Lock, Thread, Event, Timer +import select +import logging +import cmd # ################################################################################ @@ -43,20 +49,104 @@ import time gamePort = 9999 # Gameserver port. lobbyPort = 9998 # Lobby port. -lobbyDbg = True # Enable debugging. gsSize = 112 # Size of GAMESTRUCT in byte. -ipOffset = 64+4+4 # 64 byte StringSize + SDWORD + SDWORD -gameList = {} # Holds the list. -listLock = thread.allocate_lock() +logging.basicConfig(level=logging.DEBUG, format="%(asctime)-15s %(levelname)s %(message)s") + +# +################################################################################ +# Game DB + +gdb=None +gamedblock = Lock() +class GameDB: + def __init__(self): + self.list = set() + + def addGame(self, g): + """ add a game """ + with gamedblock: + self.list.add(g) + + def removeGame(self, g): + """ remove a game from the list""" + with gamedblock: + # if g is not in thel list, ignore the KeyError exception + try: + self.list.remove(g) + except KeyError: + pass + + # only games with a valid description + def getGames(self): + """ filter all games with a valid description """ + return filter(lambda x: x.description, self.list) + + def getAllGames(self): + """ return all knwon games """ + return self.list + + def getGamesByHost(self, host): + """ filter all games of a certain host""" + return filter(lambda x: x.host == host, self.getGames()) + + def listGames(self): + with gamedblock: + gamesCount=len(self.getGames()) + logging.debug("Gameserver list: %i game(s)" % (gamesCount)) + for game in self.getGames(): + logging.debug(" %s" % game) + + + +# +################################################################################ +# Game class + +class Game: + """ class for a single game """ + + def __init__(self): + self.description = None + self.size = None + self.flags = None + self.host = None + self.maxPlayers = None + self.currentPlayers = None + self.user1 = None + self.user2 = None + self.user3 = None + self.user4 = None + + def setData(self, d): + """ decode the c-structure from the server into local varialbles""" + (self.description, self.size, self.flags, self.host, self.maxPlayers, self.currentPlayers, + self.user1, self.user2, self.user3, self.user4 ) = struct.unpack("64sII16sIIIIII", d) + self.description=self.description.strip("\x00") + self.host=self.host.strip("\x00") + logging.debug("Game: %s %s %s %s" % ( self.host, self.description, self.maxPlayers, self.currentPlayers)) + + def getData(self): + """ use local variables and build a c-structure, for sending to the clients""" + return struct.pack("64sII16sIIIIII", + self.description.ljust(64, "\x00"), + self.size, self.flags, + self.host.ljust(16, "\x00"), + self.maxPlayers, self.currentPlayers, self.user1, self.user2, self.user3, self.user4) + + def __str__(self): + return "Game: %16s %s %s %s" % ( self.host, self.description, self.maxPlayers, self.currentPlayers) + # ################################################################################ # Socket Handler. class RequestHandler(SocketServer.ThreadingMixIn, SocketServer.StreamRequestHandler): def handle(self): - global gameList - # Read the incoming command. + # client address + gameHost = self.client_address[0] + + # Read the incoming command. netCommand = self.rfile.read(4) # Skip the trailing NULL. @@ -66,121 +156,85 @@ class RequestHandler(SocketServer.ThreadingMixIn, SocketServer.StreamRequestHand # Process the incoming command. # ################################# + logging.debug("Command(%s): %s" % (gameHost, netCommand)) # Add a game. if netCommand == 'addg': - # Debug - if lobbyDbg: - print "<- addg" - - # Fix the server address. - gameHost = self.client_address[0] - # Check we can connect to the host s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: - if lobbyDbg: - print " \- Checking gameserver's vitality..." + logging.debug("Checking gameserver's vitality...") s.settimeout(10.0) s.connect((gameHost, gamePort)) - except: - if lobbyDbg: - print " \- Gameserver did not respond!" s.close() + except: + logging.debug("Gameserver did not respond!") return - - # The host is valid, close the socket and continue - if lobbyDbg: - print " \- Adding gameserver." - s.close() - - while len(gameHost) < 16: - gameHost += '\0' - - currentGameData = None - - # and start receiving updates about the game - while True: - # Receive the gamestruct. - try: + + # The host is valid + logging.debug("Adding gameserver.") + try: + # create a game object + g=Game() + + # put it in the database + gdb.addGame(g) + + # and start receiving updates about the game + while True: newGameData = self.rfile.read(gsSize) - except: - newGameData = None - - # remove the previous data from the list - if currentGameData: - listLock.acquire() - try: - if lobbyDbg: - print "Removing game from", self.client_address[0] - if currentGameData in gameList: - del gameList[currentGameData] - finally: - listLock.release() - - if not newGameData: - # incomplete data - break - - # Update the new gameData whith the gameHost - currentGameData = newGameData[:ipOffset] + gameHost + newGameData[ipOffset+16:] - - # Put the game in the database - listLock.acquire() - try: - if lobbyDbg: - print " \- Adding game from", self.client_address[0] - gameList[currentGameData] = time.time() - finally: - listLock.release() - + if not newGameData: + logging.debug("End of gameserver") + return + + #set Gamedata + g.setData(newGameData) + #set gamehost + g.host = gameHost + gdb.listGames() + + except KeyError: + logging.warning("Communication error with %s" % g ) + finally: + if g: + gdb.removeGame(g) # Get a game list. elif netCommand == 'list': - # Debug - if lobbyDbg: - print "<- list" - print " \- I know ", len(gameList), " games." - # Lock the gamelist to prevent new games while output. - listLock.acquire() - - # Expire old games by removing them from the list - # Note: The thread is still alive and running - now = time.time() - newGameList = {} - for game, lastUpdate in gameList.iteritems(): - # time out in 1 hour - if lastUpdate > now - 3600: - newGameList[game] = lastUpdate - else: - if lobbyDbg: - print "Game expired" - gameList = newGameList - - # Transmit the length of the following list as unsigned integer (in network byte-order: big-endian). - count = struct.pack('!I', len(gameList)) - self.wfile.write(count) - - # Transmit the single games. - for game in gameList.iterkeys(): - self.wfile.write(game) - - # Remove the lock. - listLock.release() - + with gamedblock: + gamesCount=len(gdb.getGames()) + logging.debug("Gameserver list: %i game(s)" % (gamesCount)) + + # Transmit the length of the following list as unsigned integer (in network byte-order: big-endian). + count = struct.pack('!I', gamesCount) + self.wfile.write(count) + + # Transmit the single games. + for game in gdb.getGames(): + logging.debug(" %s" % game) + self.wfile.write(game.getData()) + # If something unknown apperas. else: - print "Recieved a unknown command: ", netCommand - + raise Exception("Recieved a unknown command: %s" % netCommand) # ################################################################################ # The legendary Main. if __name__ == '__main__': - print "Starting Warzone 2100 lobby server on port ", lobbyPort + logging.info("Starting Warzone 2100 lobby server on port %d" % lobbyPort) + gdb=GameDB() SocketServer.ThreadingTCPServer.allow_reuse_address = True tcpserver = SocketServer.ThreadingTCPServer(('0.0.0.0', lobbyPort), RequestHandler) - tcpserver.serve_forever() + try: + while True: + tcpserver.handle_request() + except KeyboardInterrupt: + pass + logging.info("Shutting down lobby server, cleaning up") + for game in gdb.getAllGames(): + game.requestHandler.finish() + tcpserver.server_close() diff --git a/tools/masterserver/wztest.py b/tools/masterserver/wztest.py new file mode 100644 index 000000000..66027f6cd --- /dev/null +++ b/tools/masterserver/wztest.py @@ -0,0 +1,107 @@ +#!/usr/bin/python2.5 +# +# This file is part of the Warzone 2100 Resurrection Project. +# Copyright (c) 2007 Warzone 2100 Resurrection Project +# +# Warzone 2100 is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# You should have received a copy of the GNU General Public License +# along with Warzone 2100; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +# -------------------------------------------------------------------------- +# MasterServer Test v0.1 by Gerhard Schaden (gschaden) +# -------------------------------------------------------------------------- +# +################################################################################ +# + +__author__ = "Gerhard Schaden" +__version__ = "0.1" +__bpydoc__ = """\ +This script simulates a Warzone 2100 2.x client to test the masterserver +""" + +# +################################################################################ +# + +import wzmasterserver as wz +import socket +import logging +from threading import Timer +import SocketServer +import time +import struct + +server="localhost" + +# simulate a gameserver +class RequestHandler(SocketServer.ThreadingMixIn, SocketServer.StreamRequestHandler): + def handle(self): + logging.debug("got connection from lobbyserver") + +def simulateGameServer(): + SocketServer.ThreadingTCPServer.allow_reuse_address = True + tcpserver = SocketServer.ThreadingTCPServer(('0.0.0.0', wz.gamePort), RequestHandler) + tcpserver.handle_request() + tcpserver.server_close() + + +# thread with simulates adding a game +def doAddGame(): + + # gamestrucuure + g=wz.Game() + g.description = "description" + g.size = 0 + g.flags = 0 + g.host = "1.1.1.1" + g.maxPlayers = 8 + g.currentPlayers = 1 + g.user1 = 0 + g.user2 = 0 + g.user3 = 0 + g.user4 = 0 + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + logging.debug("connect to lobby") + s.settimeout(10.0) + s.connect((server, wz.lobbyPort)) + s.send("addg ") + s.send(g.getData()) + #hold the game open for 10 seconds + time.sleep(10) + s.close() + +#thread with simulates listing the available games +def doListGames(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + logging.debug("connect to lobby") + s.settimeout(10.0) + s.connect((server, wz.lobbyPort)) + s.send("list ") + n = struct.unpack("!I", s.recv(4))[0] + logging.debug("read %d games" % n) + for i in range(n): + g = wz.Game() + g.setData(s.recv(wz.gsSize)) + logging.debug("%s" % g) + + #hold the game open for 10 seconds + s.close() + +#start gameserver +t=Timer(0, simulateGameServer) +t.start() + +#start add game thread +t=Timer(1, doAddGame) +t.start() + +#start list Thread +t=Timer(3, doListGames) +t.start()