pykdebugparser 1.4.0__tar.gz → 1.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/PKG-INFO +1 -2
  2. pykdebugparser-1.6.0/pykdebugparser/highlight.py +66 -0
  3. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/pykdebugparser.py +70 -8
  4. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/bsd.py +114 -0
  5. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/dyld.py +119 -17
  6. pykdebugparser-1.6.0/pykdebugparser/trace_handlers/fsystem.py +64 -0
  7. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/mach.py +129 -1
  8. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/trace.py +18 -0
  9. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/traces_parser.py +13 -0
  10. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/PKG-INFO +1 -2
  11. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/SOURCES.txt +2 -0
  12. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/requires.txt +0 -1
  13. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pyproject.toml +1 -2
  14. pykdebugparser-1.6.0/tests/test_highlight.py +52 -0
  15. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/tests/test_pykdebugparser.py +80 -0
  16. pykdebugparser-1.4.0/pykdebugparser/trace_handlers/fsystem.py +0 -22
  17. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/LICENSE +0 -0
  18. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/README.md +0 -0
  19. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/__init__.py +0 -0
  20. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/__main__.py +0 -0
  21. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/callstacks_parser.py +0 -0
  22. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/kd_buf_parser.py +0 -0
  23. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/kevent.py +0 -0
  24. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/os_log_event.py +0 -0
  25. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace.codes +0 -0
  26. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_codes.py +0 -0
  27. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/__init__.py +0 -0
  28. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/perf.py +0 -0
  29. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/turnstile.py +0 -0
  30. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/dependency_links.txt +0 -0
  31. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/entry_points.txt +0 -0
  32. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/top_level.txt +0 -0
  33. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/setup.cfg +0 -0
  34. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/tests/test_kd_buf_parser.py +0 -0
  35. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/tests/test_kevent.py +0 -0
  36. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/tests/test_os_log_event.py +0 -0
  37. {pykdebugparser-1.4.0 → pykdebugparser-1.6.0}/tests/test_trace_codes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pykdebugparser
3
- Version: 1.4.0
3
+ Version: 1.6.0
4
4
  Summary: Python parser for kdebug events
5
5
  Author: Matan Perelman
6
6
  Author-email: matan1008@gmail.com
@@ -39,7 +39,6 @@ Requires-Python: >=3.9
39
39
  Description-Content-Type: text/markdown
40
40
  License-File: LICENSE
41
41
  Requires-Dist: bytestruct>=0.1.0
42
- Requires-Dist: pygments
43
42
  Requires-Dist: click
44
43
  Requires-Dist: termcolor
45
44
  Provides-Extra: testing
@@ -0,0 +1,66 @@
1
+ """Syntax highlighting of trace lines, such as `open("/etc/hosts", O_RDONLY), fd: 3`.
2
+
3
+ Lines are colored with the terminal's 16 basic colors, so they follow its theme:
4
+
5
+ - the call or event name at the start, bold;
6
+ - strings (paths, symbols, ...), green;
7
+ - numbers, cyan;
8
+ - constants (flags, enums, signals: `O_RDONLY`, `KERN_SUCCESS`, `SIGKILL`), yellow;
9
+ - an error (`errno: ENOENT(2)`, a `KERN_*` failure), red;
10
+ - the labels of results and fields (`fd:`, `count:`), dim.
11
+
12
+ A trace is mostly the same calls with the same arguments, so the most recent lines are cached.
13
+ """
14
+ import re
15
+ from functools import lru_cache
16
+
17
+ _RESET = '\x1b[0m'
18
+ _BOLD = '\x1b[1m'
19
+ _DIM = '\x1b[2m'
20
+ _RED = '\x1b[31m'
21
+ _GREEN = '\x1b[32m'
22
+ _YELLOW = '\x1b[33m'
23
+ _CYAN = '\x1b[36m'
24
+
25
+ _TOKEN = re.compile(
26
+ r'(?P<string>"[^"]*")'
27
+ r'|(?P<number>-?0[xX][0-9a-fA-F]+|-?\d+)'
28
+ r'|(?P<label>[A-Za-z_][\w ]*?:)(?= |$)'
29
+ r'|(?P<name>[A-Za-z_]\w*)'
30
+ )
31
+ # A constant is all capitals, digits and underscores, with at least one capital.
32
+ _CONSTANT = re.compile(r'[A-Z][A-Z0-9_]*')
33
+ # `KERN_SUCCESS` is a result, not an error.
34
+ _SUCCESS = {'KERN_SUCCESS'}
35
+
36
+
37
+ @lru_cache(maxsize=1 << 14)
38
+ def highlight(text: str) -> str:
39
+ """`text` with terminal colors (SGR escape sequences); the text itself is unchanged."""
40
+ out = []
41
+ pos = 0
42
+ error = False # inside the value of an `errno:` result
43
+ first = True
44
+ for match in _TOKEN.finditer(text):
45
+ if match.start() > pos:
46
+ out.append(text[pos:match.start()])
47
+ pos = match.end()
48
+ kind = match.lastgroup
49
+ value = match.group()
50
+ if kind == 'string':
51
+ out.append(_GREEN + value + _RESET)
52
+ elif kind == 'number':
53
+ out.append((_RED if error else _CYAN) + value + _RESET)
54
+ elif kind == 'label':
55
+ error = value == 'errno:'
56
+ out.append((_RED if error else _DIM) + value + _RESET)
57
+ elif first:
58
+ out.append(_BOLD + value + _RESET)
59
+ elif _CONSTANT.fullmatch(value):
60
+ failed = error or (value.startswith('KERN_') and value not in _SUCCESS)
61
+ out.append((_RED if failed else _YELLOW) + value + _RESET)
62
+ else:
63
+ out.append(value)
64
+ first = False
65
+ out.append(text[pos:])
66
+ return ''.join(out)
@@ -1,25 +1,53 @@
1
+ import dataclasses
1
2
  from datetime import datetime
3
+ from enum import Enum
2
4
  import io
3
5
  import math
4
6
 
5
- from pygments import highlight, lexers, formatters
6
7
  from termcolor import colored
7
8
 
8
9
  from pykdebugparser.callstacks_parser import CallstacksParser
10
+ from pykdebugparser.highlight import highlight
9
11
  from pykdebugparser.kd_buf_parser import KdBufParser
10
12
  from pykdebugparser.kevent import DgbFuncQual
11
13
  from pykdebugparser.trace_codes import default_trace_codes
14
+ from pykdebugparser.trace_handlers import UnknownEnumValue
15
+ from pykdebugparser.trace_handlers.trace import TRACE_LOST_EVENTS
12
16
  from pykdebugparser.traces_parser import TracesParser
13
17
  from pykdebugparser.os_log_event import OsLogEvent
14
18
 
15
- c_lexer = lexers.CLexer()
16
- color_formatter = formatters.TerminalTrueColorFormatter(style='stata-dark')
17
19
 
18
20
  DBG_TRACE = 7
19
21
  DBG_FSYSTEM = 3
20
22
  DBG_BSD = 4
21
23
 
22
24
 
