comparison 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
comparison
equal deleted inserted replaced
-1:000000000000 0:4385a7d0efd1
1 #!/usr/bin/python2.1
2 # Copyright (C) 2000-2001 The OpenRPG Project
3 #
4 # openrpg-dev@lists.sourceforge.net
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19 # --
20 #
21 # File: meta_server_lib.py
22 # Author: Chris Davis
23 # Maintainer:
24 # Version:
25 # $Id: meta_server_lib.py,v 1.40 2007/04/04 01:18:42 digitalxero Exp $
26 #
27 # Description: A collection of functions to communicate with the meta server.
28 #
29
30
31 #added debug flag for meta messages to cut console server spam --Snowdog
32 META_DEBUG = 0
33
34 __version__ = "$Id: meta_server_lib.py,v 1.40 2007/04/04 01:18:42 digitalxero Exp $"
35
36 from orpg.orpg_version import PROTOCOL_VERSION
37 from orpg.orpg_xml import *
38 import orpg.dirpath
39 import orpg.tools.validate
40 import urllib
41 import orpg.minidom
42 from threading import *
43 import time
44 import sys
45 import random
46 import traceback
47 import re
48
49 metacache_lock = RLock()
50
51 def get_server_dom(data=None,path=None):
52 # post data at server and get the resulting DOM
53
54 if path == None:
55 # get meta server URI
56 path = getMetaServerBaseURL()
57
58 # POST the data
59 if META_DEBUG:
60 print
61 print "Sending the following POST info to Meta at " + path + ":"
62 print "=========================================="
63 print data
64 print
65 file = urllib.urlopen(path, data)
66 data = file.read()
67 file.close()
68
69 # Remove any leading or trailing data. This can happen on some satellite connections
70 p = re.compile('(<servers>.*?</servers>)',re.DOTALL|re.IGNORECASE)
71 mo = p.search(data)
72 if mo:
73 data = mo.group(0)
74
75 if META_DEBUG:
76 print
77 print "Got this string from the Meta at " + path + ":"
78 print "==============================================="
79 print data
80 print
81 # build dom
82 xml_dom = parseXml(data)
83 xml_dom = xml_dom._get_documentElement()
84 return xml_dom
85
86 def post_server_data( name, realHostName=None):
87 # build POST data
88 ## data = urllib.urlencode( {"server_data[name]":name,
89 ## "server_data[version]":PROTOCOL_VERSION,
90 ## "act":"new"} )
91 ##
92 if realHostName:
93 data = urllib.urlencode( {"server_data[name]":name,
94 "server_data[version]":PROTOCOL_VERSION,
95 "act":"new",
96 "REMOTE_ADDR": realHostName } )
97
98 else:
99 #print "Letting meta server decide the hostname to list..."
100 data = urllib.urlencode( {"server_data[name]":name,
101 "server_data[version]":PROTOCOL_VERSION,
102 "act":"new"} )
103
104 xml_dom = get_server_dom( data , "http://openrpg.sf.net/openrpg_servers.php")
105 ret_val = int( xml_dom.getAttribute( "id" ) )
106 return ret_val
107
108 def post_failed_connection(id,meta=None,address=None,port=None):
109 # For now, turning this off. This needs to be re-vamped for
110 # handling multiple Metas.
111 return 0
112 # data = urllib.urlencode({"id":id,"act":"failed"});
113 # xml_dom = get_server_dom(data)
114 # ret_val = int(xml_dom.getAttribute("return"))
115 # return ret_val
116
117 def remove_server(id):
118 data = urllib.urlencode({"id":id,"act":"del"});
119 xml_dom = get_server_dom(data)
120 ret_val = int(xml_dom.getAttribute("return"))
121 return ret_val
122
123
124 def byStartAttribute(first,second):
125 # This function is used to easily sort a list of nodes
126 # by their start time
127
128 if first.hasAttribute("start"):
129 first_start = int(first.getAttribute("start"))
130 else:
131 first_start = 0
132
133 if second.hasAttribute("start"):
134 second_start = int(second.getAttribute("start"))
135 else:
136 second_start = 0
137
138 # Return the result of the cmp function on the two strings
139 return cmp(first_start,second_start)
140
141 def byNameAttribute(first,second):
142 # This function is used to easily sort a list of nodes
143 # by their name attribute
144
145 # Ensure there is something to sort with for each
146
147 if first.hasAttribute("name"):
148 first_name = str(first.getAttribute("name")).lower()
149 else:
150 first_name = ""
151
152 if second.hasAttribute("name"):
153 second_name = str(second.getAttribute("name")).lower()
154 else:
155 second_name = ""
156
157 # Return the result of the cmp function on the two strings
158
159 return cmp(first_name,second_name)
160
161
162 def get_server_list(versions = None,sort_by="start"):
163 data = urllib.urlencode({"version":PROTOCOL_VERSION,"ports":"%"})
164 all_metas = getMetaServers(versions,1) # get the list of metas
165 base_meta = getMetaServerBaseURL()
166
167 #all_metas.reverse() # The last one checked will take precedence, so reverse the order
168 # so that the top one on the actual list is checked last
169
170 return_hash = {} # this will end up with an amalgamated list of servers
171
172 for meta in all_metas: # check all of the metas
173
174 # get the server's xml from the current meta
175 bad_meta = 0
176 #print "Getting server list from " + meta + "..."
177 try:
178 xml_dom = get_server_dom(data=data,path=meta)
179 except:
180 #print "Trouble getting servers from " + meta + "..."
181 bad_meta = 1
182
183 if bad_meta:
184 continue
185
186 if base_meta == meta:
187 #print "This is our base meta: " + meta
188 updateMetaCache(xml_dom)
189
190 node_list = xml_dom.getElementsByTagName('server')
191
192 if len(node_list): # if there are entries in the node list
193 # otherwise, just loop to next meta
194
195 # for each node found, we're going to check the nodes from prior
196 # metas in the list. If a match is found, then use the new values.
197 for n in node_list:
198
199 # set them from current node
200
201 if not n.hasAttribute('name'):
202 n.setAttribute('name','NO_NAME_GIVEN')
203 name = n.getAttribute('name')
204 if not n.hasAttribute('num_users'):
205 n.setAttribute('num_users','N/A')
206 num_users = n.getAttribute('num_users')
207 if not n.hasAttribute('address'):
208 n.setAttribute('address','NO_ADDRESS_GIVEN')
209 address = n.getAttribute('address')
210 if not n.hasAttribute('port'):
211 n.setAttribute('port','6774')
212 port = n.getAttribute('port')
213 n.setAttribute('meta',meta)
214 end_point = str(address) + ":" + str(port)
215 if return_hash.has_key(end_point):
216 if META_DEBUG: print "Replacing duplicate server entry at " + end_point
217 return_hash[end_point] = n
218
219 # At this point, we have an amalgamated list of servers
220 # Now, we have to construct a new DOM to pass back.
221
222 # Create a servers element
223 return_dom = orpg.minidom.Element("servers")
224
225 # get the nodes stored in return_hash
226 return_list = return_hash.values()
227
228 # sort them by their name attribute. Uses byNameAttribute()
229 # defined above as a comparison function
230
231 if sort_by == "start":
232 return_list.sort(byStartAttribute)
233 elif sort_by == "name":
234 return_list.sort(byNameAttribute)
235
236 # Add each node to the DOM
237 for n in return_list:
238 return_dom.appendChild(n)
239 return return_dom
240
241 ## List Format:
242 ## <servers>
243 ## <server address=? id=? name=? failed_count=? >
244 ## </servers>
245
246 def updateMetaCache(xml_dom):
247 try:
248 if META_DEBUG: print "Updating Meta Server Cache"
249 metaservers = xml_dom.getElementsByTagName( 'metaservers' ) # pull out the metaservers bit
250 if len(metaservers) == 0:
251 cmetalist = getRawMetaList()
252 xml_dom = get_server_dom(cmetalist[0])
253 metaservers = xml_dom.getElementsByTagName( 'metaservers' )
254 authoritative = metaservers[0].getAttribute('auth')
255 if META_DEBUG: print " Authoritive Meta: "+str(authoritative)
256 metas = metaservers[0].getElementsByTagName("meta") # get the list of metas
257 if META_DEBUG: print " Meta List ("+str(len(metas))+" servers)"
258 try:
259 metacache_lock.acquire()
260 ini = open(orpg.dirpath.dir_struct["user"]+"metaservers.cache","w")
261 for meta in metas:
262 if META_DEBUG: print " Writing: "+str(meta.getAttribute('path'))
263 ini.write(str(meta.getAttribute('path')) + " " + str(meta.getAttribute('versions')) + "\n")
264 ini.close()
265 finally:
266 metacache_lock.release()
267 except Exception, e:
268 if META_DEBUG: traceback.print_exc()
269 print "Meta Server Lib: UpdateMetaCache(): " + str(e)
270
271 def getRawMetaList():
272 try:
273 try:
274 metacache_lock.acquire()
275 # Read in the metas
276 orpg.tools.validate.Validate().config_file("metaservers.cache","metaservers.cache")
277 ini = open(orpg.dirpath.dir_struct["user"]+"metaservers.cache","r")
278 metas = ini.readlines()
279 ini.close()
280 return metas
281 finally:
282 metacache_lock.release()
283 except Exception, e:
284 if META_DEBUG: traceback.print_exc()
285 print "Meta Server Lib: getRawMetaList(): " + str(e)
286 return []
287
288 def getMetaServers(versions = None, pick_random=0):
289 # get meta server URLs as a list
290
291 # versions is a list of acceptable version numbers.
292 # A False truth value will use getMetaServerBaseURL()
293
294 # set a default if we have weird reading problems
295 # default_url = "http://www.openrpg.com/openrpg_servers.php"
296
297 meta_names = []
298
299 if(versions): # If versions are supplied, then look in metaservers.conf
300 try:
301 # read in the metas from file
302 # format of file is one meta entry per line
303 # each entry will be the meta url, followed by one or more version numbers that it
304 # handle. Generally, this will be either a 1 for the original Meta format, or
305 # 2 for the new one.
306
307 # Read in the metas
308 metas = getRawMetaList()
309 #print str(metas)
310
311 # go through each one to check if it should be returned, based on the
312 # version numbers allowed.
313 for meta in metas:
314
315 # split the line on whitespace
316 # obviously, your meta servers urls shouldn't contain whitespace. duh.
317 words = meta.split()
318
319 success = 0 # init success flag for version check
320
321 for version in versions: # run through each allowed version from caller
322 if version in words[1:]: # if the allowed version token was found
323 success += 1 # then increment the success indicator
324
325 if success: # if the meta entry is acceptable to the caller
326 meta_names.append(words[0]) # add the entry
327 if META_DEBUG: print "adding metaserver " + meta
328
329 # at this point, we should have at least one name from the cache. If not ...
330 if not meta_names:
331 default_meta = getMetaServerBaseURL() # grab the meta from ini.xml
332 meta_names.append(default_meta) # add it to the return list
333 # print "Warning!!\nNo valid metaservers cached."
334 # print "Using meta from MetaServerBaseURL: " + default_meta + "\n"
335 # if we have more than one and want a random one
336 elif pick_random:
337 if META_DEBUG: print "choosing random meta from: " + str(meta_names)
338 i = int(random.uniform(0,len(meta_names)))
339 #meta = meta_names[i]
340 meta_names = [meta_names[i]]
341 if META_DEBUG: print "using: " + str(meta_names)
342 else:
343 if META_DEBUG: print "using all metas: " + str(meta_names)
344 return meta_names
345 except Exception,e:
346 print e
347 #print "using default meta server URI: " + default_url
348 metas = []
349 #metas.append(default_url)
350 return metas # return an empty list
351 else: # otherwise, use MetaServerBaseURL()
352 url = getMetaServerBaseURL()
353 meta_names.append(url)
354 return meta_names
355
356 def getMetaServerBaseURL():
357 # get meta server URL
358 url = "http://www.openrpg.com/openrpg_servers.php"
359 try:
360 orpg.tools.validate.Validate().config_file("settings.xml","default_settings.xml")
361 ini = open(orpg.dirpath.dir_struct["user"]+"settings.xml","r")
362 txt = ini.read()
363 tree = parseXml(txt)._get_documentElement()
364 ini.close()
365 node_list = tree.getElementsByTagName("MetaServerBaseURL")
366 if node_list:
367 url = node_list[0].getAttribute("value")
368
369 # allow tree to be collected
370 try:
371 tree.unlink()
372 except:
373 pass
374
375 except Exception,e:
376 print e
377 # print "using meta server URI: " + url
378 return url
379
380 #######################################################################################
381 # Beginning of Class registerThread
382 #
383 # A Class to Manage Registration with the Meta2
384 # Create an instance and call it's start() method
385 # if you want to be (and stay) registered. This class
386 # will take care of registering and re-registering as
387 # often as necessary to stay in the Meta list.
388 #
389 # You may call register() yourself if you wish to change your
390 # server's name. It will immediately update the Meta. There
391 # is no need to unregister first.
392 #
393 # Call unregister() when you no longer want to be registered.
394 # This will result in the registerThread dying after
395 # attempting to immediately remove itself from the Meta.
396 #
397 # If you need to become registered again after that, you
398 # must create a new instance of class registerThread. Don't
399 # just try to call register() on the old, dead thread class.
400
401
402 class registerThread(Thread):
403 # Originally, I wrote this as a sub-class of wxThread, but
404 # A) I couldn't get it to import right
405 # B) I realized that I want this to be used in a server,
406 # which I don't want needing wxWindows to run!
407 #
408 # Because of this fact, there are some methods from wxThread
409 # that I implemented to minimize changes to the code I had
410 # just written, i.e. TestDeleteStatus() and Delete()
411
412 def __init__(self,name=None,realHostName=None,num_users = "Hmmm",MetaPath=None,port=6774,register_callback=None):
413
414 Thread.__init__(self,name="registerThread")
415 self.rlock = RLock() # Re-entrant lock used to make this class thread safe
416 self.die_event = Event() # The main loop in run() will wait with timeout on this
417 if name:
418 self.name = name # Name that the server want's displayed on the Meta
419 else:
420 self.name = "Unnamed server" # But use this if for some crazy reason no name is
421 # passed to the constructor
422 self.num_users = num_users # the number of users currently on this server
423 self.realHostName = realHostName # Name to advertise for connection
424 self.id = "0" # id returned from Meta. Defaults to "0", which
425 # indicates a new registration.
426 self.cookie = "0" # cookie returned from Meta. Defaults to "0",which
427 # indicates a new registration.
428 self.interval = 0 # interval returned from Meta. Is how often to
429 # re-register, in minutes.
430 self.destroy = 0 # Used to flag that this thread should die
431 self.port = str(port)
432 self.register_callback = register_callback # set a method to call to report result of register
433 # This thread will communicate with one and only one
434 # Meta. If the Meta in ini.xml is changed after
435 # instantiation, then this instance must be
436 # unregistered and a new instance instantiated.
437 #
438 # Also, if MetaPath is specified, then use that. Makes
439 # it easier to have multiple registerThreads going to keep the server registered
440 # on multiple (compatible) Metas.
441
442 if MetaPath == None:
443 self.path = getMetaServerBaseURL() # Do this if no Meta specified
444 else:
445 self.path = MetaPath
446
447 def getIdAndCookie(self):
448 return self.id, self.cookie
449
450 def TestDeleteStatus(self):
451 try:
452 self.rlock.acquire()
453 return self.die_event.isSet()
454 finally:
455 self.rlock.release()
456
457 def Delete(self):
458 try:
459 self.rlock.acquire()
460 self.die_event.set()
461 finally:
462 self.rlock.release()
463
464 def run(self):
465 # This method gets called by Thread implementation
466 # when self.start() is called to begin the thread's
467 # execution
468 #
469 # We will basically enter a loop that continually
470 # re-registers this server and sleeps Interval
471 # minutes until the thread is ordered to die in place
472 while(not self.TestDeleteStatus()): # Loop while until told to die
473 # Otherwise, call thread safe register().
474 self.register(self.name, self.realHostName, self.num_users)
475 if META_DEBUG: print "Sent Registration Data"
476
477 # register() will end up setting the state variables
478 # for us, including self.interval.
479 try:
480 self.rlock.acquire() # Serialize access to this state information
481
482 if self.interval >= 3: # If the number of minutes is one or greater
483 self.interval -= 1 # wake up with 30 seconds left to re-register
484 else:
485 self.interval = .5 # Otherwise, we probably experienced some kind
486 # of error from the Meta in register(). Sleep
487 # for 6 seconds and start from scratch.
488
489 finally: # no matter what, release the lock
490 self.rlock.release()
491 # Wait interval minutes for a command to die
492 die_signal = self.die_event.wait(self.interval*60)
493
494 # If we get past the while loop, it's because we've been asked to die,
495 # so just let run() end. Once this occurs, the thread is dead and
496 # calls to Thread.isAlive() return False.
497
498 def unregister(self):
499 # This method can (I hope) be called from both within the thread
500 # and from other threads. It will attempt to unregister this
501 # server from the Meta database
502 # When this is either accomplished or has been tried hard enough
503 # (after which it just makes sense to let the Meta remove the
504 # entry itself when we don't re-register using this id),
505 # this method will either cause the thread to immediately die
506 # (if called from this thread's context) or set the Destroy flag
507 # (if called from the main thread), a positive test for which will cause
508 # the code in Entry() to exit() when the thread wakes up and
509 # checks TestDeleteStatus().
510 # lock the critical section. The unlock will
511 # automatically occur at the end of the function in the finally clause
512 try:
513 self.rlock.acquire()
514 if not self.isAlive(): # check to see if this thread is dead
515 return 1 # If so, return an error result
516 # Do the actual unregistering here
517 data = urllib.urlencode( {"server_data[id]":self.id,
518 "server_data[cookie]":self.cookie,
519 "server_data[version]":PROTOCOL_VERSION,
520 "act":"unregister"} )
521 try:
522 xml_dom = get_server_dom(data=data, path=self.path) # this POSTS the request and returns the result
523 if xml_dom.hasAttribute("errmsg"):
524 print "Error durring unregistration: " + xml_dom.getAttribute("errmsg")
525 except:
526 if META_DEBUG: print "Problem talking to Meta. Will go ahead and die, letting Meta remove us."
527 # If there's an error, echo it to the console
528
529 # No special handling is required. If the de-registration worked we're done. If
530 # not, then it's because we've already been removed or have a bad cookie. Either
531 # way, we can't do anything else, so die.
532 self.Delete() # This will cause the registerThread to die in register()
533 # prep xml_dom for garbage collection
534 try:
535 xml_dom.unlink()
536 except:
537 pass
538 return 0
539 finally:
540 self.rlock.release()
541
542 def register(self, name=None, realHostName=None, num_users=None):
543 # Designed to handle the registration, both new and
544 # repeated.
545 #
546 # It is intended to be called once every interval
547 # (or interval - delta) minutes.
548
549 # lock the critical section. The unlock will
550 # automatically occur at the end of the function in the finally clause
551 try:
552 self.rlock.acquire()
553 if not self.isAlive(): # check to see if this thread is dead
554 return 1 # If so, return an error result
555
556 # Set the server's attibutes, if specified.
557 if name:
558 self.name = name
559 if num_users != None:
560 self.num_users = num_users
561 if realHostName:
562 self.realHostName = realHostName
563 # build POST data
564 if self.realHostName:
565 data = urllib.urlencode( {"server_data[id]":self.id,
566 "server_data[cookie]":self.cookie,
567 "server_data[name]":self.name,
568 "server_data[port]":self.port,
569 "server_data[version]":PROTOCOL_VERSION,
570 "server_data[num_users]":self.num_users,
571 "act":"register",
572 "server_data[address]": self.realHostName } )
573 else:
574 if META_DEBUG: print "Letting meta server decide the hostname to list..."
575 data = urllib.urlencode( {"server_data[id]":self.id,
576 "server_data[cookie]":self.cookie,
577 "server_data[name]":self.name,
578 "server_data[port]":self.port,
579 "server_data[version]":PROTOCOL_VERSION,
580 "server_data[num_users]":self.num_users,
581 "act":"register"} )
582 try:
583 xml_dom = get_server_dom(data=data,path=self.path) # this POSTS the request and returns the result
584 except:
585 if META_DEBUG: print "Problem talking to server. Setting interval for retry ..."
586 if META_DEBUG: print data
587 if META_DEBUG: print
588 self.interval = 0
589 # If we are in the registerThread thread, then setting interval to 0
590 # will end up causing a retry in about 6 seconds (see self.run())
591 # If we are in the main thread, then setting interval to 0 will do one
592 # of two things:
593 # 1) Do the same as if we were in the registerThread
594 # 2) Cause the next, normally scheduled register() call to use the values
595 # provided in this call.
596 #
597 # Which case occurs depends on where the registerThread thread is when
598 # the main thread calls register().
599 return 0 # indicates that it was okay to call, not that no errors occurred
600 # If there is a DOM returned ....
601 if xml_dom:
602 # If there's an error, echo it to the console
603 if xml_dom.hasAttribute("errmsg"):
604 print "Error durring registration: " + xml_dom.getAttribute("errmsg")
605 if META_DEBUG: print data
606 if META_DEBUG: print
607 # No special handling is required. If the registration worked, id, cookie, and interval
608 # can be stored and used for the next time.
609 # If an error occurred, then the Meta will delete us and we need to re-register as
610 # a new server. The way to indicate this is with a "0" id and "0" cookie sent to
611 # the server during the next registration. Since that's what the server returns to
612 # us on an error anyway, we just store them and the next registration will
613 # automatically be set up as a new one.
614 #
615 # Unless the server calls register() itself in the meantime. Of course, that's okay
616 # too, because a success on THAT register() call will set up the next one to use
617 # the issued id and cookie.
618 #
619 # The interval is stored unconditionally for similar reasons. If there's an error,
620 # the interval will be less than 1, and the main thread's while loop will reset it
621 # to 6 seconds for the next retry.
622 # Is it wrong to have a method where there's more comments than code? :)
623 try:
624 self.interval = int(xml_dom.getAttribute("interval"))
625 self.id = xml_dom.getAttribute("id")
626 self.cookie = xml_dom.getAttribute("cookie")
627 if not xml_dom.hasAttribute("errmsg"):
628 updateMetaCache(xml_dom)
629 except:
630 if META_DEBUG: print
631 if META_DEBUG: print "OOPS! Is the Meta okay? It should be returning an id, cookie, and interval."
632 if META_DEBUG: print "Check to see what it really returned.\n"
633 # Let xml_dom get garbage collected
634 try:
635 xml_dom.unlink()
636 except:
637 pass
638 else: # else if no DOM is returned from get_server_dom()
639 print "Error - no DOM constructed from Meta message!"
640 return 0 # Let caller know it was okay to call us
641 finally:
642 self.rlock.release()
643 # End of class registerThread
644 ################################################################################