File indexing completed on 2024-04-21 04:01:50
0001 # -*- coding: utf-8 -*- 0002 0003 """ 0004 Copyright (C) 2008-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de> 0005 0006 SPDX-License-Identifier: GPL-2.0 0007 0008 """ 0009 0010 import logging 0011 import os 0012 import string 0013 0014 from locale import getpreferredencoding 0015 from sys import _getframe 0016 0017 # util must not import twisted or we need to change kajongg.py 0018 0019 from common import Internal, Debug # pylint: disable=redefined-builtin 0020 from qt import Qt, QEvent 0021 from util import elapsedSince, traceback, gitHead, callers 0022 from mi18n import i18n 0023 from dialogs import Sorry, Information, NoPrompt 0024 0025 0026 SERVERMARK = '&&SERVER&&' 0027 0028 0029 class Fmt(string.Formatter): 0030 0031 """this formatter can parse {id(x)} and output a short ascii form for id""" 0032 alphabet = string.ascii_uppercase + string.ascii_lowercase 0033 base = len(alphabet) 0034 formatter = None 0035 0036 @staticmethod 0037 def num_encode(number, length=4): 0038 """make a short unique ascii string out of number, truncate to length""" 0039 result = [] 0040 while number and len(result) < length: 0041 number, remainder = divmod(number, Fmt.base) 0042 result.append(Fmt.alphabet[remainder]) 0043 return ''.join(reversed(result)) 0044 0045 def get_value(self, key, args, kwargs): 0046 if key.startswith('id(') and key.endswith(')'): 0047 idpar = key[3:-1] 0048 if idpar == 'self': 0049 idpar = 'SELF' 0050 if kwargs[idpar] is None: 0051 return 'None' 0052 if Debug.neutral: 0053 return '....' 0054 return Fmt.num_encode(id(kwargs[idpar])) 0055 if key == 'self': 0056 return kwargs['SELF'] 0057 return kwargs[key] 0058 0059 Fmt.formatter = Fmt() 0060 0061 def id4(obj): 0062 """object id for debug messages""" 0063 if obj is None: 0064 return 'NONE' 0065 if hasattr(obj, 'uid'): 0066 return obj.uid 0067 return '.' if Debug.neutral else Fmt.num_encode(id(obj)) 0068 0069 def fmt(text, **kwargs): 0070 """use the context dict for finding arguments. 0071 For something like {self} output 'self:selfValue'""" 0072 if '}' in text: 0073 parts = [] 0074 for part in text.split('}'): 0075 if '{' not in part: 0076 parts.append(part) 0077 else: 0078 part2 = part.split('{') 0079 if part2[1] == 'callers': 0080 if part2[0]: 0081 parts.append('%s:{%s}' % (part2[0], part2[1])) 0082 else: 0083 parts.append('{%s}' % part2[1]) 0084 else: 0085 showName = part2[1] + ':' 0086 if showName.startswith('_hide'): 0087 showName = '' 0088 if showName.startswith('self.'): 0089 showName = showName[5:] 0090 parts.append('%s%s{%s}' % (part2[0], showName, part2[1])) 0091 text = ''.join(parts) 0092 argdict = _getframe(1).f_locals 0093 argdict.update(kwargs) 0094 if 'self' in argdict: 0095 # formatter.format will not accept 'self' as keyword 0096 argdict['SELF'] = argdict['self'] 0097 del argdict['self'] 0098 return Fmt.formatter.format(text, **argdict) 0099 0100 0101 def translateServerMessage(msg): 0102 """because a PB exception can not pass a list of arguments, the server 0103 encodes them into one string using SERVERMARK as separator. That 0104 string is always english. Here we unpack and translate it into the 0105 client language.""" 0106 if msg.find(SERVERMARK) >= 0: 0107 return i18n(*tuple(msg.split(SERVERMARK)[1:-1])) 0108 return msg 0109 0110 0111 def dbgIndent(this, parent): 0112 """show messages indented""" 0113 if this.indent == 0: 0114 return '' 0115 pIndent = parent.indent if parent else 0 0116 return (' │ ' * (pIndent)) + ' ├' + '─' * (this.indent - pIndent - 1) 0117 0118 0119 def __logUnicodeMessage(prio, msg): 0120 """if we can encode the str msg to ascii, do so. 0121 Otherwise convert the str object into an utf-8 encoded 0122 str object. 0123 The logger module would log the str object with the 0124 marker feff at the beginning of every message, we do not want that.""" 0125 msg = msg.encode(getpreferredencoding(), 'ignore')[:4000] 0126 msg = msg.decode(getpreferredencoding()) 0127 Internal.logger.log(prio, msg) 0128 0129 0130 def __enrichMessage(msg, withGamePrefix=True): 0131 """ 0132 Add some optional prefixes to msg: S/C, process id, time, git commit. 0133 0134 @param msg: The original message. 0135 @type msg: C{str} 0136 @param withGamePrefix: If set, prepend the game prefix. 0137 @type withGamePrefix: C{Boolean} 0138 @rtype: C{str} 0139 """ 0140 result = msg # set the default 0141 if withGamePrefix and Internal.logPrefix: 0142 result = '{prefix}{process}: {msg}'.format( 0143 prefix=Internal.logPrefix, 0144 process=os.getpid() if Debug.process else '', 0145 msg=msg) 0146 if Debug.time: 0147 result = '{:08.4f} {}'.format(elapsedSince(Debug.time), result) 0148 if Debug.git: 0149 head = gitHead() 0150 if head not in ('current', None): 0151 result = 'git:{}/p3 {}'.format(head, result) 0152 if int(Debug.callers): 0153 result = ' ' + result 0154 return result 0155 0156 0157 def __exceptionToString(exception): 0158 """ 0159 Convert exception into a useful string for logging. 0160 0161 @param exception: The exception to be logged. 0162 @type exception: C{Exception} 0163 0164 @rtype: C{str} 0165 """ 0166 parts = [] 0167 for arg in exception.args: 0168 if hasattr(arg, 'strerror'): 0169 # when using py kde 4, this is already translated at this point 0170 # but I do not know what it does differently with gettext and if 0171 # I can do the same with the python gettext module 0172 parts.append( 0173 '[Errno {}] {}'.format(arg.errno, i18n(arg.strerror))) 0174 elif arg is None: 0175 pass 0176 else: 0177 parts.append(str(arg)) 0178 if hasattr(exception, 'filename'): 0179 parts.append(exception.filename) 0180 return ' '.join(parts) 0181 0182 0183 def logMessage(msg, prio, showDialog, showStack=False, withGamePrefix=True): 0184 """writes info message to log and to stdout""" 0185 # pylint: disable=R0912 0186 if isinstance(msg, Exception): 0187 msg = __exceptionToString(msg) 0188 msg = str(msg) 0189 msg = translateServerMessage(msg) 0190 __logUnicodeMessage(prio, __enrichMessage(msg, withGamePrefix)) 0191 if showStack: 0192 if showStack is True: 0193 lower = 2 0194 else: 0195 lower = -showStack - 3 0196 for line in traceback.format_stack()[lower:-3]: 0197 if 'logException' not in line: 0198 __logUnicodeMessage(prio, ' ' + line.strip()) 0199 if int(Debug.callers): 0200 __logUnicodeMessage(prio, callers(int(Debug.callers))) 0201 if showDialog and not Internal.isServer: 0202 return Information(msg) if prio == logging.INFO else Sorry(msg, always=True) 0203 return NoPrompt(msg) 0204 0205 0206 def logInfo(msg, showDialog=False, withGamePrefix=True): 0207 """log an info message""" 0208 return logMessage(msg, logging.INFO, showDialog, withGamePrefix=withGamePrefix) 0209 0210 0211 def logError(msg, showStack=True, withGamePrefix=True): 0212 """log an error message""" 0213 return logMessage(msg, logging.ERROR, True, showStack=showStack, withGamePrefix=withGamePrefix) 0214 0215 0216 def logDebug(msg, showStack=False, withGamePrefix=True, btIndent=None): 0217 """log this message and show it on stdout 0218 if btIndent is set, message is indented by depth(backtrace)-btIndent""" 0219 if btIndent: 0220 depth = traceback.extract_stack() 0221 msg = ' ' * (len(depth) - btIndent) + msg 0222 return logMessage(msg, logging.DEBUG, False, showStack=showStack, withGamePrefix=withGamePrefix) 0223 0224 0225 def logWarning(msg, withGamePrefix=True): 0226 """log this message and show it on stdout""" 0227 return logMessage(msg, logging.WARNING, True, withGamePrefix=withGamePrefix) 0228 0229 0230 def logException(exception: str, withGamePrefix=True): 0231 """logs error message and re-raises exception""" 0232 logError(exception, withGamePrefix=withGamePrefix) 0233 raise Exception(exception) 0234 0235 0236 class EventData(str): 0237 0238 """used for generating a nice string""" 0239 events = {y: x for x, y in QEvent.__dict__.items() if isinstance(y, int)} 0240 # those are not documented for qevent but appear in Qt5Core/qcoreevent.h 0241 extra = { 0242 15: 'Create', 0243 16: 'Destroy', 0244 20: 'Quit', 0245 152: 'AcceptDropsChange', 0246 154: 'Windows:ZeroTimer' 0247 } 0248 events.update(extra) 0249 keys = {y: x for x, y in Qt.__dict__.items() if isinstance(y, int)} 0250 0251 def __new__(cls, receiver, event, prefix=None): 0252 """create the wanted string""" 0253 # pylint: disable=too-many-branches 0254 if event.type() in cls.events: 0255 # ignore unknown event types 0256 name = cls.events[event.type()] 0257 value = '' 0258 if hasattr(event, 'key'): 0259 if event.key() in cls.keys: 0260 value = cls.keys[event.key()] 0261 else: 0262 value = 'unknown key:%s' % event.key() 0263 if hasattr(event, 'text'): 0264 eventText = str(event.text()) 0265 if eventText and eventText != '\r': 0266 value += ':%s' % eventText 0267 if value: 0268 value = '(%s)' % value 0269 msg = '%s%s->%s' % (name, value, receiver) 0270 if hasattr(receiver, 'text'): 0271 if receiver.__class__.__name__ != 'QAbstractSpinBox': 0272 # accessing QAbstractSpinBox.text() gives a segfault 0273 try: 0274 msg += '(%s)' % receiver.text() 0275 except TypeError: 0276 msg += '(%s)' % receiver.text 0277 elif hasattr(receiver, 'objectName'): 0278 msg += '(%s)' % receiver.objectName() 0279 else: 0280 msg = 'unknown event:%s' % event.type() 0281 if prefix: 0282 msg = ': '.join([prefix, msg]) 0283 if 'all' in Debug.events or any(x in msg for x in Debug.events.split(':')): 0284 logDebug(msg) 0285 return msg