mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 08:26:56 +00:00
Output macOS crash reports on CI with nicer formatting
This commit is contained in:
parent
8762a939c0
commit
e5b27d066c
2 changed files with 452 additions and 2 deletions
4
.github/workflows/ci.py
vendored
4
.github/workflows/ci.py
vendored
|
|
@ -31,8 +31,8 @@ def do_print_crash_reports():
|
|||
if items:
|
||||
time.sleep(1)
|
||||
print(os.path.basename(items[0]))
|
||||
with open(items[0]) as src:
|
||||
print(src.read())
|
||||
sdir = os.path.dirname(os.path.abspath(__file__))
|
||||
subprocess.check_call([sys.executable, os.path.join(sdir, 'macos_crash_report.py'), items[0]])
|
||||
else:
|
||||
run('sh -c "echo bt | coredumpctl debug"')
|
||||
print(flush=True)
|
||||
|
|
|
|||
450
.github/workflows/macos_crash_report.py
vendored
Executable file
450
.github/workflows/macos_crash_report.py
vendored
Executable file
|
|
@ -0,0 +1,450 @@
|
|||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import json
|
||||
import posixpath
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from functools import cached_property
|
||||
from typing import IO, List, Mapping, Optional
|
||||
|
||||
Frame = namedtuple('Frame', 'image_name image_base image_offset symbol symbol_offset')
|
||||
Register = namedtuple('Register', 'name value')
|
||||
|
||||
|
||||
def surround(x: str, start: int, end: int) -> str:
|
||||
if sys.stdout.isatty():
|
||||
x = f'\033[{start}m{x}\033[{end}m'
|
||||
return x
|
||||
|
||||
|
||||
def cyan(x: str) -> str:
|
||||
return surround(x, 96, 39)
|
||||
|
||||
|
||||
def bold(x: str) -> str:
|
||||
return surround(x, 1, 22)
|
||||
|
||||
|
||||
class BugType(Enum):
|
||||
WatchdogTimeout = '28'
|
||||
BasebandStats = '195'
|
||||
GPUEvent = '284'
|
||||
Sandbox = '187'
|
||||
TerminatingStackshot = '509'
|
||||
ServiceWatchdogTimeout = '29'
|
||||
Session = '179'
|
||||
LegacyStackshot = '188'
|
||||
MACorrelation = '197'
|
||||
iMessages = '189'
|
||||
log_power = '278'
|
||||
PowerLog = 'powerlog'
|
||||
DuetKnowledgeCollector2 = '58'
|
||||
BridgeRestore = '83'
|
||||
LegacyJetsam = '198'
|
||||
ExcResource_385 = '385'
|
||||
Modem = '199'
|
||||
Stackshot = '288'
|
||||
SystemInformation = 'system_profile'
|
||||
Jetsam_298 = '298'
|
||||
MemoryResource = '30'
|
||||
Bridge = '31'
|
||||
DifferentialPrivacy = 'diff_privacy'
|
||||
FirmwareIntegrity = '32'
|
||||
CoreAnalytics_33 = '33'
|
||||
AutoBugCapture = '34'
|
||||
EfiFirmwareIntegrity = '35'
|
||||
SystemStats = '36'
|
||||
AnonSystemStats = '37'
|
||||
Crash_9 = '9'
|
||||
Jetsam_98 = '98'
|
||||
LDCM = '100'
|
||||
Panic_10 = '10'
|
||||
Spin = '11'
|
||||
CLTM = '101'
|
||||
Hang = '12'
|
||||
Panic_110 = '110'
|
||||
ConnectionFailure = '13'
|
||||
MessageTracer = '14'
|
||||
LowBattery = '120'
|
||||
Siri = '201'
|
||||
ShutdownStall = '17'
|
||||
Panic_210 = '210'
|
||||
SymptomsCPUUsage = '202'
|
||||
AssumptionViolation = '18'
|
||||
CoreHandwriting = 'chw'
|
||||
IOMicroStackShot = '44'
|
||||
CoreAnalytics_211 = '211'
|
||||
SiriAppPrediction = '203'
|
||||
spin_45 = '45'
|
||||
PowerMicroStackshots = '220'
|
||||
BTMetadata = '212'
|
||||
SystemMemoryReset = '301'
|
||||
ResetCount = '115'
|
||||
AutoBugCapture_204 = '204'
|
||||
WifiCrashBinary = '221'
|
||||
MicroRunloopHang = '310'
|
||||
Rosetta = '213'
|
||||
glitchyspin = '302'
|
||||
System = '116'
|
||||
IOPowerSources = '141'
|
||||
PanicStats = '205'
|
||||
PowerLog_230 = '230'
|
||||
LongRunloopHang = '222'
|
||||
HomeProductsAnalytics = '311'
|
||||
DifferentialPrivacy_150 = '150'
|
||||
Rhodes = '214'
|
||||
ProactiveEventTrackerTransparency = '303'
|
||||
WiFi = '117'
|
||||
SymptomsCPUWakes = '142'
|
||||
SymptomsCPUUsageFatal = '206'
|
||||
Crash_109 = '109'
|
||||
ShortRunloopHang = '223'
|
||||
CoreHandwriting_231 = '231'
|
||||
ForceReset = '151'
|
||||
SiriAppSelection = '215'
|
||||
PrivateFederatedLearning = '304'
|
||||
Bluetooth = '118'
|
||||
SCPMotion = '143'
|
||||
HangSpin = '207'
|
||||
StepCount = '160'
|
||||
RTCTransparency = '224'
|
||||
DiagnosticRequest = '312'
|
||||
MemorySnapshot = '152'
|
||||
Rosetta_B = '216'
|
||||
AudioAccessory = '305'
|
||||
General = '119'
|
||||
HotSpotIOMicroSS = '144'
|
||||
GeoServicesTransparency = '233'
|
||||
MotionState = '161'
|
||||
AppStoreTransparency = '225'
|
||||
SiriSearchFeedback = '313'
|
||||
BearTrapReserved = '153'
|
||||
Portrait = '217'
|
||||
AWDMetricLog = 'metriclog'
|
||||
SymptomsIO = '145'
|
||||
SubmissionReserved = '170'
|
||||
WifiCrash = '209'
|
||||
Natalies = '162'
|
||||
SecurityTransparency = '226'
|
||||
BiomeMapReduce = '234'
|
||||
MemoryGraph = '154'
|
||||
MultichannelAudio = '218'
|
||||
honeybee_payload = '146'
|
||||
MesaReserved = '171'
|
||||
WifiSensing = '235'
|
||||
SiriMiss = '163'
|
||||
ExcResourceThreads_227 = '227'
|
||||
TestA = 'T01'
|
||||
NetworkUsage = '155'
|
||||
WifiReserved = '180'
|
||||
SiriActionPrediction = '219'
|
||||
honeybee_heartbeat = '147'
|
||||
ECCEvent = '172'
|
||||
KeyTransparency = '236'
|
||||
SubDiagHeartBeat = '164'
|
||||
ThirdPartyHang = '228'
|
||||
OSFault = '308'
|
||||
CoreTime = '156'
|
||||
WifiDriverReserved = '181'
|
||||
Crash_309 = '309'
|
||||
honeybee_issue = '148'
|
||||
CellularPerfReserved = '173'
|
||||
TestB = 'T02'
|
||||
StorageStatus = '165'
|
||||
SiriNotificationTransparency = '229'
|
||||
TestC = 'T03'
|
||||
CPUMicroSS = '157'
|
||||
AccessoryUpdate = '182'
|
||||
xprotect = '20'
|
||||
MultitouchFirmware = '149'
|
||||
MicroStackshot = '174'
|
||||
AppLaunchDiagnostics = '238'
|
||||
KeyboardAccuracy = '166'
|
||||
GPURestart = '21'
|
||||
FaceTime = '191'
|
||||
DuetKnowledgeCollector = '158'
|
||||
OTASUpdate = '183'
|
||||
ExcResourceThreads_327 = '327'
|
||||
ExcResource_22 = '22'
|
||||
DuetDB = '175'
|
||||
ThirdPartyHangDeveloper = '328'
|
||||
PrivacySettings = '167'
|
||||
GasGauge = '192'
|
||||
MicroStackShots = '23'
|
||||
BasebandCrash = '159'
|
||||
GPURestart_184 = '184'
|
||||
SystemWatchdogCrash = '409'
|
||||
FlashStatus = '176'
|
||||
SleepWakeFailure = '24'
|
||||
CarouselEvent = '168'
|
||||
AggregateD = '193'
|
||||
WakeupsMonitorViolation = '25'
|
||||
DifferentialPrivacy_50 = '50'
|
||||
ExcResource_185 = '185'
|
||||
UIAutomation = '177'
|
||||
ping = '26'
|
||||
SiriTransaction = '169'
|
||||
SURestore = '194'
|
||||
KtraceStackshot = '186'
|
||||
WirelessDiagnostics = '27'
|
||||
PowerLogLite = '178'
|
||||
SKAdNetworkAnalytics = '237'
|
||||
HangWorkflowResponsiveness = '239'
|
||||
CompositorClientHang = '243'
|
||||
|
||||
|
||||
class CrashReportBase:
|
||||
def __init__(self, metadata: Mapping, data: str, filename: str = None):
|
||||
self.filename = filename
|
||||
self._metadata = metadata
|
||||
self._data = data
|
||||
self._parse()
|
||||
|
||||
def _parse(self):
|
||||
self._is_json = False
|
||||
try:
|
||||
modified_data = self._data
|
||||
if '\n \n' in modified_data:
|
||||
modified_data, rest = modified_data.split('\n \n', 1)
|
||||
rest = '",' + rest.split('",', 1)[1]
|
||||
modified_data += rest
|
||||
self._data = json.loads(modified_data)
|
||||
self._is_json = True
|
||||
except json.decoder.JSONDecodeError:
|
||||
pass
|
||||
|
||||
@cached_property
|
||||
def bug_type(self) -> BugType:
|
||||
return BugType(self.bug_type_str)
|
||||
|
||||
@cached_property
|
||||
def bug_type_str(self) -> str:
|
||||
return self._metadata['bug_type']
|
||||
|
||||
@cached_property
|
||||
def incident_id(self):
|
||||
return self._metadata.get('incident_id')
|
||||
|
||||
@cached_property
|
||||
def timestamp(self) -> datetime:
|
||||
timestamp = self._metadata.get('timestamp')
|
||||
timestamp_without_timezone = timestamp.rsplit(' ', 1)[0]
|
||||
return datetime.strptime(timestamp_without_timezone, '%Y-%m-%d %H:%M:%S.%f')
|
||||
|
||||
@cached_property
|
||||
def name(self) -> str:
|
||||
return self._metadata.get('name')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
filename = ''
|
||||
if self.filename:
|
||||
filename = f'FILENAME:{posixpath.basename(self.filename)} '
|
||||
return f'<{self.__class__} {filename}TIMESTAMP:{self.timestamp}>'
|
||||
|
||||
def __str__(self) -> str:
|
||||
filename = ''
|
||||
if self.filename:
|
||||
filename = self.filename
|
||||
|
||||
return cyan(f'{self.incident_id} {self.timestamp}\n{filename}\n\n')
|
||||
|
||||
|
||||
class UserModeCrashReport(CrashReportBase):
|
||||
def _parse_field(self, name: str) -> str:
|
||||
name += ':'
|
||||
for line in self._data.split('\n'):
|
||||
if line.startswith(name):
|
||||
field = line.split(name, 1)[1]
|
||||
field = field.strip()
|
||||
return field
|
||||
|
||||
@cached_property
|
||||
def faulting_thread(self) -> int:
|
||||
if self._is_json:
|
||||
return self._data['faultingThread']
|
||||
else:
|
||||
return int(self._parse_field('Triggered by Thread'))
|
||||
|
||||
@cached_property
|
||||
def frames(self) -> List[Frame]:
|
||||
result = []
|
||||
if self._is_json:
|
||||
thread_index = self.faulting_thread
|
||||
images = self._data['usedImages']
|
||||
for frame in self._data['threads'][thread_index]['frames']:
|
||||
image = images[frame['imageIndex']]
|
||||
result.append(
|
||||
Frame(image_name=image.get('path'), image_base=image.get('base'), symbol=frame.get('symbol'),
|
||||
image_offset=frame.get('imageOffset'), symbol_offset=frame.get('symbolLocation')))
|
||||
else:
|
||||
in_frames = False
|
||||
for line in self._data.split('\n'):
|
||||
if in_frames:
|
||||
splitted = line.split()
|
||||
|
||||
if len(splitted) == 0:
|
||||
break
|
||||
|
||||
assert splitted[-2] == '+'
|
||||
image_base = splitted[-3]
|
||||
if image_base.startswith('0x'):
|
||||
result.append(Frame(image_name=splitted[1], image_base=int(image_base, 16), symbol=None,
|
||||
image_offset=int(splitted[-1]), symbol_offset=None))
|
||||
else:
|
||||
# symbolicated
|
||||
result.append(Frame(image_name=splitted[1], image_base=None, symbol=image_base,
|
||||
image_offset=None, symbol_offset=int(splitted[-1])))
|
||||
|
||||
if line.startswith(f'Thread {self.faulting_thread} Crashed:'):
|
||||
in_frames = True
|
||||
|
||||
return result
|
||||
|
||||
@cached_property
|
||||
def registers(self) -> List[Register]:
|
||||
result = []
|
||||
if self._is_json:
|
||||
thread_index = self._data['faultingThread']
|
||||
thread_state = self._data['threads'][thread_index]['threadState']
|
||||
|
||||
if 'x' in thread_state:
|
||||
for i, reg_x in enumerate(thread_state['x']):
|
||||
result.append(Register(name=f'x{i}', value=reg_x['value']))
|
||||
|
||||
for i, (name, value) in enumerate(thread_state.items()):
|
||||
if name == 'x':
|
||||
for j, reg_x in enumerate(value):
|
||||
result.append(Register(name=f'x{j}', value=reg_x['value']))
|
||||
else:
|
||||
if isinstance(value, dict):
|
||||
result.append(Register(name=name, value=value['value']))
|
||||
else:
|
||||
in_frames = False
|
||||
for line in self._data.split('\n'):
|
||||
if in_frames:
|
||||
splitted = line.split()
|
||||
|
||||
if len(splitted) == 0:
|
||||
break
|
||||
|
||||
for i in range(0, len(splitted), 2):
|
||||
register_name = splitted[i]
|
||||
if not register_name.endswith(':'):
|
||||
break
|
||||
|
||||
register_name = register_name[:-1]
|
||||
register_value = int(splitted[i + 1], 16)
|
||||
|
||||
result.append(Register(name=register_name, value=register_value))
|
||||
|
||||
if line.startswith(f'Thread {self.faulting_thread} crashed with ARM Thread State'):
|
||||
in_frames = True
|
||||
|
||||
return result
|
||||
|
||||
@cached_property
|
||||
def exception_type(self):
|
||||
if self._is_json:
|
||||
return self._data['exception'].get('type')
|
||||
else:
|
||||
return self._parse_field('Exception Type')
|
||||
|
||||
@cached_property
|
||||
def exception_subtype(self) -> Optional[str]:
|
||||
if self._is_json:
|
||||
return self._data['exception'].get('subtype')
|
||||
else:
|
||||
return self._parse_field('Exception Subtype')
|
||||
|
||||
@cached_property
|
||||
def application_specific_information(self) -> Optional[str]:
|
||||
result = ''
|
||||
if self._is_json:
|
||||
asi = self._data.get('asi')
|
||||
if asi is None:
|
||||
return None
|
||||
return asi
|
||||
else:
|
||||
in_frames = False
|
||||
for line in self._data.split('\n'):
|
||||
if in_frames:
|
||||
line = line.strip()
|
||||
if len(line) == 0:
|
||||
break
|
||||
|
||||
result += line + '\n'
|
||||
|
||||
if line.startswith('Application Specific Information:'):
|
||||
in_frames = True
|
||||
|
||||
result = result.strip()
|
||||
if not result:
|
||||
return None
|
||||
return result
|
||||
|
||||
def __str__(self) -> str:
|
||||
result = super().__str__()
|
||||
result += bold(f'Exception: {self.exception_type}\n')
|
||||
|
||||
if self.exception_subtype:
|
||||
result += bold('Exception Subtype: ')
|
||||
result += f'{self.exception_subtype}\n'
|
||||
|
||||
if self.application_specific_information:
|
||||
result += bold('Application Specific Information: ')
|
||||
result += str(self.application_specific_information)
|
||||
|
||||
result += '\n'
|
||||
|
||||
result += bold('Registers:')
|
||||
for i, register in enumerate(self.registers):
|
||||
if i % 4 == 0:
|
||||
result += '\n'
|
||||
|
||||
result += f'{register.name} = 0x{register.value:016x} '.rjust(30)
|
||||
|
||||
result += '\n\n'
|
||||
|
||||
result += bold('Frames:\n')
|
||||
for frame in self.frames:
|
||||
image_base = '_HEADER'
|
||||
if frame.image_base is not None:
|
||||
image_base = f'0x{frame.image_base:x}'
|
||||
result += f'\t[{frame.image_name}] {image_base}'
|
||||
if frame.image_offset:
|
||||
result += f' + 0x{frame.image_offset:x}'
|
||||
if frame.symbol is not None:
|
||||
result += f' ({frame.symbol} + 0x{frame.symbol_offset:x})'
|
||||
result += '\n'
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_crash_report_from_file(crash_report_file: IO) -> CrashReportBase:
|
||||
metadata = json.loads(crash_report_file.readline())
|
||||
|
||||
try:
|
||||
bug_type = BugType(metadata['bug_type'])
|
||||
except ValueError:
|
||||
return CrashReportBase(metadata, crash_report_file.read(), crash_report_file.name)
|
||||
|
||||
bug_type_parsers = {
|
||||
BugType.Crash_109: UserModeCrashReport,
|
||||
BugType.Crash_309: UserModeCrashReport,
|
||||
BugType.ExcResourceThreads_327: UserModeCrashReport,
|
||||
BugType.ExcResource_385: UserModeCrashReport,
|
||||
}
|
||||
|
||||
parser = bug_type_parsers.get(bug_type)
|
||||
if parser is None:
|
||||
return CrashReportBase(metadata, crash_report_file.read(), crash_report_file.name)
|
||||
|
||||
return parser(metadata, crash_report_file.read(), crash_report_file.name)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
with open(sys.argv[-1]) as f:
|
||||
print(get_crash_report_from_file(f))
|
||||
Loading…
Add table
Add a link
Reference in a new issue