ConnectionRepository.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. from pandac.PandaModules import *
  2. from direct.task import Task
  3. from direct.directnotify import DirectNotifyGlobal
  4. from direct.distributed.DoInterestManager import DoInterestManager
  5. from PyDatagram import PyDatagram
  6. from PyDatagramIterator import PyDatagramIterator
  7. import types
  8. import imp
  9. class ConnectionRepository(DoInterestManager, CConnectionRepository):
  10. """
  11. This is a base class for things that know how to establish a
  12. connection (and exchange datagrams) with a gameserver. This
  13. includes ClientRepository and AIRepository.
  14. """
  15. notify = DirectNotifyGlobal.directNotify.newCategory("ConnectionRepository")
  16. taskPriority = -30
  17. def __init__(self, config):
  18. assert self.notify.debugCall()
  19. DoInterestManager.__init__(self)
  20. CConnectionRepository.__init__(self)
  21. self.setPythonRepository(self)
  22. self.config = config
  23. # Set this to 'http' to establish a connection to the server
  24. # using the HTTPClient interface, which ultimately uses the
  25. # OpenSSL socket library (even though SSL is not involved).
  26. # This is not as robust a socket library as NSPR's, but the
  27. # HTTPClient interface does a good job of negotiating the
  28. # connection over an HTTP proxy if one is in use.
  29. #
  30. # Set it to 'nspr' to use Panda's net interface
  31. # (e.g. QueuedConnectionManager, etc.) to establish the
  32. # connection, which ultimately uses the NSPR socket library.
  33. # This is a much better socket library, but it may be more
  34. # than you need for most applications; and there is no support
  35. # for proxies.
  36. #
  37. # Set it to 'default' to use the HTTPClient interface if a
  38. # proxy is in place, but the NSPR interface if we don't have a
  39. # proxy.
  40. self.connectMethod = self.config.GetString('connect-method', 'default')
  41. self.connectHttp = None
  42. self.http = None
  43. # This DatagramIterator is constructed once, and then re-used
  44. # each time we read a datagram.
  45. self.__di = PyDatagramIterator()
  46. self.recorder = None
  47. # This is the string that is appended to symbols read from the
  48. # DC file. The AIRepository will redefine this to 'AI'.
  49. self.dcSuffix = ''
  50. def readDCFile(self, dcFileNames = None):
  51. """
  52. Reads in the dc files listed in dcFileNames, or if
  53. dcFileNames is None, reads in all of the dc files listed in
  54. the Configrc file.
  55. """
  56. dcFile = self.getDcFile()
  57. dcFile.clear()
  58. self.dclassesByName = {}
  59. self.dclassesByNumber = {}
  60. self.hashVal = 0
  61. dcImports = {}
  62. if dcFileNames == None:
  63. readResult = dcFile.readAll()
  64. if not readResult:
  65. self.notify.error("Could not read dc file.")
  66. else:
  67. for dcFileName in dcFileNames:
  68. readResult = dcFile.read(Filename(dcFileName))
  69. if not readResult:
  70. self.notify.error("Could not read dc file: %s" % (dcFileName))
  71. self.hashVal = dcFile.getHash()
  72. # Now import all of the modules required by the DC file.
  73. for n in range(dcFile.getNumImportModules()):
  74. moduleName = dcFile.getImportModule(n)
  75. # Maybe the module name is represented as "moduleName/AI".
  76. suffix = moduleName.split('/')
  77. moduleName = suffix[0]
  78. if self.dcSuffix and self.dcSuffix in suffix[1:]:
  79. moduleName += self.dcSuffix
  80. elif self.dcSuffix == 'UD' and 'AI' in suffix[1:]: #HACK:
  81. moduleName += 'AI'
  82. importSymbols = []
  83. for i in range(dcFile.getNumImportSymbols(n)):
  84. symbolName = dcFile.getImportSymbol(n, i)
  85. # Maybe the symbol name is represented as "symbolName/AI".
  86. suffix = symbolName.split('/')
  87. symbolName = suffix[0]
  88. if self.dcSuffix and self.dcSuffix in suffix[1:]:
  89. symbolName += self.dcSuffix
  90. elif self.dcSuffix == 'UD' and 'AI' in suffix[1:]: #HACK:
  91. symbolName += 'AI'
  92. importSymbols.append(symbolName)
  93. self.importModule(dcImports, moduleName, importSymbols)
  94. # Now get the class definition for the classes named in the DC
  95. # file.
  96. for i in range(dcFile.getNumClasses()):
  97. dclass = dcFile.getClass(i)
  98. number = dclass.getNumber()
  99. className = dclass.getName() + self.dcSuffix
  100. # Does the class have a definition defined in the newly
  101. # imported namespace?
  102. classDef = dcImports.get(className)
  103. if classDef is None and self.dcSuffix == 'UD': #HACK:
  104. className = dclass.getName() + 'AI'
  105. classDef = dcImports.get(className)
  106. # Also try it without the dcSuffix.
  107. if classDef == None:
  108. className = dclass.getName()
  109. classDef = dcImports.get(className)
  110. if classDef is None:
  111. self.notify.info("No class definition for %s." % (className))
  112. else:
  113. if type(classDef) == types.ModuleType:
  114. if not hasattr(classDef, className):
  115. self.notify.error("Module %s does not define class %s." % (className, className))
  116. classDef = getattr(classDef, className)
  117. if type(classDef) != types.ClassType:
  118. self.notify.error("Symbol %s is not a class name." % (className))
  119. else:
  120. dclass.setClassDef(classDef)
  121. self.dclassesByName[className] = dclass
  122. if number >= 0:
  123. self.dclassesByNumber[number] = dclass
  124. def importModule(self, dcImports, moduleName, importSymbols):
  125. """
  126. Imports the indicated moduleName and all of its symbols
  127. into the current namespace. This more-or-less reimplements
  128. the Python import command.
  129. """
  130. module = __import__(moduleName, globals(), locals(), importSymbols)
  131. if importSymbols:
  132. # "from moduleName import symbolName, symbolName, ..."
  133. # Copy just the named symbols into the dictionary.
  134. if importSymbols == ['*']:
  135. # "from moduleName import *"
  136. if hasattr(module, "__all__"):
  137. importSymbols = module.__all__
  138. else:
  139. importSymbols = module.__dict__.keys()
  140. for symbolName in importSymbols:
  141. if hasattr(module, symbolName):
  142. dcImports[symbolName] = getattr(module, symbolName)
  143. else:
  144. raise StandardError, 'Symbol %s not defined in module %s.' % (symbolName, moduleName)
  145. else:
  146. # "import moduleName"
  147. # Copy the root module name into the dictionary.
  148. # Follow the dotted chain down to the actual module.
  149. components = moduleName.split('.')
  150. dcImports[components[0]] = module
  151. def connect(self, serverList,
  152. successCallback = None, successArgs = [],
  153. failureCallback = None, failureArgs = []):
  154. """
  155. Attempts to establish a connection to the server. May return
  156. before the connection is established. The two callbacks
  157. represent the two functions to call (and their arguments) on
  158. success or failure, respectively. The failure callback also
  159. gets one additional parameter, which will be passed in first:
  160. the return status code giving reason for failure, if it is
  161. known.
  162. """
  163. ## if self.recorder and self.recorder.isPlaying():
  164. ## # If we have a recorder and it's already in playback mode,
  165. ## # don't actually attempt to connect to a gameserver since
  166. ## # we don't need to. Just let it play back the data.
  167. ## self.notify.info("Not connecting to gameserver; using playback data instead.")
  168. ## self.connectHttp = 1
  169. ## self.tcpConn = SocketStreamRecorder()
  170. ## self.recorder.addRecorder('gameserver', self.tcpConn)
  171. ## self.startReaderPollTask()
  172. ## if successCallback:
  173. ## successCallback(*successArgs)
  174. ## return
  175. hasProxy = 0
  176. if self.checkHttp():
  177. proxies = self.http.getProxiesForUrl(serverList[0])
  178. hasProxy = (proxies != 'DIRECT')
  179. if hasProxy:
  180. self.notify.info("Connecting to gameserver via proxy list: %s" % (proxies))
  181. else:
  182. self.notify.info("Connecting to gameserver directly (no proxy).");
  183. if self.connectMethod == 'http':
  184. self.connectHttp = 1
  185. elif self.connectMethod == 'nspr':
  186. self.connectHttp = 0
  187. else:
  188. self.connectHttp = (hasProxy or serverList[0].isSsl())
  189. self.bootedIndex = None
  190. self.bootedText = None
  191. if self.connectHttp:
  192. # In the HTTP case, we can't just iterate through the list
  193. # of servers, because each server attempt requires
  194. # spawning a request and then coming back later to check
  195. # the success or failure. Instead, we start the ball
  196. # rolling by calling the connect callback, which will call
  197. # itself repeatedly until we establish a connection (or
  198. # run out of servers).
  199. ch = self.http.makeChannel(0)
  200. self.httpConnectCallback(
  201. ch, serverList, 0,
  202. successCallback, successArgs,
  203. failureCallback, failureArgs)
  204. else:
  205. # Try each of the servers in turn.
  206. for url in serverList:
  207. self.notify.info("Connecting to %s via NSPR interface." % (url.cStr()))
  208. if self.tryConnectNspr(url):
  209. self.startReaderPollTask()
  210. if successCallback:
  211. successCallback(*successArgs)
  212. return
  213. # Failed to connect.
  214. if failureCallback:
  215. failureCallback(0, '', *failureArgs)
  216. def disconnect(self):
  217. """
  218. Closes the previously-established connection.
  219. """
  220. self.notify.info("Closing connection to server.")
  221. CConnectionRepository.disconnect(self)
  222. self.stopReaderPollTask()
  223. def httpConnectCallback(self, ch, serverList, serverIndex,
  224. successCallback, successArgs,
  225. failureCallback, failureArgs):
  226. if ch.isConnectionReady():
  227. self.setConnectionHttp(ch)
  228. ## if self.recorder:
  229. ## # If we have a recorder, we wrap the connect inside a
  230. ## # SocketStreamRecorder, which will trap incoming data
  231. ## # when the recorder is set to record mode. (It will
  232. ## # also play back data when the recorder is in playback
  233. ## # mode, but in that case we never get this far in the
  234. ## # code, since we just create an empty
  235. ## # SocketStreamRecorder without actually connecting to
  236. ## # the gameserver.)
  237. ## stream = SocketStreamRecorder(self.tcpConn, 1)
  238. ## self.recorder.addRecorder('gameserver', stream)
  239. ## # In this case, we pass ownership of the original
  240. ## # connection to the SocketStreamRecorder object.
  241. ## self.tcpConn.userManagesMemory = 0
  242. ## self.tcpConn = stream
  243. self.startReaderPollTask()
  244. if successCallback:
  245. successCallback(*successArgs)
  246. elif serverIndex < len(serverList):
  247. # No connection yet, but keep trying.
  248. url = serverList[serverIndex]
  249. self.notify.info("Connecting to %s via HTTP interface." % (url.cStr()))
  250. ch.preserveStatus()
  251. ch.beginConnectTo(DocumentSpec(url))
  252. ch.spawnTask(name = 'connect-to-server',
  253. callback = self.httpConnectCallback,
  254. extraArgs = [ch, serverList, serverIndex + 1,
  255. successCallback, successArgs,
  256. failureCallback, failureArgs])
  257. else:
  258. # No more servers to try; we have to give up now.
  259. if failureCallback:
  260. failureCallback(ch.getStatusCode(), ch.getStatusString(),
  261. *failureArgs)
  262. def checkHttp(self):
  263. # Creates an HTTPClient, if possible, if we don't have one
  264. # already. This might fail if the OpenSSL library isn't
  265. # available. Returns the HTTPClient (also self.http), or None
  266. # if not set.
  267. if self.http == None:
  268. try:
  269. self.http = HTTPClient()
  270. except:
  271. pass
  272. return self.http
  273. def startReaderPollTask(self):
  274. # Stop any tasks we are running now
  275. self.stopReaderPollTask()
  276. self.accept(CConnectionRepository.getOverflowEventName(),
  277. self.handleReaderOverflow)
  278. taskMgr.add(self.readerPollUntilEmpty, "readerPollTask",
  279. priority = self.taskPriority)
  280. def stopReaderPollTask(self):
  281. taskMgr.remove("readerPollTask")
  282. self.ignore(CConnectionRepository.getOverflowEventName())
  283. def readerPollUntilEmpty(self, task):
  284. while self.readerPollOnce():
  285. pass
  286. return Task.cont
  287. def readerPollOnce(self):
  288. if self.checkDatagram():
  289. self.getDatagramIterator(self.__di)
  290. self.handleDatagram(self.__di)
  291. return 1
  292. # Unable to receive a datagram: did we lose the connection?
  293. if not self.isConnected():
  294. self.stopReaderPollTask()
  295. self.lostConnection()
  296. return 0
  297. def handleReaderOverflow(self):
  298. # this is called if the incoming-datagram queue overflowed and
  299. # we lost some data. Override and handle if desired.
  300. pass
  301. def lostConnection(self):
  302. # This should be overrided by a derived class to handle an
  303. # unexpectedly lost connection to the gameserver.
  304. self.notify.warning("Lost connection to gameserver.")
  305. def handleDatagram(self, di):
  306. # This class is meant to be pure virtual, and any classes that
  307. # inherit from it need to make their own handleDatagram method
  308. pass
  309. def send(self, datagram):
  310. if self.notify.getDebug():
  311. print "ConnectionRepository sending datagram:"
  312. datagram.dumpHex(ostream)
  313. self.sendDatagram(datagram)
  314. # debugging funcs for simulating a network-plug-pull
  315. def pullNetworkPlug(self):
  316. self.notify.warning('*** SIMULATING A NETWORK-PLUG-PULL ***')
  317. self.setSimulatedDisconnect(1)
  318. def networkPlugPulled(self):
  319. return self.getSimulatedDisconnect()
  320. def restoreNetworkPlug(self):
  321. if self.networkPlugPulled():
  322. self.notify.info('*** RESTORING SIMULATED PULLED-NETWORK-PLUG ***')
  323. self.setSimulatedDisconnect(0)
  324. def doFind(self, str):
  325. """
  326. Returns list of distributed objects with matching str in value.
  327. """
  328. for value in self.doId2do.values():
  329. if `value`.find(str) >= 0:
  330. return value
  331. def doFindAll(self, str):
  332. """
  333. Returns list of distributed objects with matching str in value.
  334. """
  335. matches = []
  336. for value in self.doId2do.values():
  337. if `value`.find(str) >= 0:
  338. matches.append(value)
  339. return matches