Mercurial > traipse_dev
diff orpg/networking/meta_server_lib.py @ 0:4385a7d0efd1 grumpy-goblin
Deleted and repushed it with the 'grumpy-goblin' branch. I forgot a y
author | sirebral |
---|---|
date | Tue, 14 Jul 2009 16:41:58 -0500 |
parents | |
children | c54768cffbd4 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/orpg/networking/meta_server_lib.py Tue Jul 14 16:41:58 2009 -0500 @@ -0,0 +1,644 @@ +#!/usr/bin/python2.1 +# Copyright (C) 2000-2001 The OpenRPG Project +# +# openrpg-dev@lists.sourceforge.net +# +# This program 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# -- +# +# File: meta_server_lib.py +# Author: Chris Davis +# Maintainer: +# Version: +# $Id: meta_server_lib.py,v 1.40 2007/04/04 01:18:42 digitalxero Exp $ +# +# Description: A collection of functions to communicate with the meta server. +# + + +#added debug flag for meta messages to cut console server spam --Snowdog +META_DEBUG = 0 + +__version__ = "$Id: meta_server_lib.py,v 1.40 2007/04/04 01:18:42 digitalxero Exp $" + +from orpg.orpg_version import PROTOCOL_VERSION +from orpg.orpg_xml import * +import orpg.dirpath +import orpg.tools.validate +import urllib +import orpg.minidom +from threading import * +import time +import sys +import random +import traceback +import re + +metacache_lock = RLock() + +def get_server_dom(data=None,path=None): + # post data at server and get the resulting DOM + + if path == None: + # get meta server URI + path = getMetaServerBaseURL() + + # POST the data + if META_DEBUG: + print + print "Sending the following POST info to Meta at " + path + ":" + print "==========================================" + print data + print + file = urllib.urlopen(path, data) + data = file.read() + file.close() + + # Remove any leading or trailing data. This can happen on some satellite connections + p = re.compile('(<servers>.*?</servers>)',re.DOTALL|re.IGNORECASE) + mo = p.search(data) + if mo: + data = mo.group(0) + + if META_DEBUG: + print + print "Got this string from the Meta at " + path + ":" + print "===============================================" + print data + print + # build dom + xml_dom = parseXml(data) + xml_dom = xml_dom._get_documentElement() + return xml_dom + +def post_server_data( name, realHostName=None): + # build POST data +## data = urllib.urlencode( {"server_data[name]":name, +## "server_data[version]":PROTOCOL_VERSION, +## "act":"new"} ) +## + if realHostName: + data = urllib.urlencode( {"server_data[name]":name, + "server_data[version]":PROTOCOL_VERSION, + "act":"new", + "REMOTE_ADDR": realHostName } ) + + else: + #print "Letting meta server decide the hostname to list..." + data = urllib.urlencode( {"server_data[name]":name, + "server_data[version]":PROTOCOL_VERSION, + "act":"new"} ) + + xml_dom = get_server_dom( data , "http://openrpg.sf.net/openrpg_servers.php") + ret_val = int( xml_dom.getAttribute( "id" ) ) + return ret_val + +def post_failed_connection(id,meta=None,address=None,port=None): + # For now, turning this off. This needs to be re-vamped for + # handling multiple Metas. + return 0 +# data = urllib.urlencode({"id":id,"act":"failed"}); +# xml_dom = get_server_dom(data) +# ret_val = int(xml_dom.getAttribute("return")) +# return ret_val + +def remove_server(id): + data = urllib.urlencode({"id":id,"act":"del"}); + xml_dom = get_server_dom(data) + ret_val = int(xml_dom.getAttribute("return")) + return ret_val + + +def byStartAttribute(first,second): + # This function is used to easily sort a list of nodes + # by their start time + + if first.hasAttribute("start"): + first_start = int(first.getAttribute("start")) + else: + first_start = 0 + + if second.hasAttribute("start"): + second_start = int(second.getAttribute("start")) + else: + second_start = 0 + + # Return the result of the cmp function on the two strings + return cmp(first_start,second_start) + +def byNameAttribute(first,second): + # This function is used to easily sort a list of nodes + # by their name attribute + + # Ensure there is something to sort with for each + + if first.hasAttribute("name"): + first_name = str(first.getAttribute("name")).lower() + else: + first_name = "" + + if second.hasAttribute("name"): + second_name = str(second.getAttribute("name")).lower() + else: + second_name = "" + + # Return the result of the cmp function on the two strings + + return cmp(first_name,second_name) + + +def get_server_list(versions = None,sort_by="start"): + data = urllib.urlencode({"version":PROTOCOL_VERSION,"ports":"%"}) + all_metas = getMetaServers(versions,1) # get the list of metas + base_meta = getMetaServerBaseURL() + + #all_metas.reverse() # The last one checked will take precedence, so reverse the order + # so that the top one on the actual list is checked last + + return_hash = {} # this will end up with an amalgamated list of servers + + for meta in all_metas: # check all of the metas + + # get the server's xml from the current meta + bad_meta = 0 + #print "Getting server list from " + meta + "..." + try: + xml_dom = get_server_dom(data=data,path=meta) + except: + #print "Trouble getting servers from " + meta + "..." + bad_meta = 1 + + if bad_meta: + continue + + if base_meta == meta: + #print "This is our base meta: " + meta + updateMetaCache(xml_dom) + + node_list = xml_dom.getElementsByTagName('server') + + if len(node_list): # if there are entries in the node list + # otherwise, just loop to next meta + + # for each node found, we're going to check the nodes from prior + # metas in the list. If a match is found, then use the new values. + for n in node_list: + + # set them from current node + + if not n.hasAttribute('name'): + n.setAttribute('name','NO_NAME_GIVEN') + name = n.getAttribute('name') + if not n.hasAttribute('num_users'): + n.setAttribute('num_users','N/A') + num_users = n.getAttribute('num_users') + if not n.hasAttribute('address'): + n.setAttribute('address','NO_ADDRESS_GIVEN') + address = n.getAttribute('address') + if not n.hasAttribute('port'): + n.setAttribute('port','6774') + port = n.getAttribute('port') + n.setAttribute('meta',meta) + end_point = str(address) + ":" + str(port) + if return_hash.has_key(end_point): + if META_DEBUG: print "Replacing duplicate server entry at " + end_point + return_hash[end_point] = n + + # At this point, we have an amalgamated list of servers + # Now, we have to construct a new DOM to pass back. + + # Create a servers element + return_dom = orpg.minidom.Element("servers") + + # get the nodes stored in return_hash + return_list = return_hash.values() + + # sort them by their name attribute. Uses byNameAttribute() + # defined above as a comparison function + + if sort_by == "start": + return_list.sort(byStartAttribute) + elif sort_by == "name": + return_list.sort(byNameAttribute) + + # Add each node to the DOM + for n in return_list: + return_dom.appendChild(n) + return return_dom + +## List Format: +## <servers> +## <server address=? id=? name=? failed_count=? > +## </servers> + +def updateMetaCache(xml_dom): + try: + if META_DEBUG: print "Updating Meta Server Cache" + metaservers = xml_dom.getElementsByTagName( 'metaservers' ) # pull out the metaservers bit + if len(metaservers) == 0: + cmetalist = getRawMetaList() + xml_dom = get_server_dom(cmetalist[0]) + metaservers = xml_dom.getElementsByTagName( 'metaservers' ) + authoritative = metaservers[0].getAttribute('auth') + if META_DEBUG: print " Authoritive Meta: "+str(authoritative) + metas = metaservers[0].getElementsByTagName("meta") # get the list of metas + if META_DEBUG: print " Meta List ("+str(len(metas))+" servers)" + try: + metacache_lock.acquire() + ini = open(orpg.dirpath.dir_struct["user"]+"metaservers.cache","w") + for meta in metas: + if META_DEBUG: print " Writing: "+str(meta.getAttribute('path')) + ini.write(str(meta.getAttribute('path')) + " " + str(meta.getAttribute('versions')) + "\n") + ini.close() + finally: + metacache_lock.release() + except Exception, e: + if META_DEBUG: traceback.print_exc() + print "Meta Server Lib: UpdateMetaCache(): " + str(e) + +def getRawMetaList(): + try: + try: + metacache_lock.acquire() + # Read in the metas + orpg.tools.validate.Validate().config_file("metaservers.cache","metaservers.cache") + ini = open(orpg.dirpath.dir_struct["user"]+"metaservers.cache","r") + metas = ini.readlines() + ini.close() + return metas + finally: + metacache_lock.release() + except Exception, e: + if META_DEBUG: traceback.print_exc() + print "Meta Server Lib: getRawMetaList(): " + str(e) + return [] + +def getMetaServers(versions = None, pick_random=0): + # get meta server URLs as a list + + # versions is a list of acceptable version numbers. + # A False truth value will use getMetaServerBaseURL() + + # set a default if we have weird reading problems + # default_url = "http://www.openrpg.com/openrpg_servers.php" + + meta_names = [] + + if(versions): # If versions are supplied, then look in metaservers.conf + try: + # read in the metas from file + # format of file is one meta entry per line + # each entry will be the meta url, followed by one or more version numbers that it + # handle. Generally, this will be either a 1 for the original Meta format, or + # 2 for the new one. + + # Read in the metas + metas = getRawMetaList() + #print str(metas) + + # go through each one to check if it should be returned, based on the + # version numbers allowed. + for meta in metas: + + # split the line on whitespace + # obviously, your meta servers urls shouldn't contain whitespace. duh. + words = meta.split() + + success = 0 # init success flag for version check + + for version in versions: # run through each allowed version from caller + if version in words[1:]: # if the allowed version token was found + success += 1 # then increment the success indicator + + if success: # if the meta entry is acceptable to the caller + meta_names.append(words[0]) # add the entry + if META_DEBUG: print "adding metaserver " + meta + + # at this point, we should have at least one name from the cache. If not ... + if not meta_names: + default_meta = getMetaServerBaseURL() # grab the meta from ini.xml + meta_names.append(default_meta) # add it to the return list +# print "Warning!!\nNo valid metaservers cached." +# print "Using meta from MetaServerBaseURL: " + default_meta + "\n" + # if we have more than one and want a random one + elif pick_random: + if META_DEBUG: print "choosing random meta from: " + str(meta_names) + i = int(random.uniform(0,len(meta_names))) + #meta = meta_names[i] + meta_names = [meta_names[i]] + if META_DEBUG: print "using: " + str(meta_names) + else: + if META_DEBUG: print "using all metas: " + str(meta_names) + return meta_names + except Exception,e: + print e + #print "using default meta server URI: " + default_url + metas = [] + #metas.append(default_url) + return metas # return an empty list + else: # otherwise, use MetaServerBaseURL() + url = getMetaServerBaseURL() + meta_names.append(url) + return meta_names + +def getMetaServerBaseURL(): + # get meta server URL + url = "http://www.openrpg.com/openrpg_servers.php" + try: + orpg.tools.validate.Validate().config_file("settings.xml","default_settings.xml") + ini = open(orpg.dirpath.dir_struct["user"]+"settings.xml","r") + txt = ini.read() + tree = parseXml(txt)._get_documentElement() + ini.close() + node_list = tree.getElementsByTagName("MetaServerBaseURL") + if node_list: + url = node_list[0].getAttribute("value") + + # allow tree to be collected + try: + tree.unlink() + except: + pass + + except Exception,e: + print e +# print "using meta server URI: " + url + return url + +####################################################################################### +# Beginning of Class registerThread +# +# A Class to Manage Registration with the Meta2 +# Create an instance and call it's start() method +# if you want to be (and stay) registered. This class +# will take care of registering and re-registering as +# often as necessary to stay in the Meta list. +# +# You may call register() yourself if you wish to change your +# server's name. It will immediately update the Meta. There +# is no need to unregister first. +# +# Call unregister() when you no longer want to be registered. +# This will result in the registerThread dying after +# attempting to immediately remove itself from the Meta. +# +# If you need to become registered again after that, you +# must create a new instance of class registerThread. Don't +# just try to call register() on the old, dead thread class. + + +class registerThread(Thread): +# Originally, I wrote this as a sub-class of wxThread, but +# A) I couldn't get it to import right +# B) I realized that I want this to be used in a server, +# which I don't want needing wxWindows to run! +# +# Because of this fact, there are some methods from wxThread +# that I implemented to minimize changes to the code I had +# just written, i.e. TestDeleteStatus() and Delete() + + def __init__(self,name=None,realHostName=None,num_users = "Hmmm",MetaPath=None,port=6774,register_callback=None): + + Thread.__init__(self,name="registerThread") + self.rlock = RLock() # Re-entrant lock used to make this class thread safe + self.die_event = Event() # The main loop in run() will wait with timeout on this + if name: + self.name = name # Name that the server want's displayed on the Meta + else: + self.name = "Unnamed server" # But use this if for some crazy reason no name is + # passed to the constructor + self.num_users = num_users # the number of users currently on this server + self.realHostName = realHostName # Name to advertise for connection + self.id = "0" # id returned from Meta. Defaults to "0", which + # indicates a new registration. + self.cookie = "0" # cookie returned from Meta. Defaults to "0",which + # indicates a new registration. + self.interval = 0 # interval returned from Meta. Is how often to + # re-register, in minutes. + self.destroy = 0 # Used to flag that this thread should die + self.port = str(port) + self.register_callback = register_callback # set a method to call to report result of register + # This thread will communicate with one and only one + # Meta. If the Meta in ini.xml is changed after + # instantiation, then this instance must be + # unregistered and a new instance instantiated. + # + # Also, if MetaPath is specified, then use that. Makes + # it easier to have multiple registerThreads going to keep the server registered + # on multiple (compatible) Metas. + + if MetaPath == None: + self.path = getMetaServerBaseURL() # Do this if no Meta specified + else: + self.path = MetaPath + + def getIdAndCookie(self): + return self.id, self.cookie + + def TestDeleteStatus(self): + try: + self.rlock.acquire() + return self.die_event.isSet() + finally: + self.rlock.release() + + def Delete(self): + try: + self.rlock.acquire() + self.die_event.set() + finally: + self.rlock.release() + + def run(self): + # This method gets called by Thread implementation + # when self.start() is called to begin the thread's + # execution + # + # We will basically enter a loop that continually + # re-registers this server and sleeps Interval + # minutes until the thread is ordered to die in place + while(not self.TestDeleteStatus()): # Loop while until told to die + # Otherwise, call thread safe register(). + self.register(self.name, self.realHostName, self.num_users) + if META_DEBUG: print "Sent Registration Data" + + # register() will end up setting the state variables + # for us, including self.interval. + try: + self.rlock.acquire() # Serialize access to this state information + + if self.interval >= 3: # If the number of minutes is one or greater + self.interval -= 1 # wake up with 30 seconds left to re-register + else: + self.interval = .5 # Otherwise, we probably experienced some kind + # of error from the Meta in register(). Sleep + # for 6 seconds and start from scratch. + + finally: # no matter what, release the lock + self.rlock.release() + # Wait interval minutes for a command to die + die_signal = self.die_event.wait(self.interval*60) + + # If we get past the while loop, it's because we've been asked to die, + # so just let run() end. Once this occurs, the thread is dead and + # calls to Thread.isAlive() return False. + + def unregister(self): + # This method can (I hope) be called from both within the thread + # and from other threads. It will attempt to unregister this + # server from the Meta database + # When this is either accomplished or has been tried hard enough + # (after which it just makes sense to let the Meta remove the + # entry itself when we don't re-register using this id), + # this method will either cause the thread to immediately die + # (if called from this thread's context) or set the Destroy flag + # (if called from the main thread), a positive test for which will cause + # the code in Entry() to exit() when the thread wakes up and + # checks TestDeleteStatus(). + # lock the critical section. The unlock will + # automatically occur at the end of the function in the finally clause + try: + self.rlock.acquire() + if not self.isAlive(): # check to see if this thread is dead + return 1 # If so, return an error result + # Do the actual unregistering here + data = urllib.urlencode( {"server_data[id]":self.id, + "server_data[cookie]":self.cookie, + "server_data[version]":PROTOCOL_VERSION, + "act":"unregister"} ) + try: + xml_dom = get_server_dom(data=data, path=self.path) # this POSTS the request and returns the result + if xml_dom.hasAttribute("errmsg"): + print "Error durring unregistration: " + xml_dom.getAttribute("errmsg") + except: + if META_DEBUG: print "Problem talking to Meta. Will go ahead and die, letting Meta remove us." + # If there's an error, echo it to the console + + # No special handling is required. If the de-registration worked we're done. If + # not, then it's because we've already been removed or have a bad cookie. Either + # way, we can't do anything else, so die. + self.Delete() # This will cause the registerThread to die in register() + # prep xml_dom for garbage collection + try: + xml_dom.unlink() + except: + pass + return 0 + finally: + self.rlock.release() + + def register(self, name=None, realHostName=None, num_users=None): + # Designed to handle the registration, both new and + # repeated. + # + # It is intended to be called once every interval + # (or interval - delta) minutes. + + # lock the critical section. The unlock will + # automatically occur at the end of the function in the finally clause + try: + self.rlock.acquire() + if not self.isAlive(): # check to see if this thread is dead + return 1 # If so, return an error result + + # Set the server's attibutes, if specified. + if name: + self.name = name + if num_users != None: + self.num_users = num_users + if realHostName: + self.realHostName = realHostName + # build POST data + if self.realHostName: + data = urllib.urlencode( {"server_data[id]":self.id, + "server_data[cookie]":self.cookie, + "server_data[name]":self.name, + "server_data[port]":self.port, + "server_data[version]":PROTOCOL_VERSION, + "server_data[num_users]":self.num_users, + "act":"register", + "server_data[address]": self.realHostName } ) + else: + if META_DEBUG: print "Letting meta server decide the hostname to list..." + data = urllib.urlencode( {"server_data[id]":self.id, + "server_data[cookie]":self.cookie, + "server_data[name]":self.name, + "server_data[port]":self.port, + "server_data[version]":PROTOCOL_VERSION, + "server_data[num_users]":self.num_users, + "act":"register"} ) + try: + xml_dom = get_server_dom(data=data,path=self.path) # this POSTS the request and returns the result + except: + if META_DEBUG: print "Problem talking to server. Setting interval for retry ..." + if META_DEBUG: print data + if META_DEBUG: print + self.interval = 0 + # If we are in the registerThread thread, then setting interval to 0 + # will end up causing a retry in about 6 seconds (see self.run()) + # If we are in the main thread, then setting interval to 0 will do one + # of two things: + # 1) Do the same as if we were in the registerThread + # 2) Cause the next, normally scheduled register() call to use the values + # provided in this call. + # + # Which case occurs depends on where the registerThread thread is when + # the main thread calls register(). + return 0 # indicates that it was okay to call, not that no errors occurred + # If there is a DOM returned .... + if xml_dom: + # If there's an error, echo it to the console + if xml_dom.hasAttribute("errmsg"): + print "Error durring registration: " + xml_dom.getAttribute("errmsg") + if META_DEBUG: print data + if META_DEBUG: print + # No special handling is required. If the registration worked, id, cookie, and interval + # can be stored and used for the next time. + # If an error occurred, then the Meta will delete us and we need to re-register as + # a new server. The way to indicate this is with a "0" id and "0" cookie sent to + # the server during the next registration. Since that's what the server returns to + # us on an error anyway, we just store them and the next registration will + # automatically be set up as a new one. + # + # Unless the server calls register() itself in the meantime. Of course, that's okay + # too, because a success on THAT register() call will set up the next one to use + # the issued id and cookie. + # + # The interval is stored unconditionally for similar reasons. If there's an error, + # the interval will be less than 1, and the main thread's while loop will reset it + # to 6 seconds for the next retry. + # Is it wrong to have a method where there's more comments than code? :) + try: + self.interval = int(xml_dom.getAttribute("interval")) + self.id = xml_dom.getAttribute("id") + self.cookie = xml_dom.getAttribute("cookie") + if not xml_dom.hasAttribute("errmsg"): + updateMetaCache(xml_dom) + except: + if META_DEBUG: print + if META_DEBUG: print "OOPS! Is the Meta okay? It should be returning an id, cookie, and interval." + if META_DEBUG: print "Check to see what it really returned.\n" + # Let xml_dom get garbage collected + try: + xml_dom.unlink() + except: + pass + else: # else if no DOM is returned from get_server_dom() + print "Error - no DOM constructed from Meta message!" + return 0 # Let caller know it was okay to call us + finally: + self.rlock.release() + # End of class registerThread + ################################################################################