File indexing completed on 2024-04-21 04:01:48

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)