pykdebugparser 1.5.0__tar.gz → 1.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/PKG-INFO +1 -2
  2. pykdebugparser-1.6.0/pykdebugparser/highlight.py +66 -0
  3. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/pykdebugparser.py +64 -3
  4. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/PKG-INFO +1 -2
  5. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/requires.txt +0 -1
  6. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pyproject.toml +1 -2
  7. pykdebugparser-1.6.0/tests/test_highlight.py +52 -0
  8. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/tests/test_pykdebugparser.py +65 -0
  9. pykdebugparser-1.5.0/pykdebugparser/highlight.py +0 -134
  10. pykdebugparser-1.5.0/tests/test_highlight.py +0 -80
  11. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/LICENSE +0 -0
  12. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/README.md +0 -0
  13. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/__init__.py +0 -0
  14. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/__main__.py +0 -0
  15. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/callstacks_parser.py +0 -0
  16. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/kd_buf_parser.py +0 -0
  17. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/kevent.py +0 -0
  18. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/os_log_event.py +0 -0
  19. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/trace.codes +0 -0
  20. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_codes.py +0 -0
  21. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/__init__.py +0 -0
  22. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/bsd.py +0 -0
  23. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/dyld.py +0 -0
  24. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/fsystem.py +0 -0
  25. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/mach.py +0 -0
  26. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/perf.py +0 -0
  27. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/trace.py +0 -0
  28. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/trace_handlers/turnstile.py +0 -0
  29. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser/traces_parser.py +0 -0
  30. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/SOURCES.txt +0 -0
  31. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/dependency_links.txt +0 -0
  32. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/entry_points.txt +0 -0
  33. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/pykdebugparser.egg-info/top_level.txt +0 -0
  34. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/setup.cfg +0 -0
  35. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/tests/test_kd_buf_parser.py +0 -0
  36. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/tests/test_kevent.py +0 -0
  37. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/tests/test_os_log_event.py +0 -0
  38. {pykdebugparser-1.5.0 → pykdebugparser-1.6.0}/tests/test_trace_codes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pykdebugparser
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Python parser for kdebug events
5
5
  Author: Matan Perelman
6
6
  Author-email: matan1008@gmail.com
@@ -39,7 +39,6 @@ Requires-Python: >=3.9
39
39
  Description-Content-Type: text/markdown
40
40
  License-File: LICENSE
41
41
  Requires-Dist: bytestruct>=0.1.0
42
- Requires-Dist: pygments
43
42
  Requires-Dist: click
44
43
  Requires-Dist: termcolor
45
44
  Provides-Extra: testing
