pykdebugparser 1.2.7__py2.py3-none-any.whl → 1.3.0__py2.py3-none-any.whl

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.
@@ -1,21 +1,27 @@
1
- import struct
2
- from collections import namedtuple
1
+ from dataclasses import dataclass, field
3
2
  import io
4
3
  import plistlib
4
+ import struct
5
+ from collections import namedtuple
6
+ from typing import Annotated, Any
5
7
 
6
- from construct import Adapter, Struct, Const, Padding, Int32ul, Int64ul, Array, GreedyRange, Byte, FixedSized, \
7
- CString, Prefixed, GreedyBytes, Aligned, Bytes, Select
8
+ from bytestruct import GREEDY_BYTES, U64_LE, BinaryParseError, BinaryStruct, ByteReader, Bytes, CodecBase, \
9
+ PaddedString, Prefixed, TruncatedDataError, u32, u64
8
10
 
9
- from pykdebugparser.kevent import from_kd_buf, KD_BUF_FORMAT
11
+ from pykdebugparser.kevent import from_kd_buf, from_kd_bufs, KD_BUF_FORMAT
10
12
  from pykdebugparser.os_log_event import OsLogEvent
11
13
 
12
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
13
17
 
14
18
  ProcessData = namedtuple('ProcessData', ['pid', 'name'])
15
19
 
16
20
  RAW_VERSION_SIZE = 4
17
21
  RAW_VERSION2_BYTES = b'\x00\x02\xaa\x55'
18
22
  RAW_VERSION3_BYTES = b'\x00\x03\xaa\x55'
23
+ # Header flag of a 64-bit kernel.
24
+ KDBG_LP64 = 0x1
19
25
  TRACEV3_STACKSHOT_END = b'stackshot_out_fl'
20
26
  TRACEV3_THREADMAP_TAG = b'\x00\x1d\x00\x00\x00\x00\x00\x00'
21
27
  TRACEV3_EVENTS_TAG = b'\x00\x1e\x00\x00\x00\x00\x00\x00'
@@ -28,61 +34,131 @@ TRACEV3_LOG_STRINGS = b'\x12\x80\x00\x00\x00\x00\x00\x00'
28
34
  TRACEV3_KERNEL_EXTENSIONS = b'\x05\x80\x00\x00\x00\x00\x00\x00'
29
35
  TRACEV3_IMAGES = b'\x04\x80\x00\x00\x01\x00\x00\x00'
30
36
 
31
- kd_threadmap = Struct(
32
- 'tid' / Int64ul,
33
- 'pid' / Int32ul,
34
- 'process' / FixedSized(0x14, CString('utf8')),
35
- )
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)
37
48
 
38
- class BplistAdapter(Adapter):
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='<'):
39
59
  """
40
- Construct adapter ti build and parse plists.
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.
41
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)
42
70
 
43
- def _decode(self, obj, context, path):
44
- return plistlib.loads(obj)
71
+ @property
72
+ def is_64bit(self) -> bool:
73
+ return bool(self.flags & KDBG_LP64)
45
74
 
46
- def _encode(self, obj, context, path):
47
- return plistlib.dumps(obj)
48
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())]
49
91
 
50
- kd_header_v2 = Struct(
51
- 'number_of_treads' / Int32ul,
52
- Padding(8),
53
- Padding(4),
54
- 'is_64bit' / Int32ul,
55
- 'tick_frequency' / Int64ul,
56
- Padding(0x100),
57
- 'threadmap' / Array(lambda ctx: ctx.number_of_treads, kd_threadmap),
58
- '_pad' / GreedyRange(Const(0, Byte)),
59
- )
60
92
 
61
- kd_header_v3 = Struct(
62
- 'tag' / Int32ul,
63
- 'sub_tag' / Int32ul,
64
- 'length' / Int64ul,
65
- 'timebase_numer' / Int32ul,
66
- 'timebase_denom' / Int32ul,
67
- 'timestamp' / Int64ul,
68
- 'walltime_secs' / Int64ul,
69
- 'walltime_usecs' / Int32ul,
70
- 'timezone_minuteswest' / Int32ul,
71
- 'timezone_dst' / Int32ul,
72
- 'flags' / Int32ul,
73
- 'tag2' / Int32ul,
74
- 'cpu_info' / Prefixed(Int64ul, BplistAdapter(GreedyBytes)),
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)]
75
100
 
76
- )
77
101
 
78
- kd_v3_threadmap = Struct(
79
- 'threadmap' / Prefixed(Int64ul, GreedyRange(kd_threadmap)),
80
- )
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
81
143
 
82
- kd_v3_additional_data = GreedyRange(Struct(
83
- 'tag' / Bytes(8),
84
- 'data' / Select(Aligned(8, Prefixed(Int64ul, GreedyBytes)), Prefixed(Int64ul, GreedyBytes)),
85
- ))
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
86
162
 
87
163
 
88
164
  def seek_until(reader, data: bytes):
@@ -138,13 +214,37 @@ class KdBufParser:
138
214
  :param reader: Stream to parse.
139
215
  :return: Generator for parsed kevents.
140
216
  """
