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/__init__.py +45 -0
- amd_debug/acpi.py +107 -0
- amd_debug/bash/amd-s2idle +89 -0
- amd_debug/battery.py +87 -0
- amd_debug/bios.py +138 -0
- amd_debug/common.py +324 -0
- amd_debug/database.py +331 -0
- amd_debug/failures.py +588 -0
- amd_debug/installer.py +404 -0
- amd_debug/kernel.py +389 -0
- amd_debug/prerequisites.py +1215 -0
- amd_debug/pstate.py +314 -0
- amd_debug/s2idle-hook +72 -0
- amd_debug/s2idle.py +406 -0
- amd_debug/sleep_report.py +453 -0
- amd_debug/templates/html +427 -0
- amd_debug/templates/md +39 -0
- amd_debug/templates/stdout +13 -0
- amd_debug/templates/txt +23 -0
- amd_debug/validator.py +863 -0
- amd_debug/wake.py +111 -0
- amd_debug_tools-0.2.0.dist-info/METADATA +180 -0
- amd_debug_tools-0.2.0.dist-info/RECORD +27 -0
- amd_debug_tools-0.2.0.dist-info/WHEEL +5 -0
- amd_debug_tools-0.2.0.dist-info/entry_points.txt +4 -0
- amd_debug_tools-0.2.0.dist-info/licenses/LICENSE +19 -0
- amd_debug_tools-0.2.0.dist-info/top_level.txt +1 -0
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
|