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