pykdebugparser 1.5.0__tar.gz → 1.6.1__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.
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/PKG-INFO +1 -2
- pykdebugparser-1.6.1/pykdebugparser/highlight.py +66 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/pykdebugparser.py +64 -3
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/trace_handlers/bsd.py +35 -26
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser.egg-info/PKG-INFO +1 -2
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser.egg-info/requires.txt +0 -1
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pyproject.toml +1 -2
- pykdebugparser-1.6.1/tests/test_highlight.py +52 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/tests/test_pykdebugparser.py +65 -0
- pykdebugparser-1.5.0/pykdebugparser/highlight.py +0 -134
- pykdebugparser-1.5.0/tests/test_highlight.py +0 -80
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/LICENSE +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/README.md +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/__init__.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/__main__.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/callstacks_parser.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/kd_buf_parser.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/kevent.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/os_log_event.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/trace.codes +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/trace_codes.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/trace_handlers/__init__.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/trace_handlers/dyld.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/trace_handlers/fsystem.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/trace_handlers/mach.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/trace_handlers/perf.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/trace_handlers/trace.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/trace_handlers/turnstile.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser/traces_parser.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser.egg-info/SOURCES.txt +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser.egg-info/dependency_links.txt +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser.egg-info/entry_points.txt +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/pykdebugparser.egg-info/top_level.txt +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/setup.cfg +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/tests/test_kd_buf_parser.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/tests/test_kevent.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/tests/test_os_log_event.py +0 -0
- {pykdebugparser-1.5.0 → pykdebugparser-1.6.1}/tests/test_trace_codes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pykdebugparser
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.1
|
|
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,4 +1,6 @@
|
|
|
1
|
+
import dataclasses
|
|
1
2
|
from datetime import datetime
|
|
3
|
+
from enum import Enum
|
|
2
4
|
import io
|
|
3
5
|
import math
|
|
4
6
|
|
|
@@ -9,6 +11,7 @@ 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
|
|
12
15
|
from pykdebugparser.trace_handlers.trace import TRACE_LOST_EVENTS
|
|
13
16
|
from pykdebugparser.traces_parser import TracesParser
|
|
14
17
|
from pykdebugparser.os_log_event import OsLogEvent
|
|
@@ -19,6 +22,32 @@ DBG_FSYSTEM = 3
|
|
|
19
22
|
DBG_BSD = 4
|
|
20
23
|
|
|
21
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
|
+
|
|
22
51
|
class PyKdebugParser:
|
|
23
52
|
def __init__(self):
|
|
24
53
|
self.filter_tid = None
|
|
@@ -99,6 +128,32 @@ class PyKdebugParser:
|
|
|
99
128
|
trace_generator = filter(lambda t: t.ktraces[0].eventid >> 24 != DBG_FSYSTEM, trace_generator)
|
|
100
129
|
return trace_generator
|
|
101
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
|
+
|
|
102
157
|
def formatted_traces(self, kdebug: io.IOBase, trace_codes=None):
|
|
103
158
|
return map(lambda t: self._format_trace(t), self.traces(kdebug, trace_codes))
|
|
104
159
|
|
|
@@ -128,13 +183,19 @@ class PyKdebugParser:
|
|
|
128
183
|
process_name = self.pids_names.get(pid, '')
|
|
129
184
|
return self.filter_process == str(pid) or self.filter_process == process_name
|
|
130
185
|
|
|
131
|
-
def
|
|
186
|
+
def _posix_time(self, timestamp):
|
|
187
|
+
"""A kevent timestamp (mach ticks) as POSIX time, or None without the device's time information."""
|
|
132
188
|
if None in (self.mach_absolute_time, self.numer, self.denom, self.usecs_since_epoch, self.timezone):
|
|
133
|
-
return
|
|
189
|
+
return None
|
|
134
190
|
offset_usec = (
|
|
135
191
|
((timestamp - self.mach_absolute_time) * self.numer) / (self.denom * 1000)
|
|
136
192
|
)
|
|
137
|
-
|
|
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) + ' '
|
|
138
199
|
# Split to seconds and microseconds exactly like `datetime.fromtimestamp` does (round half to even), so
|
|
139
200
|
# the date and time part can be formatted only once per second.
|
|
140
201
|
fraction, seconds = math.modf(posix_time)
|
|
@@ -2837,13 +2837,22 @@ DARWIN_SIGNALS = (None, 'SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIG
|
|
|
2837
2837
|
'SIGWINCH', 'SIGINFO', 'SIGUSR1', 'SIGUSR2')
|
|
2838
2838
|
|
|
2839
2839
|
|
|
2840
|
+
def pid(value: int) -> int:
|
|
2841
|
+
"""A `pid_t` argument: a C int, where -1 means any and a negative pid a process group (`kill(-pgid, sig)`)."""
|
|
2842
|
+
return ctypes.c_int32(value).value
|
|
2843
|
+
|
|
2844
|
+
|
|
2845
|
+
def signal_name(sig: int) -> str:
|
|
2846
|
+
"""A Darwin signal by name (`SIGTERM`), or its number (0 only checks that the target exists)."""
|
|
2847
|
+
return DARWIN_SIGNALS[sig] if 0 < sig < len(DARWIN_SIGNALS) else str(sig)
|
|
2848
|
+
|
|
2849
|
+
|
|
2840
2850
|
def format_wait_status(status: int) -> str:
|
|
2841
2851
|
"""Describe a wait status (`W_EXITCODE(code, signal)`) as its exit code or terminating signal."""
|
|
2842
2852
|
signal = status & 0x7f
|
|
2843
2853
|
if not signal:
|
|
2844
2854
|
return f'exit code: {(status >> 8) & 0xff}'
|
|
2845
|
-
|
|
2846
|
-
return f'signal: {name}' + (', core dumped' if status & 0x80 else '')
|
|
2855
|
+
return f'signal: {signal_name(signal)}' + (', core dumped' if status & 0x80 else '')
|
|
2847
2856
|
|
|
2848
2857
|
|
|
2849
2858
|
@dataclass
|
|
@@ -3034,7 +3043,7 @@ class BscPthreadKill:
|
|
|
3034
3043
|
result: str
|
|
3035
3044
|
|
|
3036
3045
|
def __str__(self):
|
|
3037
|
-
rep = f'pthread_kill({self.thread_port}, {self.sig})'
|
|
3046
|
+
rep = f'pthread_kill({self.thread_port}, {signal_name(self.sig)})'
|
|
3038
3047
|
if self.result:
|
|
3039
3048
|
rep += f', {self.result}'
|
|
3040
3049
|
return rep
|
|
@@ -3063,7 +3072,7 @@ class BscKill:
|
|
|
3063
3072
|
result: str
|
|
3064
3073
|
|
|
3065
3074
|
def __str__(self):
|
|
3066
|
-
rep = f'kill({self.pid}, {self.sig})'
|
|
3075
|
+
rep = f'kill({self.pid}, {signal_name(self.sig)})'
|
|
3067
3076
|
if self.result:
|
|
3068
3077
|
rep += f', {self.result}'
|
|
3069
3078
|
return rep
|
|
@@ -3991,7 +4000,7 @@ class BscObsKillpg:
|
|
|
3991
4000
|
result: str
|
|
3992
4001
|
|
|
3993
4002
|
def __str__(self):
|
|
3994
|
-
rep = f'killpg({self.pgrp}, {self.sig})'
|
|
4003
|
+
rep = f'killpg({self.pgrp}, {signal_name(self.sig)})'
|
|
3995
4004
|
if self.result:
|
|
3996
4005
|
rep += f', {self.result}'
|
|
3997
4006
|
return rep
|
|
@@ -5485,7 +5494,7 @@ def handle_bsdthread_terminate(parser, events):
|
|
|
5485
5494
|
|
|
5486
5495
|
def handle_wait4(parser, events, no_cancel=False):
|
|
5487
5496
|
args = events[0].values
|
|
5488
|
-
return BscWait4(events, args[0], args[1], args[2], args[3], serialize_result(events[-1], 'pid'), no_cancel)
|
|
5497
|
+
return BscWait4(events, pid(args[0]), args[1], args[2], args[3], serialize_result(events[-1], 'pid'), no_cancel)
|
|
5489
5498
|
|
|
5490
5499
|
|
|
5491
5500
|
def handle_recvmsg(parser, events, no_cancel=False):
|
|
@@ -5548,7 +5557,7 @@ def handle_sigreturn(parser, events):
|
|
|
5548
5557
|
|
|
5549
5558
|
|
|
5550
5559
|
def handle_kill(parser, events):
|
|
5551
|
-
return BscKill(events, events[0].values[0], events[0].values[1], serialize_result(events[-1]))
|
|
5560
|
+
return BscKill(events, pid(events[0].values[0]), events[0].values[1], serialize_result(events[-1]))
|
|
5552
5561
|
|
|
5553
5562
|
|
|
5554
5563
|
def handle_getppid(parser, events):
|
|
@@ -5688,7 +5697,7 @@ def handle_getpgrp(parser, events):
|
|
|
5688
5697
|
|
|
5689
5698
|
|
|
5690
5699
|
def handle_setpgid(parser, events):
|
|
5691
|
-
return BscSetpgid(events, events[0].values[0], events[0].values[1], serialize_result(events[-1]))
|
|
5700
|
+
return BscSetpgid(events, pid(events[0].values[0]), pid(events[0].values[1]), serialize_result(events[-1]))
|
|
5692
5701
|
|
|
5693
5702
|
|
|
5694
5703
|
def handle_setitimer(parser, events):
|
|
@@ -5885,7 +5894,7 @@ def handle_gethostuuid(parser, events):
|
|
|
5885
5894
|
|
|
5886
5895
|
|
|
5887
5896
|
def handle_obs_killpg(parser, events):
|
|
5888
|
-
return BscObsKillpg(events, events[0].values[0], events[0].values[1], serialize_result(events[-1]))
|
|
5897
|
+
return BscObsKillpg(events, pid(events[0].values[0]), events[0].values[1], serialize_result(events[-1]))
|
|
5889
5898
|
|
|
5890
5899
|
|
|
5891
5900
|
def handle_setsid(parser, events):
|
|
@@ -5893,7 +5902,7 @@ def handle_setsid(parser, events):
|
|
|
5893
5902
|
|
|
5894
5903
|
|
|
5895
5904
|
def handle_getpgid(parser, events):
|
|
5896
|
-
return BscGetpgid(events, events[0].values[0], serialize_result(events[-1], 'gid'))
|
|
5905
|
+
return BscGetpgid(events, pid(events[0].values[0]), serialize_result(events[-1], 'gid'))
|
|
5897
5906
|
|
|
5898
5907
|
|
|
5899
5908
|
def handle_setprivexec(parser, events):
|
|
@@ -5952,12 +5961,12 @@ def handle_mount(parser, events):
|
|
|
5952
5961
|
|
|
5953
5962
|
def handle_csops(parser, events):
|
|
5954
5963
|
args = events[0].values
|
|
5955
|
-
return BscCsops(events, args[0], parse_enum(CsopsOps, args[1]), args[2], args[3], serialize_result(events[-1]))
|
|
5964
|
+
return BscCsops(events, pid(args[0]), parse_enum(CsopsOps, args[1]), args[2], args[3], serialize_result(events[-1]))
|
|
5956
5965
|
|
|
5957
5966
|
|
|
5958
5967
|
def handle_csops_audittoken(parser, events):
|
|
5959
5968
|
args = events[0].values
|
|
5960
|
-
return BscCsopsAudittoken(events, args[0], parse_enum(CsopsOps, args[1]), args[2], args[3], serialize_result(events[-1]))
|
|
5969
|
+
return BscCsopsAudittoken(events, pid(args[0]), parse_enum(CsopsOps, args[1]), args[2], args[3], serialize_result(events[-1]))
|
|
5961
5970
|
|
|
5962
5971
|
|
|
5963
5972
|
def handle_waitid(parser, events, no_cancel=False):
|
|
@@ -6353,7 +6362,7 @@ def handle_psynch_cvwait(parser, events):
|
|
|
6353
6362
|
|
|
6354
6363
|
def handle_getsid(parser, events):
|
|
6355
6364
|
args = events[0].values
|
|
6356
|
-
return BscGetsid(events, args[0], serialize_result(events[-1], 'sid'))
|
|
6365
|
+
return BscGetsid(events, pid(args[0]), serialize_result(events[-1], 'sid'))
|
|
6357
6366
|
|
|
6358
6367
|
|
|
6359
6368
|
def handle_psynch_cvclrprepost(parser, events):
|
|
@@ -6399,7 +6408,7 @@ def handle_semwait_signal(parser, events, no_cancel=False):
|
|
|
6399
6408
|
|
|
6400
6409
|
def handle_proc_info(parser, events):
|
|
6401
6410
|
args = events[0].values
|
|
6402
|
-
return BscProcInfo(events, parse_enum(ProcInfoCall, args[0]), args[1], args[2], args[3], serialize_result(events[-1]))
|
|
6411
|
+
return BscProcInfo(events, parse_enum(ProcInfoCall, args[0]), pid(args[1]), args[2], args[3], serialize_result(events[-1]))
|
|
6403
6412
|
|
|
6404
6413
|
|
|
6405
6414
|
def handle_sendfile(parser, events):
|
|
@@ -6548,22 +6557,22 @@ def handle_audit_session_port(parser, events):
|
|
|
6548
6557
|
|
|
6549
6558
|
def handle_pid_suspend(parser, events):
|
|
6550
6559
|
args = events[0].values
|
|
6551
|
-
return BscPidSuspend(events, args[0], serialize_result(events[-1]))
|
|
6560
|
+
return BscPidSuspend(events, pid(args[0]), serialize_result(events[-1]))
|
|
6552
6561
|
|
|
6553
6562
|
|
|
6554
6563
|
def handle_pid_resume(parser, events):
|
|
6555
6564
|
args = events[0].values
|
|
6556
|
-
return BscPidResume(events, args[0], serialize_result(events[-1]))
|
|
6565
|
+
return BscPidResume(events, pid(args[0]), serialize_result(events[-1]))
|
|
6557
6566
|
|
|
6558
6567
|
|
|
6559
6568
|
def handle_pid_hibernate(parser, events):
|
|
6560
6569
|
args = events[0].values
|
|
6561
|
-
return BscPidHibernate(events, args[0], serialize_result(events[-1]))
|
|
6570
|
+
return BscPidHibernate(events, pid(args[0]), serialize_result(events[-1]))
|
|
6562
6571
|
|
|
6563
6572
|
|
|
6564
6573
|
def handle_pid_shutdown_sockets(parser, events):
|
|
6565
6574
|
args = events[0].values
|
|
6566
|
-
return BscPidShutdownSockets(events, args[0], args[1], serialize_result(events[-1]))
|
|
6575
|
+
return BscPidShutdownSockets(events, pid(args[0]), args[1], serialize_result(events[-1]))
|
|
6567
6576
|
|
|
6568
6577
|
|
|
6569
6578
|
def handle_shared_region_map_and_slide_np(parser, events):
|
|
@@ -6578,7 +6587,7 @@ def handle_kas_info(parser, events):
|
|
|
6578
6587
|
|
|
6579
6588
|
def handle_memorystatus_control(parser, events):
|
|
6580
6589
|
args = events[0].values
|
|
6581
|
-
return BscMemorystatusControl(events, args[0], args[1], args[2], args[3], serialize_result(events[-1]))
|
|
6590
|
+
return BscMemorystatusControl(events, args[0], pid(args[1]), args[2], args[3], serialize_result(events[-1]))
|
|
6582
6591
|
|
|
6583
6592
|
|
|
6584
6593
|
def handle_guarded_open_np(parser, events):
|
|
@@ -6609,7 +6618,7 @@ def handle_usrctl(parser, events):
|
|
|
6609
6618
|
|
|
6610
6619
|
def handle_proc_rlimit_control(parser, events):
|
|
6611
6620
|
args = events[0].values
|
|
6612
|
-
return BscProcRlimitControl(events, args[0], args[1], args[2], serialize_result(events[-1]))
|
|
6621
|
+
return BscProcRlimitControl(events, pid(args[0]), args[1], args[2], serialize_result(events[-1]))
|
|
6613
6622
|
|
|
6614
6623
|
|
|
6615
6624
|
def handle_connectx(parser, events):
|
|
@@ -6663,7 +6672,7 @@ def handle_sfi_ctl(parser, events):
|
|
|
6663
6672
|
|
|
6664
6673
|
def handle_sfi_pidctl(parser, events):
|
|
6665
6674
|
args = events[0].values
|
|
6666
|
-
return BscSfiPidctl(events, args[0], args[1], args[2], args[3], serialize_result(events[-1]))
|
|
6675
|
+
return BscSfiPidctl(events, args[0], pid(args[1]), args[2], args[3], serialize_result(events[-1]))
|
|
6667
6676
|
|
|
6668
6677
|
|
|
6669
6678
|
def handle_coalition(parser, events):
|
|
@@ -6778,7 +6787,7 @@ def handle_getattrlistat(parser, events):
|
|
|
6778
6787
|
|
|
6779
6788
|
def handle_proc_trace_log(parser, events):
|
|
6780
6789
|
args = events[0].values
|
|
6781
|
-
return BscProcTraceLog(events, args[0], args[1], serialize_result(events[-1]))
|
|
6790
|
+
return BscProcTraceLog(events, pid(args[0]), args[1], serialize_result(events[-1]))
|
|
6782
6791
|
|
|
6783
6792
|
|
|
6784
6793
|
def handle_bsdthread_ctl(parser, events):
|
|
@@ -6979,7 +6988,7 @@ def handle_fs_snapshot(parser, events):
|
|
|
6979
6988
|
|
|
6980
6989
|
def handle_terminate_with_payload(parser, events):
|
|
6981
6990
|
args = events[0].values
|
|
6982
|
-
return BscTerminateWithPayload(events, args[0], args[1], args[2], args[3], serialize_result(events[-1]))
|
|
6991
|
+
return BscTerminateWithPayload(events, pid(args[0]), args[1], args[2], args[3], serialize_result(events[-1]))
|
|
6983
6992
|
|
|
6984
6993
|
|
|
6985
6994
|
def handle_abort_with_payload(parser, events):
|
|
@@ -7065,12 +7074,12 @@ def handle_pivot_root(parser, events):
|
|
|
7065
7074
|
|
|
7066
7075
|
def handle_task_inspect_for_pid(parser, events):
|
|
7067
7076
|
args = events[0].values
|
|
7068
|
-
return BscTaskInspectForPid(events, args[0], args[1], args[2], serialize_result(events[-1]))
|
|
7077
|
+
return BscTaskInspectForPid(events, args[0], pid(args[1]), args[2], serialize_result(events[-1]))
|
|
7069
7078
|
|
|
7070
7079
|
|
|
7071
7080
|
def handle_task_read_for_pid(parser, events):
|
|
7072
7081
|
args = events[0].values
|
|
7073
|
-
return BscTaskReadForPid(events, args[0], args[1], args[2], serialize_result(events[-1]))
|
|
7082
|
+
return BscTaskReadForPid(events, args[0], pid(args[1]), args[2], serialize_result(events[-1]))
|
|
7074
7083
|
|
|
7075
7084
|
|
|
7076
7085
|
def handle_sys_preadv(parser, events, no_cancel=False):
|
|
@@ -7092,7 +7101,7 @@ def handle_ulock_wait2(parser, events):
|
|
|
7092
7101
|
|
|
7093
7102
|
def handle_proc_info_extended_id(parser, events):
|
|
7094
7103
|
args = events[0].values
|
|
7095
|
-
return BscProcInfoExtendedId(events, args[0], args[1], args[2], args[3], serialize_result(events[-1]))
|
|
7104
|
+
return BscProcInfoExtendedId(events, args[0], pid(args[1]), args[2], args[3], serialize_result(events[-1]))
|
|
7096
7105
|
|
|
7097
7106
|
|
|
7098
7107
|
def handle_openat_dprotected_np(parser, events):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pykdebugparser
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.1
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pykdebugparser"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.6.1"
|
|
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
|
|
|
@@ -179,3 +180,67 @@ def test_traces_keep_lost_events_when_filtered():
|
|
|
179
180
|
traces = list(parser.formatted_traces(BytesIO(events_buf)))
|
|
180
181
|
assert len(traces) == 1
|
|
181
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,134 +0,0 @@
|
|
|
1
|
-
"""Syntax highlighting of trace lines, as pygments' C lexer and `stata-dark` true color formatter do it.
|
|
2
|
-
|
|
3
|
-
pygments lexes each line through a general state machine, which formats about 19k lines/s: too slow for a live
|
|
4
|
-
trace. Trace lines use a small part of C's syntax, so `highlight` tokenizes them with one regular expression that
|
|
5
|
-
follows the same rules, and produces the same escape sequences. A line with anything outside that part (a label at its
|
|
6
|
-
start, `;` or `{`, comments, character literals, string escapes, floating-point numbers, ...) goes to pygments, so the
|
|
7
|
-
result is always pygments' own.
|
|
8
|
-
"""
|
|
9
|
-
import re
|
|
10
|
-
from functools import lru_cache
|
|
11
|
-
|
|
12
|
-
from pygments import highlight as pygments_highlight
|
|
13
|
-
from pygments import lexers
|
|
14
|
-
from pygments.formatters import TerminalTrueColorFormatter
|
|
15
|
-
from pygments.token import Keyword, Name, Number, Operator, Punctuation, String, Whitespace
|
|
16
|
-
|
|
17
|
-
c_lexer = lexers.CLexer()
|
|
18
|
-
color_formatter = TerminalTrueColorFormatter(style='stata-dark')
|
|
19
|
-
|
|
20
|
-
# pygments.lexers.c_cpp.CFamilyLexer's 'keywords', 'types' and builtins, the names it does not lex as `Name`.
|
|
21
|
-
_KEYWORDS = {
|
|
22
|
-
'asm', 'auto', 'break', 'const', 'constexpr', 'continue', 'countof', 'default', 'defer', 'do', 'else', 'enum',
|
|
23
|
-
'extern', 'for', 'goto', 'if', 'register', 'restricted', 'return', 'sizeof', 'static', 'switch', 'typedef',
|
|
24
|
-
'typeof', 'typeof_unqual', 'volatile', 'while', 'thread_local', 'alignas', 'alignof', 'static_assert', '_Pragma',
|
|
25
|
-
'fortran',
|
|
26
|
-
}
|
|
27
|
-
_RESERVED = {'inline', '_inline', '__inline', 'naked', 'restrict', 'thread', '__int8', '__int16', '__int32',
|
|
28
|
-
'__int64', '__wchar_t', '__m128i', '__m128d', '__m128', '__m64'} | {
|
|
29
|
-
'__' + name for name in ('asm', 'based', 'except', 'stdcall', 'cdecl', 'fastcall', 'declspec', 'finally', 'try',
|
|
30
|
-
'leave', 'w64', 'unaligned', 'raise', 'noop', 'identifier', 'forceinline', 'assume',
|
|
31
|
-
'null')}
|
|
32
|
-
_TYPES = {'bool', 'int', 'long', 'float', 'short', 'double', 'char', 'unsigned', 'signed', 'void', '_BitInt',
|
|
33
|
-
'__int128'}
|
|
34
|
-
_BUILTINS = {'true', 'false', 'NULL', 'nullptr'}
|
|
35
|
-
# Names that start another lexer state, or a string with an affix (`L"..."`): left to pygments.
|
|
36
|
-
_STATEFUL = {'struct', 'union', 'case', 'L', 'u', 'U', 'u8'}
|
|
37
|
-
|
|
38
|
-
_INT_SUFFIX = r'(?:[uU][lL]{0,2}|[lL]{1,2}[uU]?)?'
|
|
39
|
-
_DEC = r'\d(?:\'?\d)*'
|
|
40
|
-
_HEX = r'[0-9a-fA-F](?:\'?[0-9a-fA-F])*'
|
|
41
|
-
# The alternatives are tried in the lexer's order at each token start. `unsupported` marks what pygments lexes
|
|
42
|
-
# differently from this tokenizer (comments and floating-point numbers); characters no alternative matches (`'`, `\\`,
|
|
43
|
-
# `#`, `$`, a string with an escape, ...) also send the line to pygments.
|
|
44
|
-
_TOKEN = re.compile(
|
|
45
|
-
r'(?P<unsupported>/[/*]'
|
|
46
|
-
r'|0[xX](?:' + _HEX + r'\.' + _HEX + r'|\.' + _HEX + r'|' + _HEX + r')[pP]'
|
|
47
|
-
r'|-?(?:' + _DEC + r'\.' + _DEC + r'|\.' + _DEC + r'|' + _DEC + r')[eE][+-]?' + _DEC +
|
|
48
|
-
r'|-?(?:' + _DEC + r'\.|\.' + _DEC + r'|' + _DEC + r'[fFlL])'
|
|
49
|
-
r')'
|
|
50
|
-
r'|(?P<string>")(?P<content>[^\\"\n]*)(?P<close>")'
|
|
51
|
-
r'|(?P<hex>-?0[xX]' + _HEX + _INT_SUFFIX + r')'
|
|
52
|
-
r'|(?P<bin>-?0[bB][01](?:\'?[01])*' + _INT_SUFFIX + r')'
|
|
53
|
-
r'|(?P<oct>-?0(?:\'?[0-7])+' + _INT_SUFFIX + r')'
|
|
54
|
-
r'|(?P<int>-?' + _DEC + _INT_SUFFIX + r')'
|
|
55
|
-
r'|(?P<op>[~!%^&*+=|?:<>/-])'
|
|
56
|
-
r'|(?P<punct>[()\[\],.])'
|
|
57
|
-
r'|(?P<name>[a-zA-Z_]\w*)'
|
|
58
|
-
r'|(?P<ws>[^\S\n]+)'
|
|
59
|
-
)
|
|
60
|
-
# Whole-line rules of the lexer: a label at the start of a line, and functions (their rules need a `;` or `{`).
|
|
61
|
-
_UNSUPPORTED = re.compile(r'^\s*[a-zA-Z_$]\w*\s*:(?!:)|[;{]')
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _style(ttype):
|
|
65
|
-
# The formatter uses the style of the token type, or of its closest parent that has one.
|
|
66
|
-
while str(ttype) not in color_formatter.style_string and ttype.parent is not None:
|
|
67
|
-
ttype = ttype.parent
|
|
68
|
-
return color_formatter.style_string.get(str(ttype), ('', ''))
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
_GROUPS = {'hex': _style(Number.Hex), 'bin': _style(Number.Bin), 'oct': _style(Number.Oct),
|
|
72
|
-
'int': _style(Number.Integer), 'op': _style(Operator), 'punct': _style(Punctuation)}
|
|
73
|
-
_NAME_STYLES = {}
|
|
74
|
-
for _names, _ttype in ((_KEYWORDS, Keyword), (_RESERVED, Keyword.Reserved), (_TYPES, Keyword.Type),
|
|
75
|
-
(_BUILTINS, Name.Builtin)):
|
|
76
|
-
for _name in _names:
|
|
77
|
-
_NAME_STYLES[_name] = _style(_ttype)
|
|
78
|
-
_NAME = _style(Name)
|
|
79
|
-
_STRING = _style(String)
|
|
80
|
-
# Whitespace only gets a foreground color, which a space does not show: it can stay in the style before it.
|
|
81
|
-
_FG_RESET = '\x1b[39m'
|
|
82
|
-
assert _style(Whitespace)[1] == _FG_RESET
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _pygments(text: str) -> str:
|
|
86
|
-
return pygments_highlight(text, c_lexer, color_formatter).strip()
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
# Traces repeat (the same calls with the same arguments): in a busy `--bsc` trace, 77% of the lines were among the
|
|
90
|
-
# 16k most recent ones.
|
|
91
|
-
@lru_cache(maxsize=1 << 14)
|
|
92
|
-
def highlight(text: str) -> str:
|
|
93
|
-
"""`text` colored as `pygments.highlight(text, CLexer(), TerminalTrueColorFormatter('stata-dark'))` shows it.
|
|
94
|
-
|
|
95
|
-
pygments starts and ends the escape sequence of every token, even between tokens of the same color, and colors
|
|
96
|
-
spaces. Here a sequence starts only where the color changes, which renders the same in about half the bytes (the
|
|
97
|
-
terminal is the bottleneck of a busy trace).
|
|
98
|
-
"""
|
|
99
|
-
if _UNSUPPORTED.search(text):
|
|
100
|
-
return _pygments(text)
|
|
101
|
-
out = []
|
|
102
|
-
style = None # the (on, off) in effect
|
|
103
|
-
pos = 0
|
|
104
|
-
for match in _TOKEN.finditer(text):
|
|
105
|
-
if match.start() != pos:
|
|
106
|
-
return _pygments(text)
|
|
107
|
-
pos = match.end()
|
|
108
|
-
kind = match.lastgroup
|
|
109
|
-
if kind == 'ws':
|
|
110
|
-
out.append(match.group())
|
|
111
|
-
continue
|
|
112
|
-
if kind == 'unsupported':
|
|
113
|
-
return _pygments(text)
|
|
114
|
-
if kind == 'close':
|
|
115
|
-
token_style = _STRING
|
|
116
|
-
elif kind == 'name':
|
|
117
|
-
name = match.group()
|
|
118
|
-
if name in _STATEFUL:
|
|
119
|
-
return _pygments(text)
|
|
120
|
-
token_style = _NAME_STYLES.get(name, _NAME)
|
|
121
|
-
else:
|
|
122
|
-
token_style = _GROUPS[kind]
|
|
123
|
-
if style is None or token_style[0] != style[0]:
|
|
124
|
-
# Ending a foreground color is not needed when the next one replaces it.
|
|
125
|
-
if style is not None and style[1] != _FG_RESET:
|
|
126
|
-
out.append(style[1])
|
|
127
|
-
out.append(token_style[0])
|
|
128
|
-
style = token_style
|
|
129
|
-
out.append(match.group())
|
|
130
|
-
if pos != len(text):
|
|
131
|
-
return _pygments(text)
|
|
132
|
-
if style is not None:
|
|
133
|
-
out.append(style[1])
|
|
134
|
-
return ''.join(out).strip()
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
from pygments import highlight as pygments_highlight
|
|
5
|
-
|
|
6
|
-
from pykdebugparser import highlight
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@pytest.mark.parametrize('line', [
|
|
10
|
-
# The tokenizer's own lines.
|
|
11
|
-
'read(7, 0x11bf1c000, 25558), count: 25558',
|
|
12
|
-
'open("/private/var/x", O_RDONLY | O_CLOEXEC), fd: 3',
|
|
13
|
-
'open_nocancel("/System/Library/Foundation.framework//en.lproj", O_RDONLY), errno: ENOENT(2)',
|
|
14
|
-
'lookup(".tmp.OgDV0NXsrZ"), vnode id: 8638794530369352533',
|
|
15
|
-
'workq_kernreturn(256, 0x16d2c2b80, 1, 0), return: 0',
|
|
16
|
-
'sigreturn(0x16bd56ec8, 30, 0x17fcd6aa5d512e6c), errno: EJUSTRETURN(-2)',
|
|
17
|
-
'pread(12, 0xad80793d2, 4096, 0x31e000), count: 4096',
|
|
18
|
-
'getxattr("", NULL, 0x0, 0, int, true), size: 0x10',
|
|
19
|
-
'thread_suspend, tid: 55996, user_stop_count: 0, pidsuspended: True',
|
|
20
|
-
'mmap(0x0, 0755, 0b101, 10u, 3LL), count: 0x8e0000',
|
|
21
|
-
'DBG_DYLD_TIMING_OBJC_MAP',
|
|
22
|
-
# Lines left to pygments.
|
|
23
|
-
"ioctl(8, 0xc020699d /* _IOC(IOC_IN | IOC_OUT, 'i', 157, 32) */, 0x16d3cbea0)",
|
|
24
|
-
'TRACE_LOST_EVENTS: the kernel dropped trace events here',
|
|
25
|
-
'stat64("/private/var/Snapshots/com.apple.AppStore - {DEFAULT GROUP}", 0x16db399d0)',
|
|
26
|
-
'open("a\\"b"), fd: 3',
|
|
27
|
-
'struct x, case 1, L"wide", u8"x"',
|
|
28
|
-
'duration: 1.5, 2e10, .5, 3f, 7l',
|
|
29
|
-
'mixed $dollar # hash',
|
|
30
|
-
])
|
|
31
|
-
def test_highlight_renders_like_pygments(line):
|
|
32
|
-
assert _render(highlight.highlight(line)) == _render(
|
|
33
|
-
pygments_highlight(line, highlight.c_lexer, highlight.color_formatter).strip())
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_highlight_uses_fewer_escape_sequences():
|
|
37
|
-
line = 'read(7, 0x11bf1c000, 25558), count: 25558'
|
|
38
|
-
assert len(highlight.highlight(line)) < len(pygments_highlight(line, highlight.c_lexer,
|
|
39
|
-
highlight.color_formatter).strip()) / 2
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
_SGR = re.compile(r'\x1b\[([0-9;]*)m')
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _render(text):
|
|
46
|
-
"""Each character with the terminal attributes it shows (a space shows only its background)."""
|
|
47
|
-
fg = bg = None
|
|
48
|
-
bold = italic = False
|
|
49
|
-
cells = []
|
|
50
|
-
pos = 0
|
|
51
|
-
for match in _SGR.finditer(text):
|
|
52
|
-
cells += [(char, fg, bg, bold, italic) for char in text[pos:match.start()]]
|
|
53
|
-
pos = match.end()
|
|
54
|
-
codes = [int(code) for code in match.group(1).split(';') if code] or [0]
|
|
55
|
-
i = 0
|
|
56
|
-
while i < len(codes):
|
|
57
|
-
code = codes[i]
|
|
58
|
-
if code == 0:
|
|
59
|
-
fg = bg = None
|
|
60
|
-
bold = italic = False
|
|
61
|
-
elif code == 1:
|
|
62
|
-
bold = True
|
|
63
|
-
elif code == 3:
|
|
64
|
-
italic = True
|
|
65
|
-
elif code == 39:
|
|
66
|
-
fg = None
|
|
67
|
-
elif code == 49:
|
|
68
|
-
bg = None
|
|
69
|
-
elif code in (38, 48) and codes[i + 1] == 2:
|
|
70
|
-
if code == 38:
|
|
71
|
-
fg = tuple(codes[i + 2:i + 5])
|
|
72
|
-
else:
|
|
73
|
-
bg = tuple(codes[i + 2:i + 5])
|
|
74
|
-
i += 4
|
|
75
|
-
else:
|
|
76
|
-
raise ValueError(f'unexpected SGR code {code}')
|
|
77
|
-
i += 1
|
|
78
|
-
cells += [(char, fg, bg, bold, italic) for char in text[pos:]]
|
|
79
|
-
return [(char, None, bg, False, False) if char == ' ' else (char, fg, bg, bold, italic)
|
|
80
|
-
for char, fg, bg, bold, italic in cells]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|