25
+ def _fields(trace) -> dict:
26
+ """A trace's parsed values (its dataclass fields but the raw kevents), as JSON-compatible values."""
27
+ return {field.name: _jsonable(getattr(trace, field.name)) for field in dataclasses.fields(trace)
28
+ if field.name != 'ktraces'}
29
+
30
+
31
+ def _jsonable(value):
32
+ if isinstance(value, (bool, str)) or value is None:
33
+ return value
34
+ if isinstance(value, UnknownEnumValue):
35
+ return int(value)
36
+ if isinstance(value, Enum):
37
+ return value.name
38
+ if isinstance(value, (int, float)):
39
+ return value
40
+ if isinstance(value, (bytes, bytearray)):
41
+ return value.hex()
42
+ if isinstance(value, (list, tuple)):
43
+ return [_jsonable(item) for item in value]
44
+ if isinstance(value, dict):
45
+ return {str(key): _jsonable(item) for key, item in value.items()}
46
+ if dataclasses.is_dataclass(value) and not isinstance(value, type):
47
+ return _fields(value)
48
+ return str(value)
49
+
50
+
23
51
  class PyKdebugParser:
24
52
  def __init__(self):
25
53
  self.filter_tid = None
@@ -93,11 +121,39 @@ class PyKdebugParser:
93
121
  if self.filter_process is not None:
94
122
  trace_generator = filter(self._filter_process_callback, trace_generator)
95
123
  if add_trace_class:
96
- trace_generator = filter(lambda t: t.ktraces[0].eventid >> 24 != DBG_TRACE, trace_generator)
124
+ # Lost events stay: they tell the reader the trace has a gap.
125
+ trace_generator = filter(lambda t: t.ktraces[0].eventid >> 24 != DBG_TRACE
126
+ or t.ktraces[0].eventid == TRACE_LOST_EVENTS, trace_generator)
97
127
  if add_fs_class:
98
128
  trace_generator = filter(lambda t: t.ktraces[0].eventid >> 24 != DBG_FSYSTEM, trace_generator)
99
129
  return trace_generator
100
130
 
131
+ def dict_traces(self, kdebug: io.IOBase, trace_codes=None):
132
+ """The traces as JSON-compatible dicts (see `trace_dict`)."""
133
+ trace_codes_map = default_trace_codes() if trace_codes is None else trace_codes
134
+ return map(lambda t: self.trace_dict(t, trace_codes_map), self.traces(kdebug, trace_codes_map))
135
+
136
+ def trace_dict(self, trace, trace_codes=None) -> dict:
137
+ """
138
+ A trace as JSON-compatible data: when (`timestamp` in mach ticks, and `time` in ISO 8601 when the device's time
139
+ information is set), where (`tid`, `pid`, `process`), what (`event`, the trace code name of its first kevent,
140
+ and `fields`, the parsed values) and `text`, as `formatted_traces` prints it.
141
+ """
142
+ trace_codes_map = default_trace_codes() if trace_codes is None else trace_codes
143
+ first = trace.ktraces[0]
144
+ pid = self.threads_pids.get(first.tid)
145
+ posix_time = self._posix_time(first.timestamp)
146
+ return {
147
+ 'timestamp': first.timestamp,
148
+ 'time': None if posix_time is None else datetime.fromtimestamp(posix_time, tz=self.timezone).isoformat(),
149
+ 'tid': first.tid,
150
+ 'pid': pid,
151
+ 'process': self.pids_names.get(pid, '') if pid is not None else '',
152
+ 'event': trace_codes_map.get(first.eventid, hex(first.eventid)),
153
+ 'fields': _fields(trace),
154
+ 'text': str(trace),
155
+ }
156
+
101
157
  def formatted_traces(self, kdebug: io.IOBase, trace_codes=None):
102
158
  return map(lambda t: self._format_trace(t), self.traces(kdebug, trace_codes))
103
159
 
@@ -127,13 +183,19 @@ class PyKdebugParser:
127
183
  process_name = self.pids_names.get(pid, '')
128
184
  return self.filter_process == str(pid) or self.filter_process == process_name
129
185
 
130
- def _format_timestamp(self, timestamp):
186
+ def _posix_time(self, timestamp):
187
+ """A kevent timestamp (mach ticks) as POSIX time, or None without the device's time information."""
131
188
  if None in (self.mach_absolute_time, self.numer, self.denom, self.usecs_since_epoch, self.timezone):
132
- return str(timestamp) + ' '
189
+ return None
133
190
  offset_usec = (
134
191
  ((timestamp - self.mach_absolute_time) * self.numer) / (self.denom * 1000)
135
192
  )
136
- posix_time = (self.usecs_since_epoch + offset_usec) / 1000000
193
+ return (self.usecs_since_epoch + offset_usec) / 1000000
194
+
195
+ def _format_timestamp(self, timestamp):
196
+ posix_time = self._posix_time(timestamp)
197
+ if posix_time is None:
198
+ return str(timestamp) + ' '
137
199
  # Split to seconds and microseconds exactly like `datetime.fromtimestamp` does (round half to even), so
138
200
  # the date and time part can be formatted only once per second.
139
201
  fraction, seconds = math.modf(posix_time)
@@ -187,7 +249,7 @@ class PyKdebugParser:
187
249
  formatted_data += f'{self._format_process(tid):<34}'
188
250
  event_rep = str(trace)
189
251
  if self.color:
190
- event_rep = highlight(event_rep, c_lexer, color_formatter).strip()
252
+ event_rep = highlight(event_rep)
191
253
 
192
254
  return formatted_data + event_rep
193
255
 
@@ -378,11 +378,18 @@ def serialize_stat_flags(flags: int) -> List[StatFlags]:
378
378
  return stat_flags
379
379
 
380
380
 
381
+ # bsd/sys/errno.h: codes the kernel uses internally, which a syscall end event can carry.
382
+ KERNEL_ERRNOS = {-1: 'ERESTART', -2: 'EJUSTRETURN'}
383
+
384
+
381
385
  def serialize_result(end_event, success_name='', fmt=lambda x: x) -> str:
382
386
  error_code = end_event.values[0]
383
387
  res = end_event.values[1]
388
+ signed = ctypes.c_int64(error_code).value
384
389
  if error_code in errno.errorcode:
385
390
  err = f'errno: {errno.errorcode[error_code]}({error_code})'
391
+ elif signed in KERNEL_ERRNOS:
392
+ err = f'errno: {KERNEL_ERRNOS[signed]}({signed})'
386
393
  else:
387
394
  err = f'errno: {error_code}'
388
395
  success = f'{success_name}: {fmt(res)}' if success_name else ''
@@ -2823,6 +2830,55 @@ class BscGeteuid:
2823
2830
  return f'geteuid(), uid: {self.uid}'
2824
2831
 
2825
2832
 
