File indexing completed on 2024-12-29 04:59:58
0001 # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0002 # SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org> 0003 0004 import os 0005 import sys 0006 0007 sys.path.append(f'{os.path.dirname(os.path.dirname(os.path.abspath(__file__)))}/') 0008 0009 # Initialize sentry reports for exceptions in this script 0010 # NOTE: this happens before other imports so we get reports when we have systems with missing deps 0011 try: 0012 import sentry_sdk 0013 sentry_sdk.init( 0014 dsn="https://d6d53bb0121041dd97f59e29051a1781@crash-reports.kde.org/13", 0015 traces_sample_rate=1.0, 0016 release="drkonqi@" + os.getenv('DRKONQI_VERSION'), 0017 ignore_errors=[KeyboardInterrupt], 0018 ) 0019 except ImportError: 0020 print("python sentry-sdk not installed :(") 0021 0022 os.environ['LC_ALL'] = 'C.UTF-8' 0023 0024 import gdb 0025 from gdb.FrameDecorator import FrameDecorator 0026 0027 from datetime import datetime 0028 import uuid 0029 import json 0030 import subprocess 0031 import signal 0032 import re 0033 import binascii 0034 import platform 0035 import multiprocessing 0036 from pathlib import Path 0037 import psutil 0038 0039 class UnexpectedMappingException(Exception): 0040 pass 0041 0042 class NoBuildIdException(Exception): 0043 pass 0044 0045 def mangle_path(path): 0046 if not path: 0047 return path 0048 return re.sub(str(Path.home()), "$HOME", path, count=1) 0049 0050 class SentryQMLThread: 0051 def __init__(self): 0052 self.payload = None 0053 0054 # TODO this is largely a code dupe of print_qml_trace 0055 0056 if gdb.selected_inferior().connection.type == 'core': 0057 # Only live processes can be traced unfortunately since we need to 0058 # call a function on the process. That does not work on cores. 0059 return 0060 0061 # should we iterate the inferiors? Probably makes no diff for 99% of apps. 0062 for thread in gdb.selected_inferior().threads(): 0063 if not thread.is_valid() : 0064 continue 0065 thread.switch() 0066 if gdb.selected_thread() != thread: 0067 continue # failed to switch :shrug: 0068 0069 try: 0070 frame = gdb.newest_frame() 0071 except gdb.error: 0072 pass 0073 while frame: 0074 ret = qml_trace_frame(frame) 0075 if ret: 0076 self.payload = ret 0077 break 0078 try: 0079 frame = frame.older() 0080 except gdb.error: 0081 pass 0082 0083 def to_sentry_frame(self, frame): 0084 print(frame) 0085 blob = { 0086 'platform': 'other', # always different from the cpp/native frames. alas, technically this frame isn't a javascript frame 0087 'in_app': True # qml is always in the app I should think 0088 } 0089 if 'file' in frame: blob['filename'] = mangle_path(frame['file']) 0090 if 'func' in frame: blob['function'] = frame['func'] 0091 if 'line' in frame: blob['lineno'] = int(frame['line']) 0092 return blob 0093 0094 def to_sentry_frames(self, frames): 0095 lst = [] 0096 for frame in frames: 0097 data = self.to_sentry_frame(frame) 0098 if not data: 0099 continue 0100 lst.append(data) 0101 return lst 0102 0103 def to_dict(self): 0104 if not self.payload: 0105 return None 0106 0107 payload = self.payload 0108 0109 from pygdbmi import gdbmiparser 0110 result = gdbmiparser.parse_response("*stopped," + payload) 0111 frames = result['payload']['frame'] 0112 print(frames) 0113 if type(frames) is dict: # single frames traces aren't arrays to make it more fun -.- 0114 frames = [frames] 0115 lst = self.to_sentry_frames(frames) 0116 print(lst) 0117 if lst: 0118 return { 0119 'id': 'QML', # docs say this is typically a number to there is indeed no enforcement it seems 0120 'name': 'QML', 0121 'crashed': True, 0122 'stacktrace': { 0123 'frames': self.to_sentry_frames(frames) 0124 } 0125 } 0126 return None 0127 0128 def to_list(self): 0129 data = self.to_dict() 0130 if data: 0131 return [data] 0132 return [] 0133 0134 # Only grabing the most local block, technically we could also gather up encompassing scopes but it may be a bit much. 0135 class SentryVariables: 0136 def __init__(self, frame): 0137 self.frame = frame 0138 0139 def block(self): 0140 try: 0141 return self.frame.block() 0142 except: 0143 return None 0144 0145 def to_dict(self): 0146 ret = {} 0147 block = self.block() 0148 if not block: 0149 return ret 0150 0151 for symbol in block: 0152 try: 0153 ret[str(symbol)] = str(symbol.value(self.frame)) 0154 except: 0155 pass # either not a variable or not stringable 0156 return ret 0157 0158 class SentryFrame: 0159 def __init__(self, gdb_frame): 0160 self.frame = gdb_frame 0161 self.sal = gdb_frame.find_sal() 0162 0163 def type(self): 0164 return self.frame.type() 0165 0166 def filename(self): 0167 return self.sal.symtab.fullname() if (self.sal and self.sal.symtab) else None 0168 0169 def lineNumber(self): 0170 if self.sal.line < 0: 0171 return None 0172 # NOTE "The line number of the call, starting at 1." - I'm almost sure gdb starts at 0, so add 1 0173 return self.sal.line + 1 0174 0175 def function(self): 0176 return self.frame.name() or self.frame.function() or None 0177 0178 def package(self): 0179 name = gdb.solib_name(self.frame.pc()) 0180 if not name: 0181 return name 0182 # NOTE: realpath because neon's gdb is confused over UsrMerge symlinking of /lib to /usr/lib messing up 0183 # path consistency (mapping data and by extension SentryImage instances use the real path already though 0184 return os.path.realpath(name) 0185 0186 def address(self): 0187 return ('0x%x' % self.frame.pc()) 0188 0189 def to_dict(self): 0190 return { 0191 'filename': mangle_path(self.filename()), 0192 'function': self.function(), 0193 'package': mangle_path(self.package()), 0194 'instruction_addr': self.address(), 0195 'lineno': self.lineNumber(), 0196 'vars': SentryVariables(self.frame).to_dict() 0197 } 0198 0199 class SentryRegisters: 0200 def __init__(self, gdb_frame): 0201 self.frame = gdb_frame 0202 0203 def to_dict(self): 0204 js = {} 0205 try: # registers() is only available in somewhat new gdbs. (e.g. not ubuntu 20.04) 0206 for register in self.frame.architecture().registers(): 0207 if register.startswith('ymm'): # ymm actually contains stuff sentry cannot handle. alas :( 0208 continue 0209 value = self.frame.read_register(register).format_string(format='x') 0210 if value: # may be empty if the value cannot be expressed as hex (happens for extra gdb register magic - 'ymm0' etc) 0211 js[register.name] = value 0212 else: 0213 js[register.name] = "0x0" 0214 except AttributeError: 0215 return None 0216 return js 0217 0218 class LockReason: 0219 def __init__(self, frame: SentryFrame, type, class_name): 0220 self.type = type 0221 self.class_name = class_name 0222 self.thread_id = gdb.selected_thread().ptid[1] 0223 0224 def to_dict(self): 0225 return { 0226 'type': self.type, 0227 'class_name': self.class_name, 0228 'thread_id': self.thread_id, 0229 'package_name': 'java.lang', 0230 } 0231 0232 def make(frame): 0233 # export enum LockType { 0234 # LOCKED = 1, 0235 # WAITING = 2, 0236 # SLEEPING = 4, 0237 # BLOCKED = 8, 0238 # } 0239 func = frame.function() 0240 match func: 0241 case 'QtLinuxFutex::_q_futex': 0242 return LockReason(frame, 8, 'QtLinuxFutex') 0243 case ('___pthread_cond_wait', 'pthread_cond_wait'): 0244 return LockReason(frame, 2, 'pthread_cond_wait') 0245 case 'QWaitCondition::wait': 0246 return LockReason(frame, 2, 'QWaitCondition') 0247 return None 0248 0249 class SentryTrace: 0250 def __init__(self, thread, is_crashed): 0251 thread.switch() 0252 self.frame = gdb.newest_frame() 0253 self.is_crashed = is_crashed 0254 self.lock_reasons = {} 0255 self.was_main_thread = False 0256 self.crashed = self.is_crashed # different from is_crashed (=input) this indicates if we stumbled over the kcrash handler 0257 0258 def to_dict(self): 0259 frames = [ SentryFrame(frame) for frame in gdb.FrameIterator.FrameIterator(self.frame) ] 0260 self.lock_reasons = {} 0261 self.was_main_thread = False 0262 0263 kcrash_index = -1 0264 trap_index = -1 0265 for index, frame in enumerate(frames): 0266 if frame.function(): 0267 lock_reason = LockReason.make(frame) 0268 if lock_reason: 0269 r = lock_reason.to_dict() 0270 address = f'0x{str(len(self.lock_reasons.keys()))}' 0271 r['address'] = address 0272 self.lock_reasons[address] = r 0273 if frame.function().startswith('KCrash::defaultCrashHandler'): 0274 kcrash_index = index 0275 self.crashed = True 0276 if frame.function().startswith('QCoreApplication::exec'): 0277 self.was_main_thread = True 0278 if frame.type() == gdb.SIGTRAMP_FRAME: 0279 trap_index = index 0280 clip_index = max(kcrash_index, trap_index) 0281 0282 # Throw away kcrash or sigtrap frame, and above. They are useless noise - but only when on the crashing thread. 0283 if self.is_crashed and clip_index > -1: 0284 frames = frames[(clip_index + 1):] 0285 0286 # Sentry format wants oldest frame first. 0287 frames.reverse() 0288 return { 'frames': [ frame.to_dict() for frame in frames ], 'registers': SentryRegisters(self.frame).to_dict() } 0289 0290 class SentryThread: 0291 def __init__(self, gdb_thread, is_crashed): 0292 self.thread = gdb_thread 0293 self.is_crashed = is_crashed 0294 0295 def to_dict(self): 0296 # https://develop.sentry.dev/sdk/event-payloads/threads/ 0297 # As per Sentry policy, the thread that crashed with an exception should not have a stack trace, 0298 # but instead, the thread_id attribute should be set on the exception and Sentry will connect the two. 0299 trace = SentryTrace(self.thread, self.is_crashed) 0300 # NB: trace.to_dict creates members as side effect, run it asap 0301 payload = { 0302 'stacktrace': trace.to_dict(), 0303 'id': self.thread.ptid[1], 0304 'name': self.thread.name, 0305 'current': self.is_crashed, 0306 'crashed': trace.crashed, # side effect 0307 'main': trace.was_main_thread, # side effect 0308 'held_locks': trace.lock_reasons, # side effect 0309 } 0310 # States appear not documented. They are 0311 # RUNNABLE = 'Runnable', 0312 # TIMED_WAITING = 'Timed waiting', 0313 # BLOCKED = 'Blocked', 0314 # WAITING = 'Waiting', 0315 # NEW = 'New', 0316 # TERMINATED = 'Terminated', 0317 state = None 0318 if self.thread.is_exited(): 0319 state = 'Terminated' 0320 if not state: 0321 for addr, reason in trace.lock_reasons.items(): 0322 match reason['type']: 0323 case 1: 0324 state = None # locked doesn't exist as thread state 0325 case 2: 0326 state = 'Waiting' 0327 case 4: 0328 state = 'Runnable' 0329 case 8: 0330 state = 'Blocked' 0331 break 0332 payload['state'] = state 0333 return payload 0334 0335 class SentryImage: 0336 # NOTE: realpath hacks because neon's gdb is confused over UsrMerge symlinking of /lib to /usr/lib messing up 0337 # path consistency so always force realpathing for our purposes (this also is applied in SentryFrame) 0338 _objfiles = {} 0339 0340 def objfiles(self): 0341 if SentryImage._objfiles: 0342 return SentryImage._objfiles 0343 0344 objfiles = {} 0345 for objfile in gdb.objfiles(): 0346 objfiles[objfile.filename] = objfile 0347 objfiles[os.path.realpath(objfile.filename)] = objfile 0348 SentryImage._objfiles = objfiles 0349 return objfiles 0350 0351 # This can throw if objfiles fail to resolve! 0352 def __init__(self, file, start, end): 0353 # Awkwardly gdb python doesn't really give access to the solibs, meanwhile 0354 # the CLI doesn't really give access to the build_id. So we need to tuck 0355 # the two together to get comprehensive data on the loaded images. 0356 self.valid = False 0357 self.file = os.path.realpath(file) 0358 self.image_start = start 0359 self.image_end = end 0360 # Required! We can't build a debug_id without it and we require a debug_id! 0361 try: 0362 # If the mapped file isn't actually a library it will not be in the objfile rendering the image moot. 0363 # This happens because we need to construct off of proc mapping data. This also includes /dev nodes, 0364 # cache files and the like. The easiest way to filter them out is to check if the file is in the objfiles. 0365 self.objfile = self.objfiles()[self.file] 0366 except KeyError: 0367 if self.file.endswith(".so"): 0368 try: 0369 lookup = gdb.lookup_objfile(self.file) 0370 except ValueError: 0371 lookup = None 0372 objfiles = gdb.objfiles() 0373 self_objfiles = self.objfiles() # pull into scope so we have it in the trace in sentry 0374 if 'sentry_sdk' in globals(): 0375 progspace = gdb.selected_inferior().progspace 0376 pid_running = psutil.pid_exists(gdb.selected_inferior().pid) 0377 sentry_sdk.add_breadcrumb( 0378 category='debug', 0379 level='debug', 0380 message=f'Progspace {progspace} :: {progspace.filename} :: {progspace.is_valid()} :: pid running ({pid_running})', 0381 ) 0382 sentry_sdk.capture_exception(UnexpectedMappingException("unexpected mapping fail {} {} {} {}" 0383 .format(self.file, lookup, objfiles, self_objfiles))) 0384 return 0385 self.valid = True 0386 0387 def debug_id(self): 0388 # Identifier of the dynamic library or executable. 0389 # It is the value of the build_id custom section and must be formatted 0390 # as UUID truncated to the leading 16 bytes. 0391 build_id = self.build_id() 0392 if not build_id: 0393 raise NoBuildIdException(f'Unexpectedly stumbled over an objfile ({self.file}) without build_id. Not creating payload.') 0394 truncate_bytes = 16 0395 build_id = build_id + ("00" * truncate_bytes) 0396 return str(uuid.UUID(bytes_le=binascii.unhexlify(build_id)[:truncate_bytes])) 0397 0398 def build_id(self): 0399 return self.objfile.build_id 0400 0401 def to_dict(self): 0402 if not self.valid: 0403 return None 0404 # https://develop.sentry.dev/sdk/event-payloads/debugmeta 0405 0406 return { 0407 'type': 'elf', 0408 'image_addr': hex(self.image_start), 0409 'image_size': (self.image_end - self.image_start), 0410 'debug_id': self.debug_id(), 0411 # 'debug_file': None, # technically could get this from objfiles somehow but probably not useful cause it can't be used for anything 0412 'code_id': self.build_id(), 0413 'code_file': self.file, 0414 # 'image_vmaddr': None, # not available we'd have to read the ELF I think 0415 'arch': platform.machine(), 0416 } 0417 0418 def get_stdout(proc): 0419 proc = subprocess.run(proc, stdout=subprocess.PIPE) 0420 if proc.returncode != 0: 0421 return '' 0422 return proc.stdout.decode("utf-8").strip() 0423 0424 class SentryImages: 0425 _mapping_re = re.compile( 0426 r"""(?x) 0427 0428 \s* 0429 0430 (?P<start> 0431 0[xX][a-fA-F0-9]+ 0432 ) 0433 0434 \s+ 0435 0436 (?P<end> 0437 0[xX][a-fA-F0-9]+ 0438 ) 0439 0440 \s+ 0441 0442 (?P<size> 0443 0[xX][a-fA-F0-9]+ 0444 ) 0445 0446 \s+ 0447 0448 (?P<offset> 0449 0[xX][a-fA-F0-9]+ 0450 ) 0451 0452 \s+ 0453 0454 ( 0455 (?P<permissions> 0456 [rwxps-]+) 0457 \s+ 0458 )? 0459 0460 (?P<file> 0461 [\/|\/][\w|\S]+|\S+\.\S+|[a-zA-Z]* 0462 ) 0463 """ 0464 ) 0465 0466 def __init__(self): 0467 # NB: gdb also has `info sharedlibrary` but that refers to section addresses inside the image. this would mess 0468 # up symbolication as we need the correct image start in the memory region. The only way to get that is through 0469 # proc mappings. 0470 mapping = {} 0471 try: 0472 output = gdb.execute('info proc mappings', to_string=True) 0473 except: 0474 return 0475 for line in output.splitlines(): 0476 match = SentryImages._mapping_re.match(line) 0477 if not match: 0478 continue 0479 start = int(match.group('start'), 0) 0480 end = int(match.group('end'), 0) 0481 # we'll calculate size ourselves; the match is not used 0482 # offset basically just skips over previous sections so we don't really care 0483 file = match.group('file') 0484 if file not in mapping: 0485 mapping[file] = {'start': start, 'end': end} 0486 continue 0487 mapping[file]['start'] = min(mapping[file]['start'], start) 0488 mapping[file]['end'] = max(mapping[file]['end'], end) 0489 0490 # TODO: if the regexing fails we could fall back to reading /proc/1/maps instead, I'd rather have more code than useless traces because of missing images 0491 self.mappings = mapping 0492 0493 def to_list(self): 0494 ret = [] 0495 if not self.mappings: return ret 0496 for file, mapping in self.mappings.items(): 0497 image = SentryImage(file=file, start=mapping['start'], end=mapping['end']) 0498 if not image.valid: # images are invalid if the file wasn't actually found in the gdb.objfiles 0499 continue 0500 ret.append(image.to_dict()) 0501 return ret 0502 0503 class SentryEvent: 0504 def cpu_model(self): 0505 with open("/proc/cpuinfo") as f: 0506 for line in f.readlines(): 0507 key, value = line.split(':', 2) 0508 if key.strip() == 'model name': 0509 return value.strip() 0510 return None 0511 0512 def make(self, program, crash_thread): 0513 crash_signal = int(os.getenv('DRKONQI_SIGNAL')) 0514 vm = psutil.virtual_memory() 0515 boot_time = datetime.utcfromtimestamp(psutil.boot_time()).strftime('%Y-%m-%dT%H:%M:%S') 0516 0517 # crutch to get the build id. if we did this outside gdb I expect it'd be neater 0518 progfile = gdb.current_progspace().filename 0519 build_id = gdb.lookup_objfile(progfile).build_id 0520 0521 base_data = json.loads(get_stdout(['drkonqi-sentry-data'])) 0522 sentry_event = { # https://develop.sentry.dev/sdk/event-payloads/ 0523 "debug_meta": { # https://develop.sentry.dev/sdk/event-payloads/debugmeta/ 0524 "images": SentryImages().to_list() 0525 }, 0526 'threads': [ # https://develop.sentry.dev/sdk/event-payloads/threads/ 0527 SentryThread(thread, is_crashed=(thread == crash_thread)).to_dict() for thread in gdb.selected_inferior().threads() 0528 ] # + SentryQMLThread().to_list(), TODO make qml more efficient it iterates everything again after the sentry threads were collected. a right waste of time! 0529 , 0530 'event_id': uuid.uuid4().hex, 0531 # Gets overwritten by ReportInterface with a more accurate value 0532 'timestamp': datetime.utcnow().isoformat(), 0533 'message': 'Signal {} in {}'.format(crash_signal, program), 0534 'platform': 'native', 0535 'sdk': { 0536 'name': 'kde.drkonqi.gdb', 0537 'version': os.getenv('DRKONQI_VERSION'), 0538 }, 0539 'level': 'fatal', 0540 # FIXME this is kind of wrong, program ought to be mapped to the project name via our DSNs mapping table (see reportinterface.cpp) 0541 'release': "{}@unknown".format(program), 0542 'dist': build_id, 0543 'tags': { 0544 'binary': program # for fallthrough we still need a convenient way to identify things 0545 }, 0546 # TODO environment entry (could be staging for beta releases?) 0547 'contexts': { # https://develop.sentry.dev/sdk/event-payloads/contexts/ 0548 'device': { 0549 'name': base_data['Hostname'], 0550 'model': self.cpu_model(), 0551 'family': base_data['Chassis'], 0552 'simulator': base_data['Virtualization'], 0553 'arch': platform.machine(), 0554 'memory_size': vm.total, 0555 'free_memory': vm.available, 0556 'boot_time': boot_time, 0557 'timezone': base_data['Timezone'], 0558 'processor_count': multiprocessing.cpu_count() 0559 }, 0560 'os': { 0561 'name': base_data['OS_NAME'], 0562 'version': base_data['OS_VERSION_ID'], 0563 'build': base_data['OS_BUILD_ID'] if base_data['OS_BUILD_ID'] else base_data['OS_VARIANT_ID'], 0564 'kernel_version': os.uname().release, 0565 'raw_description': get_stdout(['uname', '-a']) 0566 } 0567 }, 0568 'exception': { # https://develop.sentry.dev/sdk/event-payloads/exception/ 0569 'values': [ 0570 { 0571 'value': signal.strsignal(crash_signal), 0572 'thread_id': crash_thread.ptid[1], 0573 'mechanism': { 0574 'type': 'drkonqi', 0575 'handled': False, 0576 "synthetic": True, # Docs: This flag should be set for all "segfaults" 0577 'meta': { 0578 'signal': { 0579 'number': crash_signal, 0580 'name': signal.strsignal(crash_signal) 0581 }, 0582 }, 0583 }, 0584 'stacktrace': SentryTrace(crash_thread, True).to_dict(), 0585 } 0586 ] 0587 } 0588 } 0589 0590 if os.getenv('DRKONQI_APP_VERSION'): 0591 sentry_event['release'] = '{}@{}'.format(program, os.getenv('DRKONQI_APP_VERSION')) 0592 0593 return sentry_event 0594 0595 def qml_trace_frame(frame): 0596 # NB: Super inspired by QtCreator's gdbbridge.py (GPL3). 0597 # I've made the code less of an eye sore though. 0598 0599 # This is a very exhaustive attempt at finding a frame that has a symbol to the 0600 # QV4::ExecutionEngine as we need its address to get the QML trace via qt_v4StackTraceForEngine. 0601 # Unfortunately there's no shorter way of accomplishing this since the engine isn't necessarily 0602 # appearing as a frame (consequently we can't easily get to a this pointer). 0603 0604 try: 0605 block = frame.block() 0606 except: 0607 block = None 0608 0609 if not block: 0610 return None 0611 0612 for symbol in block: 0613 if not symbol.is_variable and not symbol.is_argument: 0614 continue 0615 0616 value = symbol.value(frame) 0617 if value.is_optimized_out: # can't read values that have been optimized out 0618 continue 0619 0620 typeobj = value.type 0621 if typeobj.code != gdb.TYPE_CODE_PTR: 0622 continue 0623 0624 dereferenced_type = typeobj.target().unqualified() 0625 if dereferenced_type.name != 'QV4::ExecutionEngine': 0626 continue 0627 0628 addr = int(value) 0629 methods = [ 0630 'qt_v4StackTraceForEngine((void*)0x{0:x})', 0631 'qt_v4StackTrace(((QV4::ExecutionEngine *)0x{0:x})->currentContext())', 0632 'qt_v4StackTrace(((QV4::ExecutionEngine *)0x{0:x})->currentContext)', 0633 ] 0634 for method in methods: 0635 try: # throws when the function is invalid 0636 result = str(gdb.parse_and_eval(method.format(addr))).strip() 0637 except gdb.error: 0638 continue 0639 if result: 0640 # We need to massage the result a bit. It's of the form 0641 # "$addr stack=[...." 0642 # but we want to drop the addr as it's not useful data and can't get parsed. 0643 # Also drop the stack nesting. Serves no purpose for us. Also unescape the quotes. 0644 pos = result.find('"stack=[') 0645 if pos != -1: 0646 result = result[pos + 8:-2] 0647 result = result.replace('\\\"', '\"') 0648 return result 0649 0650 return None 0651 0652 def print_qml_frame(frame): 0653 data = { 0654 'level': '?', 0655 'func': '?', 0656 'file': '?', 0657 'line': '?' 0658 } 0659 data.update(frame) 0660 print("level={level} func={func} at={file}:{line}".format(**data) ) 0661 0662 def print_qml_frames(payload): 0663 from pygdbmi import gdbmiparser 0664 response = gdbmiparser.parse_response("*stopped," + payload) 0665 frames = response['payload']['frame'] 0666 if type(frames) is dict: # single frames traces aren't arrays to make it more fun -.- 0667 print_qml_frame(frames) 0668 else: # presumably an iterable 0669 for frame in frames: 0670 print_qml_frame(frame) 0671 0672 0673 def print_qml_trace(): 0674 if gdb.selected_inferior().connection.type == 'core': 0675 # Only live processes can be traced unfortunately since we need to 0676 # call a function on the process. That does not work on cores. 0677 print('Cannot QML trace cores :(') 0678 return 0679 0680 try: 0681 from pygdbmi import gdbmiparser 0682 except ImportError: 0683 print('Cannot QML trace cores because pygdbmi is missing :(') 0684 return 0685 0686 # should we iterate the inferiors? Probably makes no diff for 99% of apps. 0687 for thread in gdb.selected_inferior().threads(): 0688 if not thread.is_valid(): 0689 continue 0690 thread.switch() 0691 if gdb.selected_thread() != thread: 0692 continue # failed to switch :shrug: 0693 0694 try: 0695 frame = gdb.newest_frame() 0696 except gdb.error: 0697 pass 0698 while frame: 0699 ret = qml_trace_frame(frame) 0700 if ret: 0701 header = "____drkonqi_qmltrace_thread:{}____".format(str(thread.num)) 0702 print(frame) 0703 print(header) 0704 print_qml_frames(ret) 0705 print('-' * len(header)) 0706 print("(beware that frames may have been optimized out)") 0707 print() # separator newline 0708 break # next thread (there should only be one engine per thread I think?) 0709 try: 0710 frame = frame.older() 0711 except gdb.error: 0712 pass 0713 0714 def print_kcrash_error_message(): 0715 symbol = gdb.lookup_static_symbol("s_kcrashErrorMessage") 0716 if not symbol or not symbol.is_valid(): 0717 return 0718 0719 try: 0720 value = symbol.value() 0721 except: # docs say value can throw! 0722 return 0723 print("KCRASH_INFO_MESSAGE: Content of s_kcrashErrorMessage: " + value.format_string()) 0724 print() # separator newline 0725 0726 def print_sentry_payload(thread): 0727 program = os.path.basename(gdb.current_progspace().filename) 0728 payload = SentryEvent().make(program, thread) 0729 0730 tmpdir = os.getenv('DRKONQI_TMP_DIR') 0731 if tmpdir: 0732 with open(tmpdir + '/sentry_payload.json', mode='w') as tmpfile: 0733 tmpfile.write(json.dumps(payload)) 0734 tmpfile.flush() 0735 0736 def print_preamble(): 0737 thread = gdb.selected_thread() 0738 if thread == None: 0739 # Can happen when e.g. the core is missing or not readable etc. We basically aren't debugging anything 0740 return 0741 if 'sentry_sdk' in globals(): 0742 sentry_sdk.add_breadcrumb( 0743 category='debug', 0744 level='debug', 0745 message=f'Selected thread {thread}', 0746 ) 0747 # run this first as it expects the current frame to be the crashing one and qml tracing changes the frames around 0748 print_kcrash_error_message() 0749 # changes current frame and thread! 0750 print_qml_trace() 0751 # prints sentry report 0752 try: 0753 print_sentry_payload(thread) 0754 except NoBuildIdException as e: 0755 print(e) 0756 pass