@@ -0,0 +1,66 @@
1
+ """Syntax highlighting of trace lines, such as `open("/etc/hosts", O_RDONLY), fd: 3`.
2
+
3
+ Lines are colored with the terminal's 16 basic colors, so they follow its theme:
4
+
5
+ - the call or event name at the start, bold;
6
+ - strings (paths, symbols, ...), green;
7
+ - numbers, cyan;
8
+ - constants (flags, enums, signals: `O_RDONLY`, `KERN_SUCCESS`, `SIGKILL`), yellow;
9
+ - an error (`errno: ENOENT(2)`, a `KERN_*` failure), red;
10
+ - the labels of results and fields (`fd:`, `count:`), dim.
11
+
12
+ A trace is mostly the same calls with the same arguments, so the most recent lines are cached.
13
+ """
14
+ import re
15
+ from functools import lru_cache
16
+
17
+ _RESET = '\x1b[0m'
18
+ _BOLD = '\x1b[1m'
19
+ _DIM = '\x1b[2m'
20
+ _RED = '\x1b[31m'
21
+ _GREEN = '\x1b[32m'
22
+ _YELLOW = '\x1b[33m'
23
+ _CYAN = '\x1b[36m'
24
+
25
+ _TOKEN = re.compile(
26
+ r'(?P<string>"[^"]*")'
27
+ r'|(?P<number>-?0[xX][0-9a-fA-F]+|-?\d+)'
28
+ r'|(?P<label>[A-Za-z_][\w ]*?:)(?= |$)'
29
+ r'|(?P<name>[A-Za-z_]\w*)'
30
+ )
31
+ # A constant is all capitals, digits and underscores, with at least one capital.
32
+ _CONSTANT = re.compile(r'[A-Z][A-Z0-9_]*')
33
+ # `KERN_SUCCESS` is a result, not an error.
34
+ _SUCCESS = {'KERN_SUCCESS'}
35
+
36
+
37
+ @lru_cache(maxsize=1 << 14)
38
+ def highlight(text: str) -> str:
39
+ """`text` with terminal colors (SGR escape sequences); the text itself is unchanged."""
40
+ out = []
41
+ pos = 0
42
+ error = False # inside the value of an `errno:` result
43
+ first = True
44
+ for match in _TOKEN.finditer(text):
45
+ if match.start() > pos:
46
+ out.append(text[pos:match.start()])
47
+ pos = match.end()
48
+ kind = match.lastgroup
49
+ value = match.group()
50
+ if kind == 'string':
51
+ out.append(_GREEN + value + _RESET)
52
+ elif kind == 'number':
53
+ out.append((_RED if error else _CYAN) + value + _RESET)
54
+ elif kind == 'label':
55
+ error = value == 'errno:'
56
+ out.append((_RED if error else _DIM) + value + _RESET)
57
+ elif first:
58
+ out.append(_BOLD + value + _RESET)
59
+ elif _CONSTANT.fullmatch(value):
60
+ failed = error or (value.startswith('KERN_') and value not in _SUCCESS)
61
+ out.append((_RED if failed else _YELLOW) + value + _RESET)
62
+ else:
63
+ out.append(value)
64
+ first = False
65
+ out.append(text[pos:])
66
+ return ''.join(out)
@@ -1,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 _format_timestamp(self, timestamp):
186
+ def _posix_time(self, timestamp):
187
+ """A kevent timestamp (mach ticks) as POSIX time, or None without the device's time information."""
132
188
  if None in (self.mach_absolute_time, self.numer, self.denom, self.usecs_since_epoch, self.timezone):
133
- return str(timestamp) + ' '
189
+ return None
134
190
  offset_usec = (
135
191
  ((timestamp - self.mach_absolute_time) * self.numer) / (self.denom * 1000)
136
192
  )
137
- posix_time = (self.usecs_since_epoch + offset_usec) / 1000000
193
+ return (self.usecs_since_epoch + offset_usec) / 1000000
194
+
195
+ def _format_timestamp(self, timestamp):
196
+ posix_time = self._posix_time(timestamp)
197
+ if posix_time is None:
198
+ return str(timestamp) + ' '
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pykdebugparser
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Python parser for kdebug events
5
5
  Author: Matan Perelman
6
6
  Author-email: matan1008@gmail.com
@@ -39,7 +39,6 @@ Requires-Python: >=3.9
39
39
  Description-Content-Type: text/markdown
40
40
  License-File: LICENSE
41
41
  Requires-Dist: bytestruct>=0.1.0
42
- Requires-Dist: pygments
43
42
  Requires-Dist: click
44
43
  Requires-Dist: termcolor
45
44
  Provides-Extra: testing
@@ -1,5 +1,4 @@
1
1
  bytestruct>=0.1.0
2
- pygments
3
2
  click
4
3
  termcolor
5
4
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pykdebugparser"
7
- version = "1.5.0"
7
+ version = "1.6.0"
8
8
  description = "Python parser for kdebug events"
9
9
  readme.file = "README.md"
10
10
  readme.content-type = "text/markdown"
@@ -25,7 +25,6 @@ classifiers = [
25
25
  ]
26
26
  dependencies = [
27
27
  "bytestruct>=0.1.0",
28
- "pygments",
29
28
  "click",
30
29
  "termcolor",
31
30
  ]
