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
master
Dennis Schridde 2007-12-23 18:40:10 +00:00
parent 6a5a93076c
commit c9e197c902
2 changed files with 264 additions and 103 deletions

View File

@ -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,11 +49,93 @@ 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)
#
################################################################################
@ -55,7 +143,9 @@ listLock = thread.allocate_lock()
class RequestHandler(SocketServer.ThreadingMixIn, SocketServer.StreamRequestHandler):
def handle(self):
global gameList
# client address
gameHost = self.client_address[0]
# Read the incoming command.
netCommand = self.rfile.read(4)
@ -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()
# The host is valid
logging.debug("Adding gameserver.")
try:
# create a game object
g=Game()
while len(gameHost) < 16:
gameHost += '\0'
currentGameData = None
# put it in the database
gdb.addGame(g)
# and start receiving updates about the game
while True:
# Receive the gamestruct.
try:
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
logging.debug("End of gameserver")
return
# Update the new gameData whith the gameHost
currentGameData = newGameData[:ipOffset] + gameHost + newGameData[ipOffset+16:]
#set Gamedata
g.setData(newGameData)
#set gamehost
g.host = gameHost
gdb.listGames()
# Put the game in the database
listLock.acquire()
try:
if lobbyDbg:
print " \- Adding game from", self.client_address[0]
gameList[currentGameData] = time.time()
except KeyError:
logging.warning("Communication error with %s" % g )
finally:
listLock.release()
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
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', len(gameList))
count = struct.pack('!I', gamesCount)
self.wfile.write(count)
# Transmit the single games.
for game in gameList.iterkeys():
self.wfile.write(game)
# Remove the lock.
listLock.release()
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()

View File

@ -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()