2833
+ # Darwin's signal numbers, which differ from Linux's.
2834
+ DARWIN_SIGNALS = (None, 'SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT', 'SIGEMT', 'SIGFPE', 'SIGKILL',
2835
+ 'SIGBUS', 'SIGSEGV', 'SIGSYS', 'SIGPIPE', 'SIGALRM', 'SIGTERM', 'SIGURG', 'SIGSTOP', 'SIGTSTP',
2836
+ 'SIGCONT', 'SIGCHLD', 'SIGTTIN', 'SIGTTOU', 'SIGIO', 'SIGXCPU', 'SIGXFSZ', 'SIGVTALRM', 'SIGPROF',
2837
+ 'SIGWINCH', 'SIGINFO', 'SIGUSR1', 'SIGUSR2')
2838
+
2839
+
2840
+ def format_wait_status(status: int) -> str:
2841
+ """Describe a wait status (`W_EXITCODE(code, signal)`) as its exit code or terminating signal."""
2842
+ signal = status & 0x7f
2843
+ if not signal:
2844
+ return f'exit code: {(status >> 8) & 0xff}'
2845
+ name = DARWIN_SIGNALS[signal] if signal < len(DARWIN_SIGNALS) else str(signal)
2846
+ return f'signal: {name}' + (', core dumped' if status & 0x80 else '')
2847
+
2848
+
2849
+ @dataclass
2850
+ class ProcExit:
2851
+ """A process exiting (`proc_exit`). An exec also ends the process image it replaces: xnu gives the new image a new
2852
+ proc and exits the old one, with the same pid and status 0."""
2853
+ ktraces: List
2854
+ pid: int
2855
+ status: int
2856
+
2857
+ def __str__(self):
2858
+ return f'proc_exit, pid: {self.pid}, {format_wait_status(self.status)}'
2859
+
2860
+
2861
+ @dataclass
2862
+ class BscExit:
2863
+ ktraces: List
2864
+ rval: int
2865
+
2866
+ def __str__(self):
2867
+ return f'exit({self.rval})'
2868
+
2869
+
2870
+ @dataclass
2871
+ class BscBsdthreadTerminate:
2872
+ ktraces: List
2873
+ stackaddr: int
2874
+ freesize: int
2875
+ port: int
2876
+ sem: int
2877
+
2878
+ def __str__(self):
2879
+ return f'bsdthread_terminate({hex(self.stackaddr)}, {hex(self.freesize)}, {self.port}, {self.sem})'
2880
+
2881
+
2826
2882
  @dataclass
2827
2883
  class BscWait4:
2828
2884
  ktraces: List
@@ -2970,6 +3026,35 @@ class BscSync:
2970
3026
  return 'sync()'
2971
3027
 
2972
3028
 
3029
+ @dataclass
3030
+ class BscPthreadKill:
3031
+ ktraces: List
3032
+ thread_port: int
3033
+ sig: int
3034
+ result: str
3035
+
3036
+ def __str__(self):
3037
+ rep = f'pthread_kill({self.thread_port}, {self.sig})'
3038
+ if self.result:
3039
+ rep += f', {self.result}'
3040
+ return rep
3041
+
3042
+
3043
+ @dataclass
3044
+ class BscSigreturn:
3045
+ ktraces: List
3046
+ uctx: int
3047
+ infostyle: int
3048
+ token: int
3049
+ result: str
3050
+
3051
+ def __str__(self):
3052
+ rep = f'sigreturn({hex(self.uctx)}, {self.infostyle}, {hex(self.token)})'
3053
+ if self.result:
3054
+ rep += f', {self.result}'
3055
+ return rep
3056
+
3057
+
2973
3058
  @dataclass
2974
3059
  class BscKill:
2975
3060
  ktraces: List
@@ -5384,6 +5469,20 @@ def handle_geteuid(parser, events):
5384
5469
  return BscGeteuid(events, events[-1].values[1])
5385
5470
 
5386
5471
 
5472
+ def handle_proc_exit(parser, events):
5473
+ args = events[0].values
5474
+ return ProcExit(events, args[0], ctypes.c_int32(args[1]).value)
5475
+
5476
+
5477
+ def handle_exit(parser, events):
5478
+ return BscExit(events, ctypes.c_int32(events[0].values[0]).value)
5479
+
5480
+
5481
+ def handle_bsdthread_terminate(parser, events):
5482
+ args = events[0].values
5483
+ return BscBsdthreadTerminate(events, args[0], args[1], args[2] & 0xffffffff, args[3] & 0xffffffff)
5484
+
5485
+
5387
5486
  def handle_wait4(parser, events, no_cancel=False):
5388
5487
  args = events[0].values
5389
5488
  return BscWait4(events, args[0], args[1], args[2], args[3], serialize_result(events[-1], 'pid'), no_cancel)
@@ -5438,6 +5537,16 @@ def handle_sync(parser, events):
5438
5537
  return BscSync(events)
5439
5538
 
5440
5539
 
5540
+ def handle_pthread_kill(parser, events):
5541
+ args = events[0].values
5542
+ return BscPthreadKill(events, args[0] & 0xffffffff, args[1], serialize_result(events[-1]))
5543
+
5544
+
5545
+ def handle_sigreturn(parser, events):
5546
+ args = events[0].values
5547
+ return BscSigreturn(events, args[0], args[1], args[2], serialize_result(events[-1]))
5548
+
5549
+
5441
5550
  def handle_kill(parser, events):
5442
5551
  return BscKill(events, events[0].values[0], events[0].values[1], serialize_result(events[-1]))
5443
5552
 
