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
+    ################################################################################