File indexing completed on 2024-04-28 07:51:06
0001 # -*- coding: utf-8 -*- 0002 0003 """ 0004 Copyright (C) 2009-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de> 0005 0006 SPDX-License-Identifier: GPL-2.0 0007 0008 """ 0009 0010 import traceback 0011 import datetime 0012 import weakref 0013 0014 from twisted.spread import pb 0015 from twisted.internet.defer import Deferred 0016 0017 from log import logInfo, logDebug, logException, id4 0018 from mi18n import i18nE 0019 from message import Message 0020 from common import Debug, StrMixin 0021 from move import Move 0022 0023 0024 class Request(StrMixin): 0025 0026 """holds a Deferred and related attributes, used as part of a DeferredBlock""" 0027 0028 def __init__(self, block, deferred, user, about): 0029 self._block = weakref.ref(block) 0030 self.deferred = deferred 0031 self._user = weakref.ref(user) 0032 self._about = weakref.ref(about) if about else None 0033 self.answer = None 0034 self.args = None 0035 self.startTime = datetime.datetime.now() 0036 player = self.block.playerForUser(user) 0037 self._player = weakref.ref(player) if player else None 0038 0039 @property 0040 def block(self): 0041 """hide weakref""" 0042 return self._block() if self._block else None 0043 0044 @property 0045 def user(self): 0046 """hide weakref""" 0047 return self._user() if self._user else None 0048 0049 @property 0050 def about(self): 0051 """hide weakref""" 0052 return self._about() if self._about else None 0053 0054 @property 0055 def player(self): 0056 """hide weakref""" 0057 return self._player() if self._player else None 0058 0059 def gotAnswer(self, rawAnswer): 0060 """convert the wired answer into something more useful""" 0061 if isinstance(rawAnswer, tuple): 0062 answer = rawAnswer[0] 0063 if isinstance(rawAnswer[1], tuple): 0064 self.args = rawAnswer[1] 0065 else: 0066 self.args = [rawAnswer[1]] 0067 else: 0068 answer = rawAnswer 0069 self.args = None 0070 if answer in Message.defined: 0071 self.answer = Message.defined[answer] 0072 else: 0073 if Debug.deferredBlock: 0074 logDebug('Request %s ignores %s' % (self, rawAnswer)) 0075 0076 def age(self): 0077 """my age in full seconds""" 0078 return int((datetime.datetime.now() - self.startTime).total_seconds()) 0079 0080 def __str__(self): 0081 cmd = self.deferred.command 0082 if self.answer: 0083 answer = str(self.answer) # TODO: needed? 0084 else: 0085 answer = 'OPEN' 0086 result = '' 0087 if Debug.deferredBlock: 0088 result += '[{id4:>4}] '.format(id4=id4(self)) 0089 result += '{cmd}->{cls}({receiver:<10}): {answer}'.format( 0090 cls=self.user.__class__.__name__, cmd=cmd, receiver=self.user.name, 0091 answer=answer) 0092 if self.age(): 0093 result += ' after {} sec'.format(self.age()) 0094 return result 0095 0096 def prettyAnswer(self): 0097 """for debug output""" 0098 if self.answer: 0099 result = str(self.answer) 0100 else: 0101 result = 'OPEN' 0102 if self.args: 0103 result += '(%s)' % ','.join(str(x) for x in self.args) 0104 return result 0105 0106 def pretty(self): 0107 """for debug output""" 0108 result = '' 0109 if Debug.deferredBlock: 0110 result += '[{id4:>4}] '.format(id4=id4(self)) 0111 result += '{cmd:<12}<-{cls:>6}({receiver:<10}): ANS={answer}'.format( 0112 cls=self.user.__class__.__name__, 0113 answer=self.prettyAnswer(), cmd=self.deferred.command, receiver=self.user.name) 0114 if self.age() > 0: 0115 result += ' after {} sec'.format(self.age()) 0116 return result 0117 0118 0119 class DeferredBlock(StrMixin): 0120 0121 """holds a list of deferreds and waits for each of them individually, 0122 with each deferred having its own independent callbacks. Fires a 0123 'general' callback after all deferreds have returned. 0124 Usage: 1. define, 2. add requests, 3. set callback""" 0125 0126 blocks = [] 0127 blockWarned = False # did we already warn about too many blocks? 0128 0129 def __init__(self, table, temp=False): 0130 dummy, dummy, function, dummy = traceback.extract_stack()[-2] 0131 self.outstanding = 0 0132 self.calledBy = function 0133 if not temp: 0134 self.garbageCollection() 0135 self.table = table 0136 self.requests = [] 0137 self.callbackMethod = None 0138 self.__callbackArgs = None 0139 self.completed = False 0140 if not temp: 0141 DeferredBlock.blocks.append(self) 0142 if not DeferredBlock.blockWarned: 0143 if len([x for x in DeferredBlock.blocks if x.table == table]) > 10: 0144 DeferredBlock.blockWarned = True 0145 logInfo('We have %d DBlocks:' % len(DeferredBlock.blocks)) 0146 for block in DeferredBlock.blocks: 0147 logInfo(str(block)) 0148 0149 def debugPrefix(self, dbgMarker=''): 0150 """prefix for debug message""" 0151 return 'T{table} B[{id4:>4}] {caller:<15} {dbgMarker:<3}(out={out})'.format( 0152 table=self.table.tableid, id4=id4(self), caller=self.calledBy[:15], 0153 dbgMarker=dbgMarker, out=self.outstanding) 0154 0155 def debug(self, dbgMarker, msg): 0156 """standard debug format""" 0157 logDebug(' '.join([self.debugPrefix(dbgMarker), msg])) 0158 0159 def __str__(self): 0160 return '%s requests=%s outstanding=%d %s callback=%s' % ( 0161 self.debugPrefix(), 0162 '[' + ','.join(str(x) for x in self.requests) + ']', 0163 self.outstanding, 0164 'is completed' if self.completed else 'not completed', 0165 self.prettyCallback()) 0166 0167 def outstandingStr(self): 0168 """like __str__ but only with outstanding answers""" 0169 return '%s callback=%s:%s' % (self.calledBy, self.prettyCallback(), 0170 '[' + ','.join(str(x) for x in self.requests if not x.answer) + ']') 0171 0172 @staticmethod 0173 def garbageCollection(): 0174 """delete completed blocks. Only to be called before 0175 inserting a new block. Assuming that block creation 0176 never overlaps.""" 0177 for block in DeferredBlock.blocks[:]: 0178 if block.callbackMethod is None: 0179 block.logBug('DBlock %s has no callback' % str(block)) 0180 if block.completed: 0181 DeferredBlock.blocks.remove(block) 0182 if len(DeferredBlock.blocks) > 100: 0183 logDebug( 0184 'We have %d DeferredBlocks, they must be leaking' % 0185 len(DeferredBlock.blocks)) 0186 0187 def __addRequest(self, deferred, user, about): 0188 """add deferred for user to this block""" 0189 assert self.callbackMethod is None, 'AddRequest: already have callback defined' 0190 assert not self.completed, 'AddRequest: already completed' 0191 request = Request(self, deferred, user, about) 0192 self.requests.append(request) 0193 self.outstanding += 1 0194 deferred.addCallback( 0195 self.__gotAnswer, 0196 request).addErrback( 0197 self.__failed, 0198 request) 0199 if Debug.deferredBlock: 0200 notifying = ' notifying' if deferred.notifying else '' 0201 rqString = '[{id4:>4}] {cmd}{notifying} {about}->{cls:>6}({receiver:<10})'.format( 0202 cls=user.__class__.__name__, 0203 id4=id4(request), cmd=deferred.command, receiver=user.name, 0204 about=about.name if about else '', notifying=notifying) 0205 self.debug('+:%d' % len(self.requests), rqString) 0206 0207 def removeRequest(self, request): 0208 """we do not want this request anymore""" 0209 self.requests.remove(request) 0210 if not request.answer: 0211 self.outstanding -= 1 0212 if Debug.deferredBlock: 0213 self.debug('-:%d' % self.outstanding, str(request)) # TODO: auch ohne? 0214 self.callbackIfDone() 0215 0216 def callback(self, method, *args): 0217 """to be done after all users answered""" 0218 assert not self.completed, 'callback already completed' 0219 assert self.callbackMethod is None, 'callback: no method defined' 0220 self.callbackMethod = method 0221 self.__callbackArgs = args 0222 if Debug.deferredBlock: 0223 self.debug('CB', self.prettyCallback()) 0224 self.callbackIfDone() 0225 0226 def __gotAnswer(self, result, request): 0227 """got answer from user""" 0228 if request in self.requests: 0229 # after having lost connection to client, an answer could still be 0230 # in the pipe 0231 if result is None: 0232 if Debug.deferredBlock: 0233 self.debug('IGN', request.pretty()) 0234 return 0235 request.gotAnswer(result) 0236 if hasattr(request.user, 'pinged'): 0237 # a Client (for robots) does not have it 0238 request.user.pinged() 0239 if Debug.deferredBlock: 0240 self.debug('ANS', request.pretty()) 0241 if hasattr(request.answer, 'notifyAction'): 0242 block = DeferredBlock(self.table, temp=True) 0243 receivers = request.answer.receivers(request) 0244 if receivers: 0245 block.tell( 0246 request.player, 0247 receivers, 0248 request.answer, 0249 notifying=True) 0250 self.outstanding -= 1 0251 assert self.outstanding >= 0, '__gotAnswer: outstanding %d' % self.outstanding 0252 self.callbackIfDone() 0253 else: 0254 if Debug.deferredBlock: 0255 self.debug('NOP', request.pretty()) 0256 0257 def __failed(self, result, request): 0258 """a user did not or not correctly answer""" 0259 if request in self.requests: 0260 self.removeRequest(request) 0261 if result.type in [pb.PBConnectionLost]: 0262 msg = i18nE('The game server lost connection to player %1') 0263 self.table.abort(msg, request.user.name) 0264 else: 0265 msg = i18nE('Error for player %1: %2\n%3') 0266 if hasattr(result, 'traceback'): 0267 traceBack = result.traceback 0268 else: 0269 traceBack = result.getBriefTraceback() 0270 self.table.abort( 0271 msg, 0272 request.user.name, 0273 result.getErrorMessage(), 0274 traceBack) 0275 0276 def logBug(self, msg): 0277 """log msg and raise exception""" 0278 for request in self.requests: 0279 logDebug(str(request)) # TODO: 0280 logException(msg) 0281 0282 def callbackIfDone(self): 0283 """if we are done, convert received answers to something more useful and callback""" 0284 if self.completed: 0285 return 0286 assert self.outstanding >= 0, 'callbackIfDone: outstanding %d' % self.outstanding 0287 if self.outstanding == 0 and self.callbackMethod is not None: 0288 self.completed = True 0289 if any(not x.answer for x in self.requests): 0290 self.logBug( 0291 'Block %s: Some requests are unanswered' % 0292 str(self)) 0293 if Debug.deferredBlock: 0294 commandText = [] 0295 for command in sorted({x.deferred.command for x in self.requests}): 0296 text = '%s:' % command 0297 answerList = [] 0298 for answer in sorted({x.prettyAnswer() for x in self.requests if x.deferred.command == command}): 0299 answerList.append((answer, [ 0300 x for x in self.requests 0301 if x.deferred.command == command and answer == x.prettyAnswer()])) 0302 answerList = sorted(answerList, key=lambda x: len(x[1])) 0303 answerTexts = [] 0304 if len(answerList) == 1: 0305 answerTexts.append( 0306 '{answer} from all'.format(answer=answerList[-1][0])) 0307 else: 0308 for answer, requests in answerList[:-1]: 0309 answerTexts.append( 0310 '{answer} from {players}'.format(answer=answer, 0311 players=','.join(x.user.name for x in requests))) 0312 answerTexts.append( 0313 '{answer} from others'.format(answer=answerList[-1][0])) 0314 text += ', '.join(answerTexts) 0315 commandText.append(text) 0316 methodName = self.prettyCallback() 0317 if methodName: 0318 methodName = ' next:%s' % methodName 0319 self.debug( 0320 'END', 0321 '{answers} {method}'.format(method=methodName, 0322 answers=' / '.join(commandText))) 0323 if self.callbackMethod is not False: 0324 self.callbackMethod(self.requests, *self.__callbackArgs) 0325 0326 def prettyCallback(self): 0327 """pretty string for callbackMethod""" 0328 if self.callbackMethod is False: 0329 result = '' 0330 elif self.callbackMethod is None: 0331 result = 'None' 0332 else: 0333 result = self.callbackMethod.__name__ 0334 if self.__callbackArgs: 0335 result += '({})'.format( 0336 ','.join([str(x) for x in self.__callbackArgs] if self.__callbackArgs else '')) 0337 return result 0338 0339 def playerForUser(self, user): 0340 """return the game player matching user""" 0341 if user.__class__.__name__.endswith('Player'): 0342 return user 0343 if self.table.game: 0344 for player in self.table.game.players: 0345 if user.name == player.name: 0346 return player 0347 return None 0348 0349 @staticmethod 0350 def __enrichMessage(game, about, command, kwargs): 0351 """add supplemental data for debugging""" 0352 if command.sendScore and about: 0353 # the clients will compare our status with theirs. This helps 0354 # very much in finding bugs. 0355 kwargs['score'] = str(about.hand) 0356 if game and game.gameid and 'token' not in kwargs: 0357 # this lets the client assert that the message is meant for the 0358 # current hand 0359 kwargs['token'] = game.handId.token() 0360 else: 0361 kwargs['token'] = None 0362 0363 def __convertReceivers(self, receivers): 0364 """try to convert Player to User or Client where possible""" 0365 for rec in receivers: 0366 if rec.__class__.__name__ == 'User': 0367 yield rec 0368 else: 0369 yield self.table.remotes[rec] 0370 0371 def tell(self, about, receivers, command, **kwargs): 0372 """send info about player 'about' to users 'receivers'""" 0373 def encodeKwargs(): 0374 """those values are classes like Meld, Tile etc. 0375 Convert to bytes""" 0376 for keyword in kwargs: 0377 if any(keyword.lower().endswith(x) for x in ('tile', 'tiles', 'meld', 'melds')): 0378 if kwargs[keyword] is not None: 0379 kwargs[keyword] = str(kwargs[keyword]) 0380 encodeKwargs() 0381 if about.__class__.__name__ == 'User': 0382 about = self.playerForUser(about) 0383 if not isinstance(receivers, list): 0384 receivers = list([receivers]) 0385 assert receivers, 'DeferredBlock.tell(%s) has no receiver' % command 0386 self.__enrichMessage(self.table.game, about, command, kwargs) 0387 aboutName = about.name if about else None 0388 if self.table.running and len(receivers) in [1, 4]: 0389 # messages are either identical for all 4 players 0390 # or identical for 3 players and different for 1 player. And 0391 # we want to capture each message exactly once. 0392 self.table.game.appendMove(about, command, kwargs) 0393 localDeferreds = [] 0394 for rec in self.__convertReceivers(receivers): 0395 0396 isClient = rec.__class__.__name__.endswith('Client') 0397 if isClient: 0398 defer = Deferred() 0399 defer.addCallback(rec.remote_move, command, **kwargs) 0400 defer.command = command.name 0401 defer.notifying = 'notifying' in kwargs 0402 self.__addRequest(defer, rec, about) 0403 localDeferreds.append(defer) 0404 else: 0405 if Debug.traffic: 0406 message = '-> {receiver:<15} about {about} {command}{kwargs}'.format( 0407 receiver=rec.name[:15], about=about, command=command, 0408 kwargs=Move.prettyKwargs(kwargs)) 0409 logDebug(message) 0410 defer = self.table.server.callRemote( 0411 rec, 0412 'move', 0413 aboutName, 0414 command.name, 0415 **kwargs) 0416 if defer: 0417 defer.command = command.name 0418 defer.notifying = 'notifying' in kwargs 0419 self.__addRequest(defer, rec, about) 0420 else: 0421 msg = i18nE('The game server lost connection to player %1') 0422 self.table.abort(msg, rec.name) 0423 0424 0425 for defer in localDeferreds: 0426 defer.callback(aboutName) # callback needs an argument ! 0427 0428 def tellPlayer(self, player, command, **kwargs): 0429 """address only one user""" 0430 self.tell(player, player, command, **kwargs) 0431 0432 def tellOthers(self, player, command, **kwargs): 0433 """tell others about player'""" 0434 self.tell( 0435 player, 0436 list( 0437 x for x in self.table.game.players if x.name != player.name), 0438 command, 0439 **kwargs) 0440 0441 def tellAll(self, player, command, **kwargs): 0442 """tell something to all players""" 0443 self.tell(player, self.table.game.players, command, **kwargs)