@@ -7133,6 +7242,8 @@ handlers = {
7133
7242
  'BSC_fchflags': handle_fchflags,
7134
7243
  'BSC_sync': handle_sync,
7135
7244
  'BSC_kill': handle_kill,
7245
+ 'BSC_pthread_kill': handle_pthread_kill,
7246
+ 'BSC_sigreturn': handle_sigreturn,
7136
7247
  'BSC_getppid': handle_getppid,
7137
7248
  'BSC_sys_dup': handle_sys_dup,
7138
7249
  'BSC_pipe': handle_pipe,
@@ -7472,6 +7583,9 @@ handlers = {
7472
7583
  'BSC_sys_crossarch_trap': handle_sys_crossarch_trap,
7473
7584
  'BSC_map_with_linking_np': handle_map_with_linking_np,
7474
7585
  'BSC_wait4': handle_wait4,
7586
+ 'proc_exit': handle_proc_exit,
7587
+ 'BSC_exit': handle_exit,
7588
+ 'BSC_bsdthread_terminate': handle_bsdthread_terminate,
7475
7589
  'BSC_mknodat': handle_mknodat,
7476
7590
  'BSC_mkfifoat': handle_mkfifoat,
7477
7591
  'BSC_freadlink': handle_freadlink,
@@ -18,6 +18,10 @@ def to_rtld_flags(flags: int):
18
18
  return [r for r in RtldFlag if r.value & flags]
19
19
 
20
20
 
21
+ def _image(path: str) -> str:
22
+ return f', image: "{path}"' if path else ''
23
+
24
+
21
25
  @dataclass
22
26
  class DyldUuidMapA:
23
27
  ktraces: List
@@ -34,9 +38,11 @@ class DyldUuidMapB:
34
38
  ktraces: List
35
39
  fid_objno: int
36
40
  fid_generation: int
41
+ path: str = ''
37
42
 
38
43
  def __str__(self):
39
- return f'DYLD_uuid_map_b, fid_objno: {self.fid_objno}, fid_generation: {hex(self.fid_generation)}'
44
+ rep = f'DYLD_uuid_map_b, fid_objno: {self.fid_objno}, fid_generation: {hex(self.fid_generation)}'
45
+ return rep + f', path: "{self.path}"' if self.path else rep
40
46
 
41
47
 
42
48
  @dataclass
@@ -55,9 +61,11 @@ class DyldUuidUnmapB:
55
61
  ktraces: List
56
62
  fid_objno: int
57
63
  fid_generation: int
64
+ path: str = ''
58
65
 
59
66
  def __str__(self):
60
- return f'DYLD_uuid_unmap_b, fid_objno: {self.fid_objno}, fid_generation: {hex(self.fid_generation)}'
67
+ rep = f'DYLD_uuid_unmap_b, fid_objno: {self.fid_objno}, fid_generation: {hex(self.fid_generation)}'
68
+ return rep + f', path: "{self.path}"' if self.path else rep
61
69
 
62
70
 
63
71
  @dataclass
@@ -77,9 +85,11 @@ class DyldUuidSharedCacheB:
77
85
  ktraces: List
78
86
  fid_objno: int
79
87
  fid_generation: int
88
+ path: str = ''
80
89
 
81
90
  def __str__(self):
82
- return f'DYLD_uuid_shared_cache_b, fid_objno: {self.fid_objno}, fid_generation: {hex(self.fid_generation)}'
91
+ rep = f'DYLD_uuid_shared_cache_b, fid_objno: {self.fid_objno}, fid_generation: {hex(self.fid_generation)}'
92
+ return rep + f', path: "{self.path}"' if self.path else rep
83
93
 
84
94
 
85
95
  @dataclass
@@ -106,9 +116,53 @@ class DyldFuncForAddImage:
106
116
  ktraces: List
107
117
  addr: int
108
118
  func: int
119
+ image: str = ''
120
+
121
+ def __str__(self):
122
+ return f'DBG_DYLD_TIMING_FUNC_FOR_ADD_IMAGE, addr: {hex(self.addr)}{_image(self.image)}, func: {hex(self.func)}'
123
+
124
+
125
+ @dataclass
126
+ class DyldFuncForRemoveImage:
127
+ ktraces: List
128
+ addr: int
129
+ func: int
130
+ image: str = ''
131
+
132
+ def __str__(self):
133
+ return (f'DBG_DYLD_TIMING_FUNC_FOR_REMOVE_IMAGE, addr: {hex(self.addr)}{_image(self.image)}, '
134
+ f'func: {hex(self.func)}')
135
+
136
+
137
+ @dataclass
138
+ class DyldStaticInitializer:
139
+ ktraces: List
140
+ mh: int
141
+ func: int
142
+ image: str = ''
143
+
144
+ def __str__(self):
145
+ return f'DBG_DYLD_TIMING_STATIC_INITIALIZER, mh: {hex(self.mh)}{_image(self.image)}, func: {hex(self.func)}'
146
+
147
+
148
+ @dataclass
149
+ class DyldObjcInit:
150
+ ktraces: List
151
+ mh: int
152
+ image: str = ''
153
+
154
+ def __str__(self):
155
+ return f'DBG_DYLD_TIMING_OBJC_INIT, mh: {hex(self.mh)}{_image(self.image)}'
156
+
157
+
158
+ @dataclass
159
+ class DyldTiming:
160
+ """A dyld phase traced without arguments (applying fixups, mapping ObjC images, ...)."""
161
+ ktraces: List
162
+ name: str
109
163
 
110
164
  def __str__(self):
111
- return f'DBG_DYLD_TIMING_FUNC_FOR_ADD_IMAGE, addr: {hex(self.addr)}, func: {hex(self.func)}'
165
+ return self.name
112
166
 
113
167
 
114
168
  @dataclass
@@ -171,34 +225,55 @@ class Dladdr:
171
225
  return f'dladdr({hex(self.addr)}), ret: {self.ret}'
172
226
 
173
227
 
174
- def handle_uuid_map_a(parser, events):
228
+ # dyld traces each image (un)load as an `_a` event with its UUID and load address, followed on the same thread by a
229
+ # `_b` event with the id of its path string. The parser keeps the loaded images per process, so the events that only
230
+ # carry a mach header (initializers, ObjC init, ...) can name their image.
231
+
232
+ def _image_path(parser, events, mh):
233
+ return parser.dyld_images.get((parser.threads_pids.get(events[0].tid, -1), mh), '')
234
+
235
+
236
+ def _map_a(parser, events, cls):
175
237
  args = events[0].values
176
- return DyldUuidMapA(events, UUID(bytes=events[0].data[:16]), args[2], args[3])
238
+ parser.dyld_pending_load_addr[events[0].tid] = args[2]
239
+ return cls(events, UUID(bytes=events[0].data[:16]), args[2], args[3])
240
+
241
+
242
+ def _map_b(parser, events, cls, loaded):
243
+ args = events[0].values
244
+ path = parser.global_strings.get(args[1], '') if args[1] else ''
245
+ load_addr = parser.dyld_pending_load_addr.pop(events[0].tid, None)
246
+ if load_addr is not None:
247
+ key = (parser.threads_pids.get(events[0].tid, -1), load_addr)
248
+ if loaded and path:
249
+ parser.dyld_images[key] = path
250
+ elif not loaded:
251
+ parser.dyld_images.pop(key, None)
252
+ return cls(events, args[0] & 0xffffffff, args[0] >> 32, path)
253
+
254
+
255
+ def handle_uuid_map_a(parser, events):
256
+ return _map_a(parser, events, DyldUuidMapA)
177
257
 
178
258
 
179
259
  def handle_uuid_map_b(parser, events):
180
- arg = events[0].values[0]
181
- return DyldUuidMapB(events, arg & 0xffffffff, arg >> 32)
260
+ return _map_b(parser, events, DyldUuidMapB, loaded=True)
182
261
 
183
262
 
184
263
  def handle_uuid_unmap_a(parser, events):
185
- args = events[0].values
186
- return DyldUuidUnmapA(events, UUID(bytes=events[0].data[:16]), args[2], args[3])
264
+ return _map_a(parser, events, DyldUuidUnmapA)
187
265
 
188
266
 
189
267
  def handle_uuid_unmap_b(parser, events):
190
- arg = events[0].values[0]
191
- return DyldUuidUnmapB(events, arg & 0xffffffff, arg >> 32)
268
+ return _map_b(parser, events, DyldUuidUnmapB, loaded=False)
192
269
 
193
270
 
194
271
  def handle_uuid_shared_cache_a(parser, events):
195
- args = events[0].values
196
- return DyldUuidSharedCacheA(events, UUID(bytes=events[0].data[:16]), args[2], args[3])
272
+ return _map_a(parser, events, DyldUuidSharedCacheA)
197
273
 
198
274
 
199
275
  def handle_uuid_shared_cache_b(parser, events):
200
- arg = events[0].values[0]
201
- return DyldUuidSharedCacheB(events, arg & 0xffffffff, arg >> 32)
276
+ return _map_b(parser, events, DyldUuidSharedCacheB, loaded=True)
202
277
 
203
278
 
204
279
  def handle_timing_launch_executable(parser, events):
@@ -217,7 +292,26 @@ def handle_timing_map_image(parser, events):
217
292
 
218
293
  def handle_timing_func_for_add_image(parser, events):
219
294
  args = events[0].values
220
- return DyldFuncForAddImage(events, args[1], args[2])
295
+ return DyldFuncForAddImage(events, args[1], args[2], _image_path(parser, events, args[1]))
296
+
297
+
298
+ def handle_timing_func_for_remove_image(parser, events):
299
+ args = events[0].values
300
+ return DyldFuncForRemoveImage(events, args[1], args[2], _image_path(parser, events, args[1]))
301
+
302
+
303
+ def handle_timing_static_initializer(parser, events):
304
+ args = events[0].values
305
+ return DyldStaticInitializer(events, args[1], args[2], _image_path(parser, events, args[1]))
306
+
307
+
308
+ def handle_timing_objc_init(parser, events):
309
+ args = events[0].values
310
+ return DyldObjcInit(events, args[1], _image_path(parser, events, args[1]))
311
+
312
+
313
+ def handle_timing(parser, events):
314
+ return DyldTiming(events, parser.trace_codes[events[0].eventid])
221
315
 
222
316
 
223
317
  def handle_timing_bootstrap_start(parser, events):
@@ -257,6 +351,14 @@ handlers = {
257
351
  'DBG_DYLD_TIMING_LAUNCH_EXECUTABLE': handle_timing_launch_executable,
258
352
  'DBG_DYLD_TIMING_MAP_IMAGE': handle_timing_map_image,
259
353
  'DBG_DYLD_TIMING_FUNC_FOR_ADD_IMAGE': handle_timing_func_for_add_image,
354
+ 'DBG_DYLD_TIMING_FUNC_FOR_REMOVE_IMAGE': handle_timing_func_for_remove_image,
355
+ 'DBG_DYLD_TIMING_STATIC_INITIALIZER': handle_timing_static_initializer,
356
+ 'DBG_DYLD_TIMING_OBJC_INIT': handle_timing_objc_init,
357
+ 'DBG_DYLD_TIMING_OBJC_MAP': handle_timing,
358
+ 'DBG_DYLD_TIMING_APPLY_FIXUPS': handle_timing,
359
+ 'DBG_DYLD_TIMING_ATTACH_CODESIGNATURE': handle_timing,
360
+ 'DBG_DYLD_TIMING_BUILD_CLOSURE': handle_timing,
361
+ 'DBG_DYLD_REMOTE_IMAGE_NOTIFIER': handle_timing,
260
362
  'DBG_DYLD_TIMING_BOOTSTRAP_START': handle_timing_bootstrap_start,
261
363
  'DBG_DYLD_TIMING_DLOPEN': handle_timing_dlopen,
262
364
  'DBG_DYLD_TIMING_DLOPEN_PREFLIGHT': handle_timing_dlopen_preflight,
@@ -0,0 +1,64 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, Optional
3
+
4
+ from pykdebugparser.trace_codes import default_trace_codes
5
+
6
+ DBG_DKRW = 0x302
7
+ # bsd/sys/kdebug.h: the code of a disk I/O event is these flags (`P_RdMetaAsync`, `P_WrDataDone`, ...).
8
+ DKIO_DONE = 0x01
9
+
10
+
11
+ @dataclass
12
+ class VfsLookup:
13
+ ktraces: List
14
+ path: str
15
+ vnode_id: int
16
+
17
+ def __str__(self):
18
+ return f'lookup("{self.path}"), vnode id: {self.vnode_id}'
19
+
20
+
21
+ @dataclass
22
+ class DiskIo:
23
+ """A disk I/O, from its issue (device, block, size) to its completion (bytes not transferred, error)."""
24
+ ktraces: List
25
+ name: str
26
+ dev: Optional[int]
27
+ blkno: Optional[int]
28
+ size: Optional[int]
29
+ resid: int
30
+ error: int
31
+
32
+ def __str__(self):
33
+ rep = self.name
34
+ if self.dev is not None:
35
+ rep += f', dev: {self.dev >> 24},{self.dev & 0xffffff}, blkno: {hex(self.blkno)}, size: {self.size}'
36
+ return rep + f', resid: {self.resid}, error: {self.error}'
37
+
38
+
39
+ def handle_vfs_lookup(parser, events):
40
+ node = parser.parse_vnode(events)
41
+ return VfsLookup(events, node.path, node.vnode_id)
42
+
43
+
44
+ def handle_disk_io(parser, events):
45
+ # xnu traces the issue as (buf, dev, blkno, size) and the completion, often on another thread, as
46
+ # (buf, vnode, resid, error); the buf address pairs them. Print one line when the I/O completes.
47
+ event = events[0]
48
+ buf = event.values[0]
49
+ if not (event.eventid >> 2) & DKIO_DONE:
50
+ parser.pending_disk_io[buf] = event
51
+ return None
52
+ start = parser.pending_disk_io.pop(buf, None)
53
+ resid, error = event.values[2], event.values[3]
54
+ if start is None:
55
+ return DiskIo(events, parser.trace_codes[event.eventid].removesuffix('Done'), None, None, None, resid, error)
56
+ args = start.values
57
+ return DiskIo([start, event], parser.trace_codes[start.eventid], args[1] & 0xffffffff, args[2], args[3], resid,
58
+ error)
59
+
60
+
61
+ handlers = {
62
+ 'VFS_LOOKUP': handle_vfs_lookup,
63
+ }
64
+ handlers.update({name: handle_disk_io for eventid, name in default_trace_codes().items() if eventid >> 16 == DBG_DKRW})
@@ -2,10 +2,14 @@ import ctypes
2
2
  from dataclasses import dataclass
3
3
  from enum import Enum
4
4
  from functools import partial
5
- from typing import List
5
+ from typing import List, Optional
6
6
 
7
+ from pykdebugparser.trace_codes import default_trace_codes
7
8
  from pykdebugparser.trace_handlers import parse_enum
8
9
 
10
+ # bsd/sys/kdebug.h: (DBG_MACH << 8) | DBG_MACH_EXCP_SYNC_ARM
11
+ DBG_MACH_EXCP_SYNC_ARM = 0x103
12
+
9
13
 
10
14
  class AsynchronousSystemTrapsReason(Enum):
11
15
  AST_NONE = 0x00
@@ -59,10 +63,13 @@ class ExceptionSyndromeRegisterClass(Enum):
59
63
  ESR_EC_TRAP_SIMD_FP = 0x07
60
64
  ESR_EC_PTRAUTH_INSTR_TRAP = 0x09
61
65
  ESR_EC_MCRR_MRRC_CP14_TRAP = 0x0c
66
+ ESR_EC_BTI_FAIL = 0x0d
62
67
  ESR_EC_ILLEGAL_INSTR_SET = 0x0e
63
68
  ESR_EC_SVC_32 = 0x11
64
69
  ESR_EC_SVC_64 = 0x15
70
+ ESR_EC_HVC_64 = 0x16
65
71
  ESR_EC_MSR_TRAP = 0x18
72
+ ESR_EC_PAC_FAIL = 0x1c
66
73
  ESR_EC_IABORT_EL0 = 0x20
67
74
  ESR_EC_IABORT_EL1 = 0x21
68
75
  ESR_EC_PC_ALIGN = 0x22
@@ -281,6 +288,25 @@ class KernelUncategorizedExcArm:
281
288
  return f'KernelUncategorizedExcArm, class: {esr_class}, far: {hex(self.far)}, pc: {hex(self.pc)}'
282
289
 
283
290
 
291
+ @dataclass
292
+ class ArmException:
293
+ """An arm64 synchronous exception (`sleh.c`): its ESR, fault address and PC, for the classes without their own
294
+ trace class (breakpoints, `brk` traps, pointer authentication failures, alignment faults, ...)."""
295
+ ktraces: List
296
+ name: str
297
+ esr: int
298
+ far: int
299
+ pc: int
300
+
301
+ def __str__(self):
302
+ esr_class = self.esr >> 26
303
+ try:
304
+ esr_class = ExceptionSyndromeRegisterClass(esr_class).name
305
+ except ValueError:
306
+ pass
307
+ return f'{self.name}, class: {esr_class}, far: {hex(self.far)}, pc: {hex(self.pc)}'
308
+
309
+
284
310
  @dataclass
285
311
  class KernelDataAbortSameElExcArm:
286
312
  ktraces: List
@@ -594,6 +620,58 @@ class SemaphoreTimedwait:
594
620
  return f'semaphore_timedwait({hex(self.wait_name)}, {self.sec}, {self.nsec}), result: {self.result.name}'
595
621
 
596
622
 
623
+ @dataclass
624
+ class TaskForPid:
625
+ ktraces: List
626
+ target_tport: int
627
+ pid: int
628
+ t: int
629
+ result: KernReturn
630
+
631
+ def __str__(self):
632
+ return f'task_for_pid({self.target_tport}, {self.pid}, {hex(self.t)}), result: {self.result.name}'
633
+
634
+
635
+ @dataclass
636
+ class TaskNameForPid:
637
+ ktraces: List
638
+ target_tport: int
639
+ pid: int
640
+ tn: int
641
+ result: KernReturn
642
+
643
+ def __str__(self):
644
+ return f'task_name_for_pid({self.target_tport}, {self.pid}, {hex(self.tn)}), result: {self.result.name}'
645
+
646
+
647
+ @dataclass
648
+ class PidForTask:
649
+ ktraces: List
650
+ task: int
651
+ pid_addr: int
652
+ result: KernReturn
653
+
654
+ def __str__(self):
655
+ return f'pid_for_task({self.task}, {hex(self.pid_addr)}), result: {self.result.name}'
656
+
657
+
658
+ @dataclass
659
+ class TaskSuspension:
660
+ """A task or thread suspended (by `task_suspend`, a debugger, ...) or resumed (osfmk `DBG_MACH_SUSPENSION`)."""
661
+ ktraces: List
662
+ name: str
663
+ target: str
664
+ target_id: int
665
+ user_stop_count: Optional[int] = None
666
+ pidsuspended: Optional[int] = None
667
+
668
+ def __str__(self):
669
+ rep = f'{self.name}, {self.target}: {self.target_id}'
670
+ if self.user_stop_count is not None:
671
+ rep += f', user_stop_count: {self.user_stop_count}, pidsuspended: {bool(self.pidsuspended)}'
672
+ return rep
673
+
674
+
597
675
  @dataclass
598
676
  class MachPortGetAttributes:
599
677
  ktraces: List
@@ -1017,6 +1095,11 @@ def handle_user_instr_abort_lower_el_exc_arm(parser, events):
1017
1095
  return UserInstrAbortLowerElExcArm(events, *events[0].values[:3])
1018
1096
 
1019
1097
 
1098
+ def handle_arm_exception(parser, events):
1099
+ args = events[0].values
1100
+ return ArmException(events, parser.trace_codes[events[0].eventid], args[0], args[1], args[2])
1101
+
1102
+
1020
1103
  def handle_user_data_abort_lower_el_exc_arm(parser, events):
1021
1104
  args = events[0].values
1022
1105
  return UserDataAbortLowerElExcArm(events, args[0], args[1], args[2])
@@ -1057,6 +1140,41 @@ def handle_msc_mach_vm_map_trap(parser, events):
1057
1140
  return MachVmMap(events, *events[0].values)
1058
1141
 
1059
1142
 
1143
+ def handle_msc_task_for_pid(parser, events):
1144
+ args = events[0].values
1145
+ return TaskForPid(events, args[0] & 0xffffffff, ctypes.c_int32(args[1]).value, args[2],
1146
+ parse_enum(KernReturn, events[-1].values[0]))
1147
+
1148
+
1149
+ def handle_msc_task_name_for_pid(parser, events):
1150
+ args = events[0].values
1151
+ return TaskNameForPid(events, args[0] & 0xffffffff, ctypes.c_int32(args[1]).value, args[2],
1152
+ parse_enum(KernReturn, events[-1].values[0]))
1153
+
1154
+
1155
+ def handle_msc_pid_for_task(parser, events):
1156
+ args = events[0].values
1157
+ return PidForTask(events, args[0] & 0xffffffff, args[1], parse_enum(KernReturn, events[-1].values[0]))
1158
+
1159
+
1160
+ def handle_task_suspend(parser, events):
1161
+ args = events[0].values
1162
+ return TaskSuspension(events, 'task_suspend', 'pid', args[0], args[1], args[2])
1163
+
1164
+
1165
+ def handle_task_resume(parser, events):
1166
+ return TaskSuspension(events, 'task_resume', 'pid', events[0].values[0])
1167
+
1168
+
1169
+ def handle_thread_suspend(parser, events):
1170
+ args = events[0].values
1171
+ return TaskSuspension(events, 'thread_suspend', 'tid', args[0], args[1], args[2])
1172
+
1173
+
1174
+ def handle_thread_resume(parser, events):
1175
+ return TaskSuspension(events, 'thread_resume', 'tid', events[0].values[0])
1176
+
1177
+
1060
1178
  def handle_msc_mach_port_allocate_trap(parser, events):
1061
1179
  args = events[0].values
1062
1180
  return MachPortAllocate(events, args[0], parse_enum(MachPortRight, args[1]), args[2])
@@ -1306,6 +1424,13 @@ handlers = {
1306
1424
  'MSC_semaphore_signal_trap': handle_msc_semaphore_signal_trap,
1307
1425
  'MSC_semaphore_wait_trap': handle_msc_semaphore_wait_trap,
1308
1426
  'MSC_semaphore_timedwait_trap': handle_msc_semaphore_timedwait_trap,
1427
+ 'MSC_task_for_pid': handle_msc_task_for_pid,
1428
+ 'MSC_task_name_for_pid': handle_msc_task_name_for_pid,
1429
+ 'MSC_pid_for_task': handle_msc_pid_for_task,
1430
+ 'MACH_task_suspend': handle_task_suspend,
1431
+ 'MACH_task_resume': handle_task_resume,
1432
+ 'MACH_thread_suspend': handle_thread_suspend,
1433
+ 'MACH_thread_resume': handle_thread_resume,
1309
1434
  'MSC_mach_port_get_attributes_trap': handle_msc_mach_port_get_attributes_trap,
1310
1435
  'MSC_mach_port_guard_trap': handle_msc_mach_port_guard_trap,
1311
1436
  'MSC_mach_port_unguard_trap': handle_msc_mach_port_unguard_trap,
@@ -1341,3 +1466,6 @@ handlers = {
1341
1466
  'SCHED_CLUTCH_CPU_THREAD_SELECT': handle_sched_clutch_cpu_thread_select,
1342
1467
  'SCHED_CLUTCH_TG_BUCKET_PRI': handle_sched_clutch_tg_bucket_pri,
1343
1468
  }
1469
+ # Every other arm64 synchronous exception class shares the same arguments.
1470
+ handlers.update({name: handle_arm_exception for eventid, name in default_trace_codes().items()
1471
+ if eventid >> 16 == DBG_MACH_EXCP_SYNC_ARM and name.endswith('_Exc_ARM') and name not in handlers})
@@ -189,6 +189,23 @@ def handle_trace_string_threadname_prev(parser, events):
189
189
  return event
190
190
 
191
191
 
192
+ # bsd/sys/kdebug.h
193
+ TRACE_LOST_EVENTS = 0x7020008
194
+
195
+
196
+ @dataclass
197
+ class TraceLostEvents:
198
+ """The kernel's trace buffer filled up and dropped events before this point."""
199
+ ktraces: List
200
+
201
+ def __str__(self):
202
+ return 'TRACE_LOST_EVENTS: the kernel dropped trace events here'
203
+
204
+
205
+ def handle_trace_lost_events(parser, events):
206
+ return TraceLostEvents(events)
207
+
208
+
192
209
  handlers = {
193
210
  'TRACE_DATA_NEWTHREAD': handle_trace_data_newthread,
194
211
  'TRACE_DATA_EXEC': handle_trace_data_exec,
@@ -200,4 +217,5 @@ handlers = {
200
217
  'TRACE_STRING_PROC_EXIT': handle_trace_string_proc_exit,
201
218
  'TRACE_STRING_THREADNAME': handle_trace_string_threadname,
202
219
  'TRACE_STRING_THREADNAME_PREV': handle_trace_string_threadname_prev,
220
+ 'TRACE_LOST_EVENTS': handle_trace_lost_events,
203
221
  }
@@ -29,6 +29,9 @@ class RawTrace:
29
29
  return rep
30
30
 
31
31
 
32
+ NO_RETURN_CALLS = ('BSC_exit', 'BSC_bsdthread_terminate')
33
+
34
+
32
35
  class TracesParser:
33
36
  def __init__(self, trace_codes_map, threads_pids, pids_names):
34
37
  self.trace_codes = trace_codes_map
@@ -46,6 +49,13 @@ class TracesParser:
46
49
  }
47
50
  self.last_data_newthread = None
48
51
  self.last_data_exec = None
52
+ # Calls that never return, parsed from their start event.
53
+ self.no_return_eventids = {eventid for eventid, name in trace_codes_map.items() if name in NO_RETURN_CALLS}
54
+ # Disk I/Os issued and not completed yet, by their buf address.
55
+ self.pending_disk_io = {}
56
+ # Images dyld loaded, by (pid, load address), and the load address of each thread's last `DYLD_uuid_*_a` event.
57
+ self.dyld_images = {}
58
+ self.dyld_pending_load_addr = {}
49
59
  # Event ids to show as `RawTrace` when they have no handler.
50
60
  self.raw_eventids = set()
51
61
  self.handlers = {}
@@ -109,6 +119,9 @@ class TracesParser:
109
119
  return list(self.vnode_generator([e for e in events if self.trace_codes.get(e.eventid) == 'VFS_LOOKUP']))
110
120
 
111
121
  def _feed_start_event(self, event, state):
122
+ if event.eventid in self.no_return_eventids:
123
+ # The thread or process ends inside the call, so its end is never traced.
124
+ return self._feed_single_event(event, state)
112
125
  tid_state = state.get(event.tid)
113
126
  if tid_state is None:
114
127
  # New tid
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pykdebugparser
3
- Version: 1.4.0
3
+ Version: 1.6.0
4
4
  Summary: Python parser for kdebug events
5
5
  Author: Matan Perelman
6
6
  Author-email: matan1008@gmail.com
@@ -39,7 +39,6 @@ Requires-Python: >=3.9
39
39
  Description-Content-Type: text/markdown
40
40
  License-File: LICENSE
41
41
  Requires-Dist: bytestruct>=0.1.0
42
- Requires-Dist: pygments
43
42
  Requires-Dist: click
44
43
  Requires-Dist: termcolor
45
44
  Provides-Extra: testing
@@ -5,6 +5,7 @@ setup.cfg
5
5
  pykdebugparser/__init__.py
6
6
  pykdebugparser/__main__.py
7
7
  pykdebugparser/callstacks_parser.py
8
+ pykdebugparser/highlight.py
8
9
  pykdebugparser/kd_buf_parser.py
9
10
  pykdebugparser/kevent.py
10
11
  pykdebugparser/os_log_event.py
@@ -26,6 +27,7 @@ pykdebugparser/trace_handlers/mach.py
26
27
  pykdebugparser/trace_handlers/perf.py
27
28
  pykdebugparser/trace_handlers/trace.py
28
29
  pykdebugparser/trace_handlers/turnstile.py
30
+ tests/test_highlight.py
29
31
  tests/test_kd_buf_parser.py
30
32
  tests/test_kevent.py
31
33
  tests/test_os_log_event.py
@@ -1,5 +1,4 @@
1
1
  bytestruct>=0.1.0
2
- pygments
3
2
  click
4
3
  termcolor
5
4
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pykdebugparser"
7
- version = "1.4.0"
7
+ version = "1.6.0"
8
8
  description = "Python parser for kdebug events"
9
9
  readme.file = "README.md"
10
10
  readme.content-type = "text/markdown"
@@ -25,7 +25,6 @@ classifiers = [
25
25
  ]
26
26
  dependencies = [
27
27
  "bytestruct>=0.1.0",
28
- "pygments",
29
28
  "click",
30
29
  "termcolor",
31
30
  ]
@@ -0,0 +1,52 @@
1
+ import re
2
+
3
+ import pytest
4
+
5
+ from pykdebugparser.highlight import highlight
6
+
7
+ _SGR = re.compile(r'\x1b\[[0-9;]*m')
8
+
9
+ BOLD, DIM, RESET = '\x1b[1m', '\x1b[2m', '\x1b[0m'
10
+ RED, GREEN, YELLOW, CYAN = '\x1b[31m', '\x1b[32m', '\x1b[33m', '\x1b[36m'
11
+
12
+
13
+ def _colored(color, text):
14
+ return color + text + RESET
15
+
16
+
17
+ @pytest.mark.parametrize('line, expected', [
18
+ ('open("/etc/hosts", O_RDONLY | O_CLOEXEC), fd: 3',
19
+ f'{_colored(BOLD, "open")}({_colored(GREEN, chr(34) + "/etc/hosts" + chr(34))}, {_colored(YELLOW, "O_RDONLY")} | '
20
+ f'{_colored(YELLOW, "O_CLOEXEC")}), {_colored(DIM, "fd:")} {_colored(CYAN, "3")}'),
21
+ ('stat64("/x"), errno: ENOENT(2)',
22
+ f'{_colored(BOLD, "stat64")}({_colored(GREEN, chr(34) + "/x" + chr(34))}), {_colored(RED, "errno:")} '
23
+ f'{_colored(RED, "ENOENT")}({_colored(RED, "2")})'),
24
+ ('task_for_pid(515, 97997, 0x16bd572fc), result: KERN_SUCCESS',
25
+ f'{_colored(BOLD, "task_for_pid")}({_colored(CYAN, "515")}, {_colored(CYAN, "97997")}, '
26
+ f'{_colored(CYAN, "0x16bd572fc")}), {_colored(DIM, "result:")} {_colored(YELLOW, "KERN_SUCCESS")}'),
27
+ ('mach_port_mod_refs(1, 0x0, MACH_PORT_RIGHT_SEND, 1), result: KERN_INVALID_RIGHT',
28
+ f'{_colored(BOLD, "mach_port_mod_refs")}({_colored(CYAN, "1")}, {_colored(CYAN, "0x0")}, '
29
+ f'{_colored(YELLOW, "MACH_PORT_RIGHT_SEND")}, {_colored(CYAN, "1")}), {_colored(DIM, "result:")} '
30
+ f'{_colored(RED, "KERN_INVALID_RIGHT")}'),
31
+ ('lookup("private/var/db"), vnode id: 42',
32
+ f'{_colored(BOLD, "lookup")}({_colored(GREEN, chr(34) + "private/var/db" + chr(34))}), '
33
+ f'{_colored(DIM, "vnode id:")} {_colored(CYAN, "42")}'),
34
+ ('sigreturn(0x1, 30, 0x2), errno: EJUSTRETURN(-2)',
35
+ f'{_colored(BOLD, "sigreturn")}({_colored(CYAN, "0x1")}, {_colored(CYAN, "30")}, {_colored(CYAN, "0x2")}), '
36
+ f'{_colored(RED, "errno:")} {_colored(RED, "EJUSTRETURN")}({_colored(RED, "-2")})'),
37
+ ('DBG_DYLD_TIMING_OBJC_MAP', _colored(BOLD, 'DBG_DYLD_TIMING_OBJC_MAP')),
38
+ ])
39
+ def test_highlight(line, expected):
40
+ assert highlight(line) == expected
41
+
42
+
43
+ @pytest.mark.parametrize('line', [
44
+ 'open("/etc/hosts", O_RDONLY), fd: 3',
45
+ "ioctl(8, 0xc020699d /* _IOC(IOC_IN | IOC_OUT, 'i', 157, 32) */, 0x16d3cbea0)",
46
+ 'TRACE_LOST_EVENTS: the kernel dropped trace events here',
47
+ 'stat64("/a b/{c}: d", 0x1), count: 0x10',
48
+ 'unterminated "string',
49
+ '',
50
+ ])
51
+ def test_highlight_keeps_the_text(line):
52
+ assert _SGR.sub('', highlight(line)) == line
@@ -1,4 +1,5 @@
1
1
  from datetime import datetime, timedelta, timezone
2
+ import json
2
3
  from io import BytesIO
3
4
  import struct
4
5
 
@@ -164,3 +165,82 @@ def test_traces_filter_eventid_without_handler():
164
165
  assert len(traces) == 2
165
166
  assert traces[0].endswith('MACH_SCHED_THREAD_SELECT(0x0, 0x0, 0x0, 0x0), end: (0x1d3, 0x0, 0x0, 0x4)')
166
167
  assert traces[1].endswith('MACH_PSET_AVG_EXEC_TIME(0x0, 0x3, 0x0, 0x0)')
168
+
169
+
170
+ def test_traces_keep_lost_events_when_filtered():
171
+ # The parser adds the TRACE class to filtered traces for its own bookkeeping and hides it again, except for lost
172
+ # events, which tell the reader the trace has a gap.
173
+ events = [
174
+ _kd_buf(1000, 0x7020008, (1, 0, 0, 0)), # TRACE_LOST_EVENTS
175
+ _kd_buf(1001, 0x1400140, (0, 3, 0, 0)), # MACH_PSET_AVG_EXEC_TIME
176
+ ]
177
+ events_buf = RAW_VERSION2_BYTES + b'\x00' * 0x11c + b''.join(events)
178
+ parser = _traces_parser()
179
+ parser.filter_class = [1]
180
+ traces = list(parser.formatted_traces(BytesIO(events_buf)))
181
+ assert len(traces) == 1
182
+ assert traces[0].endswith('TRACE_LOST_EVENTS: the kernel dropped trace events here')
183
+
184
+
185
+ def test_dict_traces():
186
+ events = [
187
+ _kd_buf(1000, 0x40c0014 | 1, (0x16d2c2b80, 0, 0, 0)), # open start
188
+ _kd_buf(2000, 0x40c0014 | 2, (2, 0, 0, 0)), # open end: ENOENT
189
+ ]
190
+ events_buf = RAW_VERSION2_BYTES + b'\x00' * 0x11c + b''.join(events)
191
+ parser = _traces_parser()
192
+ (unknown_process,) = parser.dict_traces(BytesIO(events_buf))
193
+ assert (unknown_process['pid'], unknown_process['process']) == (None, '')
194
+ # The thread's process, once the trace has seen it.
195
+ (parsed,) = parser.traces(BytesIO(events_buf))
196
+ parser.threads_pids[100] = 42
197
+ parser.pids_names[42] = 'probe'
198
+ trace = parser.trace_dict(parsed)
199
+ assert trace == {
200
+ 'timestamp': 1000,
201
+ 'time': '1970-01-01T00:00:00.000001+00:00',
202
+ 'tid': 100,
203
+ 'pid': 42,
204
+ 'process': 'probe',
205
+ 'event': 'BSC_open',
206
+ 'fields': trace['fields'],
207
+ 'text': trace['text'],
208
+ }
209
+ assert trace['text'].startswith('open(') and trace['text'].endswith('errno: ENOENT(2)')
210
+ assert json.loads(json.dumps(trace)) == trace # JSON-compatible: enums by name, flags as lists of names
211
+
212
+
213
+ def test_trace_dict_values():
214
+ from enum import Enum
215
+ from dataclasses import dataclass
216
+ from uuid import UUID
217
+ from pykdebugparser.trace_handlers import UnknownEnumValue
218
+
219
+ class Color(Enum):
220
+ RED = 1
221
+
222
+ @dataclass
223
+ class Inner:
224
+ ktraces: list
225
+ value: int
226
+
227
+ @dataclass
228
+ class Trace:
229
+ ktraces: list
230
+ color: Color
231
+ unknown: UnknownEnumValue
232
+ flags: list
233
+ uuid: UUID
234
+ data: bytes
235
+ inner: Inner
236
+
237
+ event = Kevent(timestamp=5, data=b'\x00' * 32, values=(0, 0, 0, 0), tid=7, debugid=0x40c0015, eventid=0x40c0014,
238
+ func_qualifier=1)
239
+ trace = Trace([event], Color.RED, UnknownEnumValue(9), [Color.RED], UUID(int=1), b'\x01\x02', Inner([event], 3))
240
+ parser = PyKdebugParser()
241
+ assert parser.trace_dict(trace)['fields'] == {
242
+ 'color': 'RED', 'unknown': 9, 'flags': ['RED'], 'uuid': '00000000-0000-0000-0000-000000000001',
243
+ 'data': '0102', 'inner': {'value': 3},
244
+ }
245
+ # Without the device's time information there is no wall-clock time.
246
+ assert parser.trace_dict(trace)['time'] is None
@@ -1,22 +0,0 @@
1
- from dataclasses import dataclass
2
- from typing import List
3
-
4
-
5
- @dataclass
6
- class VfsLookup:
7
- ktraces: List
8
- path: str
9
- vnode_id: int
10
-
11
- def __str__(self):
12
- return f'lookup("{self.path}"), vnode id: {self.vnode_id}'
13
-
14
-
15
- def handle_vfs_lookup(parser, events):
16
- node = parser.parse_vnode(events)
17
- return VfsLookup(events, node.path, node.vnode_id)
18
-
19
-
20
- handlers = {
21
- 'VFS_LOOKUP': handle_vfs_lookup,
22
- }
File without changes
File without changes
File without changes