pykdebugparser 1.2.7__tar.gz → 1.3.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 (48) hide show
  1. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/PKG-INFO +5 -4
  2. pykdebugparser-1.3.0/pykdebugparser/kd_buf_parser.py +314 -0
  3. pykdebugparser-1.3.0/pykdebugparser/kevent.py +46 -0
  4. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser/os_log_event.py +24 -21
  5. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser/pykdebugparser.py +44 -12
  6. pykdebugparser-1.3.0/pykdebugparser/trace_codes.py +52 -0
  7. pykdebugparser-1.3.0/pykdebugparser/trace_handlers/__init__.py +23 -0
  8. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser/trace_handlers/bsd.py +19 -16
  9. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser/trace_handlers/mach.py +26 -14
  10. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser/trace_handlers/perf.py +13 -3
  11. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser/trace_handlers/turnstile.py +4 -2
  12. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser/traces_parser.py +25 -20
  13. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser.egg-info/PKG-INFO +5 -4
  14. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser.egg-info/SOURCES.txt +2 -9
  15. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser.egg-info/requires.txt +1 -1
  16. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser.egg-info/top_level.txt +0 -1
  17. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pyproject.toml +8 -8
  18. pykdebugparser-1.3.0/tests/test_kd_buf_parser.py +121 -0
  19. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/tests/test_os_log_event.py +35 -1
  20. pykdebugparser-1.3.0/tests/test_pykdebugparser.py +148 -0
  21. pykdebugparser-1.3.0/tests/test_trace_codes.py +26 -0
  22. pykdebugparser-1.2.7/pykdebugparser/kd_buf_parser.py +0 -211
  23. pykdebugparser-1.2.7/pykdebugparser/kevent.py +0 -31
  24. pykdebugparser-1.2.7/pykdebugparser/trace_codes.py +0 -30
  25. pykdebugparser-1.2.7/pykdebugparser/trace_handlers/__init__.py +0 -0
  26. pykdebugparser-1.2.7/tests/callstacks/test_callstacks_parser.py +0 -91
  27. pykdebugparser-1.2.7/tests/conftest.py +0 -15
  28. pykdebugparser-1.2.7/tests/test_pykdebugparser.py +0 -39
  29. pykdebugparser-1.2.7/tests/test_trace_codes.py +0 -13
  30. pykdebugparser-1.2.7/tests/traces/test_bsd.py +0 -32
  31. pykdebugparser-1.2.7/tests/traces/test_dyld.py +0 -251
  32. pykdebugparser-1.2.7/tests/traces/test_mach.py +0 -452
  33. pykdebugparser-1.2.7/tests/traces/test_perf.py +0 -118
  34. pykdebugparser-1.2.7/tests/traces/test_trace.py +0 -25
  35. pykdebugparser-1.2.7/tests/traces/test_turnstile.py +0 -127
  36. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/LICENSE +0 -0
  37. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/README.md +0 -0
  38. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser/__init__.py +0 -0
  39. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser/__main__.py +0 -0
  40. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser/callstacks_parser.py +0 -0
  41. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser/trace.codes +0 -0
  42. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser/trace_handlers/dyld.py +0 -0
  43. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser/trace_handlers/fsystem.py +0 -0
  44. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser/trace_handlers/trace.py +0 -0
  45. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser.egg-info/dependency_links.txt +0 -0
  46. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/pykdebugparser.egg-info/entry_points.txt +0 -0
  47. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/setup.cfg +0 -0
  48. {pykdebugparser-1.2.7 → pykdebugparser-1.3.0}/tests/test_kevent.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pykdebugparser
3
- Version: 1.2.7
3
+ Version: 1.3.0
4
4
  Summary: Python parser for kdebug events
5
5
  Author: Matan Perelman
6
6
  Author-email: matan1008@gmail.com
@@ -28,16 +28,17 @@ License: MIT License
28
28
 
29
29
  Project-URL: Homepage, https://github.com/matan1008/pykdebugparser
30
30
  Project-URL: Source, https://github.com/matan1008/pykdebugparser
31
- Classifier: Programming Language :: Python :: 3.8
32
31
  Classifier: Programming Language :: Python :: 3.9
33
32
  Classifier: Programming Language :: Python :: 3.10
34
33
  Classifier: Programming Language :: Python :: 3.11
35
34
  Classifier: Programming Language :: Python :: 3.12
36
35
  Classifier: Programming Language :: Python :: 3.13