@@ -0,0 +1,52 @@
1
+ import re
2
+
3
+ import pytest
4
+
5
+ from pykdebugparser.highlight import highlight
6
+
7
+ _SGR = re.compile(r'\x1b\[[0-9;]*m')
8
+
9
+ BOLD, DIM, RESET = '\x1b[1m', '\x1b[2m', '\x1b[0m'
10
+ RED, GREEN, YELLOW, CYAN = '\x1b[31m', '\x1b[32m', '\x1b[33m', '\x1b[36m'
11
+
12
+
13
+ def _colored(color, text):
14
+ return color + text + RESET
15
+
16
+
17
+ @pytest.mark.parametrize('line, expected', [
18
+ ('open("/etc/hosts", O_RDONLY | O_CLOEXEC), fd: 3',
19
+ f'{_colored(BOLD, "open")}({_colored(GREEN, chr(34) + "/etc/hosts" + chr(34))}, {_colored(YELLOW, "O_RDONLY")} | '
20
+ f'{_colored(YELLOW, "O_CLOEXEC")}), {_colored(DIM, "fd:")} {_colored(CYAN, "3")}'),
21
+ ('stat64("/x"), errno: ENOENT(2)',
22
+ f'{_colored(BOLD, "stat64")}({_colored(GREEN, chr(34) + "/x" + chr(34))}), {_colored(RED, "errno:")} '
23
+ f'{_colored(RED, "ENOENT")}({_colored(RED, "2")})'),
24
+ ('task_for_pid(515, 97997, 0x16bd572fc), result: KERN_SUCCESS',
25
+ f'{_colored(BOLD, "task_for_pid")}({_colored(CYAN, "515")}, {_colored(CYAN, "97997")}, '
26
+ f'{_colored(CYAN, "0x16bd572fc")}), {_colored(DIM, "result:")} {_colored(YELLOW, "KERN_SUCCESS")}'),
27
+ ('mach_port_mod_refs(1, 0x0, MACH_PORT_RIGHT_SEND, 1), result: KERN_INVALID_RIGHT',
28
+ f'{_colored(BOLD, "mach_port_mod_refs")}({_colored(CYAN, "1")}, {_colored(CYAN, "0x0")}, '
29
+ f'{_colored(YELLOW, "MACH_PORT_RIGHT_SEND")}, {_colored(CYAN, "1")}), {_colored(DIM, "result:")} '
30
+ f'{_colored(RED, "KERN_INVALID_RIGHT")}'),
31
+ ('lookup("private/var/db"), vnode id: 42',
32
+ f'{_colored(BOLD, "lookup")}({_colored(GREEN, chr(34) + "private/var/db" + chr(34))}), '
33
+ f'{_colored(DIM, "vnode id:")} {_colored(CYAN, "42")}'),
34
+ ('sigreturn(0x1, 30, 0x2), errno: EJUSTRETURN(-2)',
35
+ f'{_colored(BOLD, "sigreturn")}({_colored(CYAN, "0x1")}, {_colored(CYAN, "30")}, {_colored(CYAN, "0x2")}), '
36
+ f'{_colored(RED, "errno:")} {_colored(RED, "EJUSTRETURN")}({_colored(RED, "-2")})'),
37
+ ('DBG_DYLD_TIMING_OBJC_MAP', _colored(BOLD, 'DBG_DYLD_TIMING_OBJC_MAP')),
38
+ ])
39
+ def test_highlight(line, expected):
40
+ assert highlight(line) == expected
41
+
42
+
43
+ @pytest.mark.parametrize('line', [
44
+ 'open("/etc/hosts", O_RDONLY), fd: 3',
45
+ "ioctl(8, 0xc020699d /* _IOC(IOC_IN | IOC_OUT, 'i', 157, 32) */, 0x16d3cbea0)",
46
+ 'TRACE_LOST_EVENTS: the kernel dropped trace events here',
47
+ 'stat64("/a b/{c}: d", 0x1), count: 0x10',
48
+ 'unterminated "string',
49
+ '',
50
+ ])
51
+ def test_highlight_keeps_the_text(line):
52
+ assert _SGR.sub('', highlight(line)) == line
@@ -1,4 +1,5 @@
1
1
  from datetime import datetime, timedelta, timezone
2
+ import json
2
3
  from io import BytesIO
3
4
  import struct
4
5
 
@@ -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