141
- parsed_header = kd_header_v2.parse_stream(reader)
142
- self.set_thread_map(parsed_header.threadmap)
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''
143
232
  while True:
144
- buf = reader.read(KEVENT_SIZE)
233
+ buf = read1(KEVENTS_READ_SIZE)
145
234
  if not buf:
146
235
  break
147
- yield from_kd_buf(buf)
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)
148
248
 
149
249
  def parse_v3(self, reader: io.IOBase):
150
250
  """
@@ -152,18 +252,21 @@ class KdBufParser:
152
252
  :param reader: Stream to parse.
153
253
  :return: Generator for parsed kevents.
154
254
  """
155
- self.v3_header = Aligned(8, kd_header_v3).parse_stream(reader)
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)
156
259
  # Align the reader to 8 bytes from the beginning of the stream
157
260
  reader.read(8 - RAW_VERSION_SIZE)
158
261
  # The threadmap tag appears randomly in the stackshot.
159
262
  seek_until(reader, TRACEV3_STACKSHOT_END)
160
263
  seek_until(reader, TRACEV3_THREADMAP_TAG)
161
- threadmap = kd_v3_threadmap.parse_stream(reader).threadmap
162
- self.set_thread_map(threadmap)
264
+ threadmap_size = U64_LE.parse(read_exactly(reader, U64_LE.size))
265
+ self.set_thread_map(parse_threadmap(read_exactly(reader, threadmap_size)))
163
266
 
164
267
  while True:
165
268
  seek_until(reader, TRACEV3_EVENTS_TAG)
166
- size = Int64ul.parse_stream(reader)
269
+ size = U64_LE.parse(read_exactly(reader, U64_LE.size))
167
270
  reader.read(8) # All zeros, unknown.
168
271
  for _ in range(size // KEVENT_SIZE):
169
272
  buf = reader.read(KEVENT_SIZE)
@@ -172,7 +275,7 @@ class KdBufParser:
172
275
  break
173
276
  reader.seek(-8, 1)
174
277
 
175
- additional_data = kd_v3_additional_data.parse_stream(reader)
278
+ additional_data = parse_v3_blocks(reader.read())
176
279
 
177
280
  self.trace_codes = ''
178
281
  self.kernel_extensions = {'Binaries': []}
pykdebugparser/kevent.py CHANGED
@@ -5,6 +5,9 @@ import struct
5
5
  KDBG_EVENTID_MASK = 0xfffffffc
6
6
  KDBG_FUNC_MASK = 0x00000003
7
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
8
11
  Kevent = namedtuple('Kevent', ['timestamp', 'data', 'values', 'tid', 'debugid', 'eventid', 'func_qualifier'])
9
12
 
10
13
 
@@ -24,8 +27,20 @@ def from_kd_buf(kd_buf: bytes) -> Kevent:
24
27
  :param kd_buf: Buffer of kd_buf kevent's struct.
25
28
  :return: Parsed kevent.
26
29
  """
27
- timestamp, args_buf, tid, debugid, cpuid, unused = struct.unpack(KD_BUF_FORMAT, kd_buf)
28
- eventid = debugid & KDBG_EVENTID_MASK
29
- qual = debugid & KDBG_FUNC_MASK
30
- args = struct.unpack('<QQQQ', args_buf) # There are 4 arguments, 64 bit each.
31
- return Kevent(timestamp, args_buf, args, tid, debugid, eventid, qual)
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)
@@ -1,4 +1,4 @@
1
- from typing import Mapping
1
+ from typing import Iterable, Mapping, Optional, Set
2
2
  from pathlib import Path
3
3
 
4
4
 
@@ -28,3 +28,25 @@ def default_trace_codes() -> Mapping[int, str]:
28
28
  """
29
29
  with open(Path(__file__).resolve().parent.joinpath('trace.codes'), 'r') as fd:
30
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)