37
- Requires-Python: >=3.8
36
+ Classifier: Programming Language :: Python :: 3.14
37
+ Classifier: Programming Language :: Python :: 3.15
38
+ Requires-Python: >=3.9
38
39
  Description-Content-Type: text/markdown
39
40
  License-File: LICENSE
40
- Requires-Dist: construct
41
+ Requires-Dist: bytestruct>=0.1.0
41
42
  Requires-Dist: pygments
42
43
  Requires-Dist: click
43
44
  Requires-Dist: termcolor
@@ -0,0 +1,314 @@
1
+ from dataclasses import dataclass, field
2
+ import io
3
+ import plistlib
4
+ import struct
5
+ from collections import namedtuple
6
+ from typing import Annotated, Any
7
+
8
+ from bytestruct import GREEDY_BYTES, U64_LE, BinaryParseError, BinaryStruct, ByteReader, Bytes, CodecBase, \
9
+ PaddedString, Prefixed, TruncatedDataError, u32, u64
10
+
11
+ from pykdebugparser.kevent import from_kd_buf, from_kd_bufs, KD_BUF_FORMAT
12
+ from pykdebugparser.os_log_event import OsLogEvent
13
+
14
+ KEVENT_SIZE = struct.calcsize(KD_BUF_FORMAT)
15
+ # Upper bound for a single bulk read of kevents.
16
+ KEVENTS_READ_SIZE = KEVENT_SIZE * 1024
17
+
18
+ ProcessData = namedtuple('ProcessData', ['pid', 'name'])
19
+
20
+ RAW_VERSION_SIZE = 4
21
+ RAW_VERSION2_BYTES = b'\x00\x02\xaa\x55'
22
+ RAW_VERSION3_BYTES = b'\x00\x03\xaa\x55'
23
+ # Header flag of a 64-bit kernel.
24
+ KDBG_LP64 = 0x1
25
+ TRACEV3_STACKSHOT_END = b'stackshot_out_fl'
26
+ TRACEV3_THREADMAP_TAG = b'\x00\x1d\x00\x00\x00\x00\x00\x00'
27
+ TRACEV3_EVENTS_TAG = b'\x00\x1e\x00\x00\x00\x00\x00\x00'
28
+ TRACEV3_MORE_EVENTS = b'\x00\x20\x00\x00\x00\x00\x00\x00'
29
+ TRACEV3_DYLD_MODULES = b'\x01\x80\x00\x00\x00\x00\x00\x00'
30
+ TRACEV3_TRACE_CODES = b'\x0f\x80\x00\x00\x00\x00\x00\x00'
31
+ TRACEV3_PROCESSES = b'\x10\x80\x00\x00\x00\x00\x00\x00'
32
+ TRACEV3_LOG_EVENTS = b'\x11\x80\x00\x00\x00\x00\x00\x00'
33
+ TRACEV3_LOG_STRINGS = b'\x12\x80\x00\x00\x00\x00\x00\x00'
34
+ TRACEV3_KERNEL_EXTENSIONS = b'\x05\x80\x00\x00\x00\x00\x00\x00'
35
+ TRACEV3_IMAGES = b'\x04\x80\x00\x00\x01\x00\x00\x00'
36
+
37
+
38
+ class Bplist(CodecBase[Any]):
39
+ """
40
+ Codec for a plist (all remaining bytes).
41
+ """
42
+
43
+ def read(self, reader: ByteReader) -> Any:
44
+ return plistlib.loads(GREEDY_BYTES.read(reader))
45
+
46
+ def build(self, value: Any) -> bytes:
47
+ return plistlib.dumps(value)
48
+
49
+
50
+ @dataclass
51
+ class KdThreadmap(BinaryStruct, byte_order='<'):
52
+ tid: u64
53
+ pid: u32
54
+ process: Annotated[str, PaddedString(0x14)]
55
+
56
+
57
+ @dataclass
58
+ class KdHeaderV2(BinaryStruct, byte_order='<'):
59
+ """
60
+ Header of a trace version 2 (`RAW_VERSION2`), as written by kperf's `kperf_buffer_readdata`.
61
+ The fields up to `tod_usecs` follow XNU's `RAW_header`; kperf always writes the time of day as zero.
62
+ """
63
+ number_of_threads: u32
64
+ tod_secs: u64
65
+ tod_usecs: u32
66
+ flags: u32
67
+ tick_frequency: u64
68
+ # Never written by kperf, so it holds whatever the buffer contained before.
69
+ _unused: Annotated[bytes, Bytes(0x100)] = field(default=bytes(0x100), init=False, repr=False, compare=False)
70
+
71
+ @property
72
+ def is_64bit(self) -> bool:
73
+ return bool(self.flags & KDBG_LP64)
74
+
75
+
76
+ @dataclass
77
+ class KdHeaderV3(BinaryStruct, byte_order='<'):
78
+ tag: u32
79
+ sub_tag: u32
80
+ length: u64
81
+ timebase_numer: u32
82
+ timebase_denom: u32
83
+ timestamp: u64
84
+ walltime_secs: u64
85
+ walltime_usecs: u32
86
+ timezone_minuteswest: u32
87
+ timezone_dst: u32
88
+ flags: u32
89
+ tag2: u32
90
+ cpu_info: Annotated[Any, Prefixed(U64_LE, Bplist())]
91
+
92
+
93
+ @dataclass
94
+ class KdV3Block(BinaryStruct, byte_order='<'):
95
+ """
96
+ An additional data block of a trace version 3, aligned to 8 bytes (unless it is the last one).
97
+ """
98
+ tag: Annotated[bytes, Bytes(8)]
99
+ data: Annotated[bytes, Prefixed(U64_LE, GREEDY_BYTES)]
100
+
101
+
102
+ def read_exactly(reader: io.IOBase, size: int) -> bytes:
103
+ """
104
+ Read exactly `size` bytes from a stream.
105
+ :param reader: Stream to read from.
106
+ :param size: Number of bytes to read.
107
+ :return: The data read.
108
+ :raises TruncatedDataError: The stream ended first.
109
+ """
110
+ data = reader.read(size)
111
+ if len(data) != size:
112
+ raise TruncatedDataError(f'expected {size} bytes, found {len(data)}')
113
+ return data
114
+
115
+
116
+ def skip_zeros(reader: io.IOBase):
117
+ """
118
+ Skip zero bytes in a stream, leaving it at the first non-zero byte (or at its end).
119
+ Reads a byte at a time and seeks back once, so a stream never consumes data past the zeros.
120
+ :param reader: Stream to skip in.
121
+ """
122
+ while True:
123
+ position = reader.tell()
124
+ if reader.read(1) != b'\x00':
125
+ reader.seek(position, 0)
126
+ return
127
+
128
+
129
+ def parse_threadmap(data: bytes) -> list:
130
+ """
131
+ Parse consecutive threadmap entries, stopping at the first one that can't be parsed.
132
+ :param data: Threadmap entries.
133
+ :return: Parsed entries.
134
+ """
135
+ reader = ByteReader(data)
136
+ threadmap = []
137
+ while reader.remaining:
138
+ try:
139
+ threadmap.append(KdThreadmap.read(reader))
140
+ except BinaryParseError:
141
+ break
142
+ return threadmap
143
+
144
+
145
+ def parse_v3_blocks(data: bytes) -> list:
146
+ """
147
+ Parse the additional data blocks of a trace version 3, stopping at the first one that can't be parsed.
148
+ :param data: Additional data blocks.
149
+ :return: Parsed blocks.
150
+ """
151
+ reader = ByteReader(data)
152
+ blocks = []
153
+ while True:
154
+ try:
155
+ block = KdV3Block.read(reader)
156
+ except BinaryParseError:
157
+ break
158
+ blocks.append(block)
159
+ # The last block may be unpadded.
160
+ reader.skip(min(-(KdV3Block.SIZE + 8 + len(block.data)) % 8, reader.remaining))
161
+ return blocks
162
+
163
+
164
+ def seek_until(reader, data: bytes):
165
+ """
166
+ Read from a stream until matching data.
167
+ Reading is aligned to the data size.
168
+ :param reader: Stream to read from.
169
+ :param data: Data to match.
170
+ """
171
+ found = reader.read(len(data))
172
+ while found != data:
173
+ found = found[1:] + reader.read(1)
174
+
175
+
176
+ class KdBufParser:
177
+ """
178
+ Parser for raw kd_buf buffer.
179
+ """
180
+
181
+ def __init__(self, threads_pids=None, pids_names=None):
182
+ self.threads_pids = {} if threads_pids is None else threads_pids
183
+ self.pids_names = {} if pids_names is None else pids_names
184
+ self.versions = {
185
+ RAW_VERSION2_BYTES: self.parse_v2,
186
+ RAW_VERSION3_BYTES: self.parse_v3,
187
+ }
188
+ self.trace_codes = ''
189
+ self.images = {}
190
+ self.dyld_modules = {}
191
+ self.processes = {}
192
+ self.kernel_extensions = {'Binaries': []}
193
+ self.v3_header = None
194
+
195
+ def parse(self, reader: io.IOBase):
196
+ """
197
+ Parse kevents from a stream.
198
+ :param reader: Stream to read from.
199
+ :return: Generator for parsed kevents.
200
+ """
201
+ version = reader.read(RAW_VERSION_SIZE)
202
+ return self.versions[version](reader)
203
+
204
+ def set_thread_map(self, parsed_threadmap):
205
+ self.threads_pids.clear()
206
+ self.pids_names.clear()
207
+ for thread in parsed_threadmap:
208
+ self.threads_pids[thread.tid] = thread.pid
209
+ self.pids_names[thread.pid] = thread.process
210
+
211
+ def parse_v2(self, reader: io.IOBase):
212
+ """
213
+ Parse trace version 2.
214
+ :param reader: Stream to parse.
215
+ :return: Generator for parsed kevents.
216
+ """
217
+ header = KdHeaderV2.parse(read_exactly(reader, KdHeaderV2.SIZE))
218
+ self.set_thread_map([KdThreadmap.parse(read_exactly(reader, KdThreadmap.SIZE))
219
+ for _ in range(header.number_of_threads)])
220
+ skip_zeros(reader)
221
+ read1 = getattr(reader, 'read1', None)
222
+ if read1 is None:
223
+ # Without `read1` a bigger `read` might block until more data than currently available arrives.
224
+ while True:
225
+ buf = reader.read(KEVENT_SIZE)
226
+ if not buf:
227
+ break
228
+ yield from_kd_buf(buf)
229
+ return
230
+ # `read1` returns whatever is available, so kevents are decoded in bulk without delaying live streams.
231
+ leftover = b''
232
+ while True:
233
+ buf = read1(KEVENTS_READ_SIZE)
234
+ if not buf:
235
+ break
236
+ if leftover:
237
+ buf = leftover + buf
238
+ end = len(buf) - len(buf) % KEVENT_SIZE
239
+ if end != len(buf):
240
+ leftover = buf[end:]
241
+ buf = buf[:end]
242
+ else:
243
+ leftover = b''
244
+ yield from from_kd_bufs(buf)
245
+ if leftover:
246
+ # A truncated kevent, fail just like a non-bulk read would.
247
+ yield from_kd_buf(leftover)
248
+
249
+ def parse_v3(self, reader: io.IOBase):
250
+ """
251
+ Parse trace version 3.
252
+ :param reader: Stream to parse.
253
+ :return: Generator for parsed kevents.
254
+ """
255
+ header = read_exactly(reader, KdHeaderV3.SIZE + U64_LE.size)
256
+ header += read_exactly(reader, U64_LE.unpack_from(header, KdHeaderV3.SIZE)[0])
257
+ read_exactly(reader, -len(header) % 8)
258
+ self.v3_header = KdHeaderV3.parse(header)
259
+ # Align the reader to 8 bytes from the beginning of the stream
260
+ reader.read(8 - RAW_VERSION_SIZE)
261
+ # The threadmap tag appears randomly in the stackshot.
262
+ seek_until(reader, TRACEV3_STACKSHOT_END)
263
+ seek_until(reader, TRACEV3_THREADMAP_TAG)
264
+ threadmap_size = U64_LE.parse(read_exactly(reader, U64_LE.size))
265
+ self.set_thread_map(parse_threadmap(read_exactly(reader, threadmap_size)))
266
+
267
+ while True:
268
+ seek_until(reader, TRACEV3_EVENTS_TAG)
269
+ size = U64_LE.parse(read_exactly(reader, U64_LE.size))
270
+ reader.read(8) # All zeros, unknown.
271
+ for _ in range(size // KEVENT_SIZE):
272
+ buf = reader.read(KEVENT_SIZE)
273
+ yield from_kd_buf(buf)
274
+ if reader.read(len(TRACEV3_MORE_EVENTS)) != TRACEV3_MORE_EVENTS:
275
+ break
276
+ reader.seek(-8, 1)
277
+
278
+ additional_data = parse_v3_blocks(reader.read())
279
+
280
+ self.trace_codes = ''
281
+ self.kernel_extensions = {'Binaries': []}
282
+ self.dyld_modules = {}
283
+ self.images = {}
284
+ self.processes = {}
285
+
286
+ log_events = []
287
+ log_strings = {}
288
+
289
+ for block in additional_data:
290
+ if block.tag == TRACEV3_DYLD_MODULES:
291
+ data = plistlib.loads(block.data)
292
+ if not self.dyld_modules:
293
+ self.dyld_modules.update(data)
294
+ else:
295
+ self.dyld_modules['Binaries'].extend(data['Binaries'])
296
+ elif block.tag == TRACEV3_TRACE_CODES:
297
+ self.trace_codes += block.data.decode()
298
+ elif block.tag == TRACEV3_PROCESSES:
299
+ self.processes = plistlib.loads(block.data)
300
+ elif block.tag == TRACEV3_KERNEL_EXTENSIONS:
301
+ self.kernel_extensions['Binaries'].extend(plistlib.loads(block.data)['Binaries'])
302
+ elif block.tag == TRACEV3_IMAGES:
303
+ self.images = plistlib.loads(block.data)
304
+ elif block.tag == TRACEV3_LOG_EVENTS:
305
+ log_events.extend(plistlib.loads(block.data)['Events'])
306
+ elif block.tag == TRACEV3_LOG_STRINGS:
307
+ log_strings = {v: k for k, v in plistlib.loads(block.data)['StringIndex'].items()}
308
+
309
+ for event in log_events:
310
+ log_event = OsLogEvent.from_raw_log_event(event, log_strings)
311
+ if log_event.process and log_event.thread_identifier:
312
+ self.threads_pids[log_event.thread_identifier] = log_event.process_identifier
313
+ self.pids_names[log_event.process_identifier] = log_event.process
314
+ yield log_event
@@ -0,0 +1,46 @@
1
+ from collections import namedtuple
2
+ import enum
3
+ import struct
4
+
5
+ KDBG_EVENTID_MASK = 0xfffffffc
6
+ KDBG_FUNC_MASK = 0x00000003
7
+ KD_BUF_FORMAT = '<Q32sQIIQ'
8
+ # Same layout as `KD_BUF_FORMAT`, with the 32 bytes of arguments unpacked as 4 64-bit values.
9
+ KD_BUF_STRUCT = struct.Struct('<QQQQQQIIQ')
10
+ KD_BUF_SIZE = KD_BUF_STRUCT.size
11
+ Kevent = namedtuple('Kevent', ['timestamp', 'data', 'values', 'tid', 'debugid', 'eventid', 'func_qualifier'])
12
+
13
+
14
+ class DgbFuncQual(enum.Enum):
15
+ """
16
+ Event's role in the trace.
17
+ """
18
+ DBG_FUNC_NONE = 0
19
+ DBG_FUNC_START = 1
20
+ DBG_FUNC_END = 2
21
+ DBG_FUNC_ALL = 3
22
+
23
+
24
+ def from_kd_buf(kd_buf: bytes) -> Kevent:
25
+ """
26
+ Create a Kevent object from a kd_buf kevent's struct.
27
+ :param kd_buf: Buffer of kd_buf kevent's struct.
28
+ :return: Parsed kevent.
29
+ """
30
+ timestamp, arg0, arg1, arg2, arg3, tid, debugid, cpuid, unused = KD_BUF_STRUCT.unpack(kd_buf)
31
+ return Kevent(timestamp, kd_buf[8:40], (arg0, arg1, arg2, arg3), tid, debugid, debugid & KDBG_EVENTID_MASK,
32
+ debugid & KDBG_FUNC_MASK)
33
+
34
+
35
+ def from_kd_bufs(kd_bufs: bytes):
36
+ """
37
+ Create Kevent objects from consecutive kd_buf kevent's structs.
38
+ :param kd_bufs: Buffer of kd_buf kevent's structs, its length must be a multiple of the struct size.
39
+ :return: Generator of parsed kevents.
40
+ """
41
+ new_kevent = tuple.__new__ # Skips the namedtuple's Python-level __new__, the result is identical.
42
+ offset = 8
43
+ for timestamp, arg0, arg1, arg2, arg3, tid, debugid, cpuid, unused in KD_BUF_STRUCT.iter_unpack(kd_bufs):
44
+ yield new_kevent(Kevent, (timestamp, kd_bufs[offset:offset + 32], (arg0, arg1, arg2, arg3), tid, debugid,
45
+ debugid & KDBG_EVENTID_MASK, debugid & KDBG_FUNC_MASK))
46
+ offset += KD_BUF_SIZE
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
3
3
  import enum
4
4
  from typing import List, Dict
5
5
 
6
- from construct import Struct, Byte, Int32ul, BitStruct, Padding, Flag, BitsInteger, Int64ul
6
+ from bytestruct import U64_LE, BinaryStruct, u8, u32
7
7
 
8
8
 
9
9
  class OsLogType(enum.Enum):
@@ -81,7 +81,7 @@ class FirehoseTracepointSignpostType(enum.IntFlag):
81
81
  scope_system = 0xc0
82
82
 
83
83
 
84
- class FirehoseTracepointSingpostFlags(enum.Enum):
84
+ class FirehoseTracepointSingpostFlags(enum.IntFlag):
85
85
  has_private_data = 1
86
86
  has_subsystem = 2
87
87
  has_rules = 4
@@ -100,22 +100,24 @@ tracepoint_types = {
100
100
 
101
101
  tracepoint_flags = {
102
102
  FirehoseTracepointNamespace.log: FirehoseTracepointLogFlags,
103
- FirehoseTracepointNamespace.trace: FirehoseTracepointSingpostFlags,
103
+ FirehoseTracepointNamespace.signpost: FirehoseTracepointSingpostFlags,
104
104
  }
105
105
 
106
- firehose_tracepoint_id = Struct(
107
- 'namespace' / Byte,
108
- 'type_' / Byte,
109
- 'trace_flags' / BitStruct(
110
- Padding(2),
111
- 'has_large_offset' / Flag,
112
- 'has_unique_pid' / Flag,
113
- 'pc_style' / BitsInteger(3),
114
- 'has_current_aid' / Flag,
115
- ),
116
- 'flags' / Byte,
117
- 'code' / Int32ul,
118
- )
106
+ # `trace_flags` bits.
107
+ TRACE_FLAGS_HAS_LARGE_OFFSET = 0x20
108
+ TRACE_FLAGS_HAS_UNIQUE_PID = 0x10
109
+ TRACE_FLAGS_PC_STYLE_SHIFT = 1
110
+ TRACE_FLAGS_PC_STYLE_MASK = 0x7
111
+ TRACE_FLAGS_HAS_CURRENT_AID = 0x01
112
+
113
+
114
+ @dataclass
115
+ class FirehoseTracepointId(BinaryStruct, byte_order='<'):
116
+ namespace: u8
117
+ type_: u8
118
+ trace_flags: u8
119
+ flags: u8
120
+ code: u32
119
121
 
120
122
 
121
123
  @dataclass
@@ -262,7 +264,8 @@ class OsLogEvent:
262
264
 
263
265
  @classmethod
264
266
  def parse_trace_identifier(cls, trace_identifier):
265
- trace_id = firehose_tracepoint_id.parse(Int64ul.build(trace_identifier))
267
+ trace_id = FirehoseTracepointId.parse(U64_LE.build(trace_identifier))
268
+ trace_flags = trace_id.trace_flags
266
269
  trace_namespace = FirehoseTracepointNamespace(trace_id.namespace)
267
270
  if trace_namespace in tracepoint_types:
268
271
  type_ = tracepoint_types[trace_namespace](trace_id.type_)
@@ -275,10 +278,10 @@ class OsLogEvent:
275
278
  return TraceIdentifier(
276
279
  namespace=trace_namespace,
277
280
  type_=type_,
278
- has_large_offset=trace_id.trace_flags.has_large_offset,
279
- has_unique_pid=trace_id.trace_flags.has_unique_pid,
280
- pc_style=FirehoseTracepointFlagsPcStyle(trace_id.trace_flags.pc_style),
281
- has_current_aid=trace_id.trace_flags.has_current_aid,
281
+ has_large_offset=bool(trace_flags & TRACE_FLAGS_HAS_LARGE_OFFSET),
282
+ has_unique_pid=bool(trace_flags & TRACE_FLAGS_HAS_UNIQUE_PID),
283
+ pc_style=FirehoseTracepointFlagsPcStyle((trace_flags >> TRACE_FLAGS_PC_STYLE_SHIFT) & TRACE_FLAGS_PC_STYLE_MASK),
284
+ has_current_aid=bool(trace_flags & TRACE_FLAGS_HAS_CURRENT_AID),
282
285
  flags=tracepoint_flags[trace_namespace](
283
286
  trace_id.flags) if trace_namespace in tracepoint_flags else None,
284
287
  code=trace_id.code,
@@ -1,5 +1,6 @@
1
1
  from datetime import datetime
2
2
  import io
3
+ import math
3
4
 
4
5
  from pygments import highlight, lexers, formatters
5
6
  from termcolor import colored
@@ -25,6 +26,8 @@ class PyKdebugParser:
25
26
  self.filter_process = None
26
27
  self.filter_class = []
27
28
  self.filter_subclass = []
29
+ # Event ids (debugids without the function qualifier) to include, on top of the class/subclass filters.
30
+ self.filter_eventid = set()
28
31
  self.show_timestamp = True
29
32
  self.show_name = True
30
33
  self.show_func_qual = True
@@ -41,15 +44,29 @@ class PyKdebugParser:
41
44
  self.pids_names = {}
42
45
  self.dyld_addresses = []
43
46
  self.dyld_uuids = []
47
+ self._timestamp_cache = None
44
48
 
45
49
  def kevents(self, kdebug: io.IOBase):
46
50
  events_generator = KdBufParser(self.threads_pids, self.pids_names).parse(kdebug)
47
- events_generator = filter(lambda e: not isinstance(e, OsLogEvent), events_generator)
48
- if self.filter_tid is not None:
49
- events_generator = filter(lambda e: e.tid == self.filter_tid, events_generator)
50
- if self.filter_class or self.filter_subclass:
51
- events_generator = filter(lambda e: self._is_eventid_allowed(e.eventid), events_generator)
52
- return events_generator
51
+ return self._filter_kevents(events_generator, self.filter_tid is not None,
52
+ bool(self.filter_class or self.filter_subclass or self.filter_eventid))
53
+
54
+ def _filter_kevents(self, events_generator, by_tid, by_class):
55
+ # A single generator instead of a `filter` chain, avoiding a lambda call per kevent per filter.
56
+ filter_class = self.filter_class
57
+ filter_subclass = self.filter_subclass
58
+ filter_eventid = set(self.filter_eventid)
59
+ for event in events_generator:
60
+ if isinstance(event, OsLogEvent):
61
+ continue
62
+ if by_tid and event.tid != self.filter_tid:
63
+ continue
64
+ if by_class:
65
+ eventid = event.eventid
66
+ if not ((eventid >> 24 in filter_class) or (eventid >> 16 in filter_subclass)
67
+ or eventid in filter_eventid):
68
+ continue
69
+ yield event
53
70
 
54
71
  def formatted_kevents(self, kdebug: io.IOBase, trace_codes=None):
55
72
  trace_codes_map = default_trace_codes() if trace_codes is None else trace_codes
@@ -58,11 +75,12 @@ class PyKdebugParser:
58
75
  def traces(self, kdebug: io.IOBase, trace_codes=None):
59
76
  trace_codes_map = default_trace_codes() if trace_codes is None else trace_codes
60
77
 
61
- has_filters = self.filter_class or self.filter_subclass
78
+ has_filters = self.filter_class or self.filter_subclass or self.filter_eventid
62
79
  add_trace_class = has_filters and DBG_TRACE not in self.filter_class
63
80
  if add_trace_class:
64
81
  self.filter_class.append(DBG_TRACE)
65
- has_bsd = DBG_BSD in self.filter_class or any(filter(lambda sc: sc >> 8 == DBG_BSD, self.filter_subclass))
82
+ has_bsd = (DBG_BSD in self.filter_class or any(sc >> 8 == DBG_BSD for sc in self.filter_subclass)
83
+ or any(eventid >> 24 == DBG_BSD for eventid in self.filter_eventid))
66
84
  add_fs_class = has_filters and has_bsd and DBG_FSYSTEM not in self.filter_class
67
85
  if add_fs_class:
68
86
  self.filter_class.append(DBG_FSYSTEM)
@@ -113,9 +131,22 @@ class PyKdebugParser:
113
131
  offset_usec = (
114
132
  ((timestamp - self.mach_absolute_time) * self.numer) / (self.denom * 1000)
115
133
  )
116
- ts = datetime.fromtimestamp((self.usecs_since_epoch + offset_usec) / 1000000, tz=self.timezone)
117
- time_string = ts.strftime('%Y-%m-%d %H:%M:%S.%f')
118
- return f'{time_string:<27}'
134
+ posix_time = (self.usecs_since_epoch + offset_usec) / 1000000
135
+ # Split to seconds and microseconds exactly like `datetime.fromtimestamp` does (round half to even), so
136
+ # the date and time part can be formatted only once per second.
137
+ fraction, seconds = math.modf(posix_time)
138
+ microseconds = round(fraction * 1000000)
139
+ if microseconds >= 1000000:
140
+ microseconds -= 1000000
141
+ seconds += 1.0
142
+ elif microseconds < 0:
143
+ microseconds += 1000000
144
+ seconds -= 1.0
145
+ cache = self._timestamp_cache
146
+ if cache is None or cache[0] != seconds or cache[1] is not self.timezone:
147
+ prefix = datetime.fromtimestamp(seconds, tz=self.timezone).strftime('%Y-%m-%d %H:%M:%S.')
148
+ cache = self._timestamp_cache = (seconds, self.timezone, prefix)
149
+ return f'{cache[2]}{microseconds:06d}'.ljust(27)
119
150
 
120
151
  def _format_process(self, tid):
121
152
  pid = self.threads_pids.get(tid, -1)
@@ -184,4 +215,5 @@ class PyKdebugParser:
184
215
  return event_rep
185
216
 
186
217
  def _is_eventid_allowed(self, event_id):
187
- return (event_id >> 24 in self.filter_class) or (event_id >> 16 in self.filter_subclass)
218
+ return ((event_id >> 24 in self.filter_class) or (event_id >> 16 in self.filter_subclass)
219
+ or event_id in self.filter_eventid)
@@ -0,0 +1,52 @@
1
+ from typing import Iterable, Mapping, Optional, Set
2
+ from pathlib import Path
3
+
4
+
5
+ def from_trace_codes_text(codes_text: str) -> Mapping[int, str]:
6
+ """
7
+ Convert a trace codes text to dictionary.
8
+ :param codes_text: Trace codes file data.
9
+ :return: Mapping between code and event name.
10
+ """
11
+ return {int(s[0], 16): s[1] for s in map(lambda l: l.split(), codes_text.splitlines())}
12
+
13
+
14
+ def from_trace_codes_file(path: str) -> Mapping[int, str]:
15
+ """
16
+ Read trace codes from a file.
17
+ :param path: Trace codes file path.
18
+ :return: Mapping between code and event name.
19
+ """
20
+ with open(path, 'r') as fd:
21
+ return from_trace_codes_text(fd.read())
22
+
23
+
24
+ def default_trace_codes() -> Mapping[int, str]:
25
+ """
26
+ Get the default trace codes mapping.
27
+ :return: Mapping between code and event name.
28
+ """
29
+ with open(Path(__file__).resolve().parent.joinpath('trace.codes'), 'r') as fd:
30
+ return from_trace_codes_text(fd.read())
31
+
32
+
33
+ def syscall_eventids(names: Iterable[str], trace_codes: Optional[Mapping[int, str]] = None) -> Set[int]:
34
+ """
35
+ Get the event ids of BSD syscalls by name, for `PyKdebugParser.filter_eventid`.
36
+ A name also matches its `_nocancel` variant (e.g. `open` gives `BSC_open` and `BSC_open_nocancel`).
37
+ :param names: Syscall names, such as `open` or `openat`.
38
+ :param trace_codes: Mapping between code and event name; the default trace codes if omitted.
39
+ :return: The event ids.
40
+ :raises ValueError: A name is not a known syscall.
41
+ """
42
+ codes = {name: code for code, name in (default_trace_codes() if trace_codes is None else trace_codes).items()}
43
+ eventids = set()
44
+ unknown = []
45
+ for name in names:
46
+ matches = {codes[event] for event in (f'BSC_{name}', f'BSC_{name}_nocancel') if event in codes}
47
+ if not matches:
48
+ unknown.append(name)
49
+ eventids |= matches
50
+ if unknown:
51
+ raise ValueError(f'unknown syscalls: {", ".join(unknown)}')
52
+ return eventids
@@ -0,0 +1,23 @@
1
+ class UnknownEnumValue(int):
2
+ """
3
+ A raw value that its enum doesn't define (e.g. a combination of flags or a value added in a newer OS).
4
+ It has `name` and `value` like an enum member, so it is formatted as the raw number instead of failing the parse.
5
+ """
6
+
7
+ @property
8
+ def name(self) -> str:
9
+ return str(int(self))
10
+
11
+ @property
12
+ def value(self) -> int:
13
+ return int(self)
14
+
15
+
16
+ def parse_enum(enum_type, value):
17
+ """
18
+ Convert a raw value to an enum member, falling back to `UnknownEnumValue` for values the enum doesn't define.
19
+ """
20
+ try:
21
+ return enum_type(value)
22
+ except ValueError:
23
+ return UnknownEnumValue(value)