amd-debug-tools 0.2.0__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.

Potentially problematic release.


This version of amd-debug-tools might be problematic. Click here for more details.

amd_debug/kernel.py ADDED
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: MIT
3
+ """Kernel log analysis"""
4
+
5
+ import logging
6
+ import re
7
+ import os
8
+ import subprocess
9
+ from datetime import timedelta
10
+
11
+ from amd_debug.common import systemd_in_use, read_file, fatal_error
12
+
13
+
14
+ def get_kernel_command_line() -> str:
15
+ """Get the kernel command line"""
16
+ cmdline = read_file(os.path.join("/proc", "cmdline"))
17
+ # borrowed from https://github.com/fwupd/fwupd/blob/1.9.5/libfwupdplugin/fu-common-linux.c#L95
18
+ filtered = [
19
+ "apparmor",
20
+ "audit",
21
+ "auto",
22
+ "boot",
23
+ "BOOT_IMAGE",
24
+ "console",
25
+ "crashkernel",
26
+ "cryptdevice",
27
+ "cryptkey",
28
+ "dm",
29
+ "earlycon",
30
+ "earlyprintk",
31
+ "ether",
32
+ "initrd",
33
+ "ip",
34
+ "LANG",
35
+ "loglevel",
36
+ "luks.key",
37
+ "luks.name",
38
+ "luks.options",
39
+ "luks.uuid",
40
+ "mitigations",
41
+ "mount.usr",
42
+ "mount.usrflags",
43
+ "mount.usrfstype",
44
+ "netdev",
45
+ "netroot",
46
+ "nfsaddrs",
47
+ "nfs.nfs4_unique_id",
48
+ "nfsroot",
49
+ "noplymouth",
50
+ "ostree",
51
+ "quiet",
52
+ "rd.dm.uuid",
53
+ "rd.luks.allow-discards",
54
+ "rd.luks.key",
55
+ "rd.luks.name",
56
+ "rd.luks.options",
57
+ "rd.luks.uuid",
58
+ "rd.lvm.lv",
59
+ "rd.lvm.vg",
60
+ "rd.md.uuid",
61
+ "rd.systemd.mask",
62
+ "rd.systemd.wants",
63
+ "resume",
64
+ "resumeflags",
65
+ "rhgb",
66
+ "ro",
67
+ "root",
68
+ "rootflags",
69
+ "roothash",
70
+ "rw",
71
+ "security",
72
+ "showopts",
73
+ "splash",
74
+ "swap",
75
+ "systemd.mask",
76
+ "systemd.show_status",
77
+ "systemd.unit",
78
+ "systemd.verity_root_data",
79
+ "systemd.verity_root_hash",
80
+ "systemd.wants",
81
+ "udev.log_priority",
82
+ "verbose",
83
+ "vt.handoff",
84
+ "zfs",
85
+ ]
86
+ # remove anything that starts with something in filtered from cmdline
87
+ return " ".join([x for x in cmdline.split() if not x.startswith(tuple(filtered))])
88
+
89
+
90
+ def sscanf_bios_args(line):
91
+ """Extracts the format string and arguments from a BIOS trace line"""
92
+ if re.search(r"ex_trace_point", line):
93
+ return True
94
+ elif re.search(r"ex_trace_args", line):
95
+ parts = line.split(": ", 1)
96
+ if len(parts) < 2:
97
+ return None
98
+
99
+ t = parts[1].strip()
100
+ match = re.match(r'"(.*?)"(,.*)', t)
101
+ if match:
102
+ format_string = match.group(1).strip().replace("\\n", "")
103
+ args_part = match.group(2).strip(", ")
104
+ arguments = [arg.strip() for arg in args_part.split(",")]
105
+
106
+ format_specifiers = re.findall(r"%([xXdD])", format_string)
107
+
108
+ converted_args = []
109
+ arg_index = 0
110
+ for specifier in format_specifiers:
111
+ if arg_index < len(arguments):
112
+ value = arguments[arg_index]
113
+ if value == "Unknown":
114
+ converted_args.append(-1)
115
+ elif specifier.lower() == "x":
116
+ try:
117
+ converted_args.append(int(value, 16))
118
+ except ValueError:
119
+ return None
120
+ else: # Decimal conversion
121
+ try:
122
+ converted_args.append(int(value))
123
+ except ValueError:
124
+ try:
125
+ converted_args.append(int(value, 16))
126
+ except ValueError:
127
+ return None
128
+ arg_index += 1
129
+ else:
130
+ break
131
+
132
+ try:
133
+ return format_string % tuple(converted_args)
134
+ except TypeError:
135
+ return None
136
+ else:
137
+ # If no format string is found, assume no format modifiers and return True
138
+ return True
139
+ # evmisc-0132 ev_queue_notify_reques: Dispatching Notify on [UBTC] (Device) Value 0x80 (Status Change) Node 00000000851b15c1
140
+ elif re.search(r"ev_queue_notify_reques", line):
141
+ parts = line.split(": ", 1)
142
+ if len(parts) < 2:
143
+ return None
144
+ return parts[1].split("Node")[0].strip()
145
+
146
+ return None
147
+
148
+
149
+ class KernelLogger:
150
+ """Base class for kernel loggers"""
151
+
152
+ def __init__(self):
153
+ pass
154
+
155
+ def seek(self):
156
+ """Seek to the beginning of the log"""
157
+
158
+ def seek_tail(self, tim=None):
159
+ """Seek to the end of the log"""
160
+
161
+ def process_callback(self, callback, priority):
162
+ """Process the log"""
163
+
164
+ def match_line(self, _matches) -> str:
165
+ """Find lines that match all matches"""
166
+ return ""
167
+
168
+ def match_pattern(self, _pattern) -> str:
169
+ """Find lines that match a pattern"""
170
+ return ""
171
+
172
+
173
+ class InputFile(KernelLogger):
174
+ """Class for input file parsing"""
175
+
176
+ def __init__(self, fname):
177
+ self.since_support = False
178
+ self.buffer = None
179
+ self.seeked = False
180
+ self.buffer = read_file(fname)
181
+
182
+ def process_callback(self, callback, priority=None):
183
+ """Process the log"""
184
+ for entry in self.buffer.split("\n"):
185
+ callback(entry, priority)
186
+
187
+
188
+ class DmesgLogger(KernelLogger):
189
+ """Class for dmesg logging"""
190
+
191
+ def __init__(self):
192
+ self.since_support = False
193
+ self.buffer = None
194
+ self.seeked = False
195
+
196
+ cmd = ["dmesg", "-h"]
197
+ result = subprocess.run(cmd, check=True, capture_output=True)
198
+ for line in result.stdout.decode("utf-8").split("\n"):
199
+ if "--since" in line:
200
+ self.since_support = True
201
+ logging.debug("dmesg since support: %d", self.since_support)
202
+
203
+ self.command = ["dmesg", "-t", "-k"]
204
+ self._refresh_head()
205
+
206
+ def _refresh_head(self):
207
+ self.buffer = []
208
+ self.seeked = False
209
+ result = subprocess.run(self.command, check=True, capture_output=True)
210
+ if result.returncode == 0:
211
+ self.buffer = result.stdout.decode("utf-8")
212
+
213
+ def seek(self):
214
+ """Seek to the beginning of the log"""
215
+ if self.seeked:
216
+ self._refresh_head()
217
+
218
+ def seek_tail(self, tim=None):
219
+ """Seek to the end of the log"""
220
+ if tim:
221
+ if self.since_support:
222
+ # look 10 seconds back because dmesg time isn't always accurate
223
+ fuzz = tim - timedelta(seconds=10)
224
+ cmd = self.command + [
225
+ "--time-format=iso",
226
+ f"--since={fuzz.strftime('%Y-%m-%dT%H:%M:%S')}",
227
+ ]
228
+ else:
229
+ cmd = self.command
230
+ result = subprocess.run(cmd, check=True, capture_output=True)
231
+ if result.returncode == 0:
232
+ self.buffer = result.stdout.decode("utf-8")
233
+ if self.since_support:
234
+ self.seeked = True
235
+
236
+ def process_callback(self, callback, _priority=None):
237
+ """Process the log"""
238
+ for entry in self.buffer.split("\n"):
239
+ callback(entry, _priority)
240
+
241
+ def match_line(self, matches):
242
+ """Find lines that match all matches"""
243
+ for entry in self.buffer.split("\n"):
244
+ for match in matches:
245
+ if match not in entry:
246
+ break
247
+ return entry
248
+ return ""
249
+
250
+ def match_pattern(self, pattern) -> str:
251
+ for entry in self.buffer.split("\n"):
252
+ if re.search(pattern, entry):
253
+ return entry
254
+ return ""
255
+
256
+ def capture_header(self):
257
+ """Capture the header of the log"""
258
+ return self.buffer.split("\n")[0]
259
+
260
+
261
+ class CySystemdLogger(KernelLogger):
262
+ """Class for logging using systemd journal using cython"""
263
+
264
+ def __init__(self):
265
+ from cysystemd.reader import JournalReader, JournalOpenMode, Rule
266
+
267
+ boot_reader = JournalReader()
268
+ boot_reader.open(JournalOpenMode.SYSTEM)
269
+ boot_reader.seek_tail()
270
+ boot_reader.skip_previous(1)
271
+
272
+ current_boot_id = None
273
+ for entry in boot_reader:
274
+ if hasattr(entry, "data") and "_BOOT_ID" in entry.data:
275
+ current_boot_id = entry.data["_BOOT_ID"]
276
+ break
277
+ if not current_boot_id:
278
+ raise RuntimeError("Unable to find current boot ID")
279
+
280
+ rules = Rule("_BOOT_ID", current_boot_id) & Rule("_TRANSPORT", "kernel")
281
+
282
+ self.journal = JournalReader()
283
+ self.journal.open(JournalOpenMode.SYSTEM)
284
+ self.journal.add_filter(rules)
285
+
286
+ def seek(self):
287
+ """Seek to the beginning of the log"""
288
+ self.journal.seek_head()
289
+
290
+ def seek_tail(self, tim=None):
291
+ """Seek to the end of the log"""
292
+ if tim:
293
+ timestamp_usec = int(tim.timestamp() * 1_000_000)
294
+ self.journal.seek_realtime_usec(timestamp_usec)
295
+ else:
296
+ self.journal.seek_tail()
297
+
298
+ def process_callback(self, callback, _priority=None):
299
+ """Process the log"""
300
+ for entry in self.journal:
301
+ callback(entry["MESSAGE"], entry["PRIORITY"])
302
+
303
+ def match_line(self, matches):
304
+ """Find lines that match all matches"""
305
+ for entry in self.journal:
306
+ for match in matches:
307
+ if match not in entry["MESSAGE"]:
308
+ break
309
+ return entry["MESSAGE"]
310
+ return None
311
+
312
+ def match_pattern(self, pattern):
313
+ """Find lines that match a pattern"""
314
+ for entry in self.journal:
315
+ if re.search(pattern, entry["MESSAGE"]):
316
+ return entry["MESSAGE"]
317
+ return None
318
+
319
+
320
+ class SystemdLogger(KernelLogger):
321
+ """Class for logging using systemd journal"""
322
+
323
+ def __init__(self):
324
+ from systemd import journal # pylint: disable=import-outside-toplevel
325
+
326
+ self.journal = journal.Reader()
327
+ self.journal.this_boot()
328
+ self.journal.log_level(journal.LOG_INFO)
329
+ self.journal.add_match(_TRANSPORT="kernel")
330
+ self.journal.add_match(PRIORITY=journal.LOG_DEBUG)
331
+
332
+ def seek(self):
333
+ """Seek to the beginning of the log"""
334
+ self.journal.seek_head()
335
+
336
+ def seek_tail(self, tim=None):
337
+ if tim:
338
+ self.journal.seek_realtime(tim)
339
+ else:
340
+ self.journal.seek_tail()
341
+
342
+ def process_callback(self, callback, _priority=None):
343
+ """Process the log"""
344
+ for entry in self.journal:
345
+ callback(entry["MESSAGE"], entry["PRIORITY"])
346
+
347
+ def match_line(self, matches):
348
+ """Find lines that match all matches"""
349
+ for entry in self.journal:
350
+ for match in matches:
351
+ if match not in entry["MESSAGE"]:
352
+ break
353
+ return entry["MESSAGE"]
354
+ return ""
355
+
356
+ def match_pattern(self, pattern):
357
+ """Find lines that match a pattern"""
358
+ for entry in self.journal:
359
+ if re.search(pattern, entry["MESSAGE"]):
360
+ return entry["MESSAGE"]
361
+ return ""
362
+
363
+
364
+ def get_kernel_log(input_file=None) -> KernelLogger:
365
+ """Get the kernel log provider"""
366
+ kernel_log = None
367
+ if input_file:
368
+ kernel_log = InputFile(input_file)
369
+ elif systemd_in_use():
370
+ try:
371
+ kernel_log = CySystemdLogger()
372
+ except ImportError:
373
+ kernel_log = None
374
+ except RuntimeError as e:
375
+ logging.debug(e)
376
+ kernel_log = None
377
+ if not kernel_log:
378
+ try:
379
+ kernel_log = SystemdLogger()
380
+ except ModuleNotFoundError:
381
+ pass
382
+ if not kernel_log:
383
+ try:
384
+ kernel_log = DmesgLogger()
385
+ except subprocess.CalledProcessError as e:
386
+ fatal_error(f"{e}")
387
+ kernel_log = None
388
+ logging.debug("Kernel log provider: %s", kernel_log.__class__.__name__)
389
+ return kernel_log