Mercurial > traipse_dev
view orpg/networking/meta_server_lib.py @ 32:c223c1051f4a traipse_dev
Update Manager moves into Beta 0.3 with a nice new look. The skeleton is
designed, not the functionality will be added.
author | sirebral |
---|---|
date | Sun, 02 Aug 2009 16:43:55 -0500 |
parents | 4385a7d0efd1 |
children | c54768cffbd4 |
line wrap: on
line source
#!/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 ################################################################################