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.

@@ -0,0 +1,1215 @@
1
+ #!/usr/bin/python3
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ import logging
5
+ import os
6
+ import platform
7
+ import re
8
+ import shutil
9
+ import subprocess
10
+ import tempfile
11
+ import struct
12
+ import pyudev
13
+ from datetime import datetime
14
+ from packaging import version
15
+
16
+ from amd_debug.wake import WakeIRQ
17
+ from amd_debug.kernel import get_kernel_log, SystemdLogger, DmesgLogger
18
+ from amd_debug.common import (
19
+ BIT,
20
+ clear_temporary_message,
21
+ get_distro,
22
+ get_pretty_distro,
23
+ get_property_pyudev,
24
+ is_root,
25
+ minimum_kernel,
26
+ print_color,
27
+ print_temporary_message,
28
+ read_file,
29
+ read_msr,
30
+ AmdTool,
31
+ )
32
+ from amd_debug.battery import Batteries
33
+ from amd_debug.database import SleepDatabase
34
+ from amd_debug.failures import (
35
+ AcpiNvmeStorageD3Enable,
36
+ AmdHsmpBug,
37
+ AmdgpuPpFeatureMask,
38
+ ASpmWrong,
39
+ DeepSleep,
40
+ DevSlpDiskIssue,
41
+ DevSlpHostIssue,
42
+ DMArNotEnabled,
43
+ DmiNotSetup,
44
+ FadtWrong,
45
+ I2CHidBug,
46
+ KernelRingBufferWrapped,
47
+ LimitedCores,
48
+ MissingAmdgpu,
49
+ MissingAmdgpuFirmware,
50
+ MissingAmdPmc,
51
+ MissingDriver,
52
+ MissingIommuACPI,
53
+ MissingIommuPolicy,
54
+ MissingThunderbolt,
55
+ MissingXhciHcd,
56
+ MSRFailure,
57
+ RogAllyMcuPowerSave,
58
+ RogAllyOldMcu,
59
+ SleepModeWrong,
60
+ SMTNotEnabled,
61
+ TaintedKernel,
62
+ UnservicedGpio,
63
+ UnsupportedModel,
64
+ WCN6855Bug,
65
+ )
66
+
67
+ # test if fwupd can report device firmware versions
68
+ try:
69
+ import gi
70
+ from gi.repository import GLib as _
71
+
72
+ gi.require_version("Fwupd", "2.0")
73
+ from gi.repository import Fwupd # pylint: disable=wrong-import-position
74
+
75
+ FWUPD = True
76
+ except ImportError:
77
+ FWUPD = False
78
+ except ValueError:
79
+ FWUPD = False
80
+
81
+
82
+ class Headers:
83
+ """Headers for the script"""
84
+
85
+ NvmeSimpleSuspend = "platform quirk: setting simple suspend"
86
+ RootError = "Must be executed by root user"
87
+ BrokenPrerequisites = "Your system does not meet s2idle prerequisites!"
88
+ ExplanationReport = "Explanations for your system"
89
+
90
+
91
+ class PrerequisiteValidator(AmdTool):
92
+ """Class to validate the prerequisites for s2idle"""
93
+
94
+ def __init__(self, tool_debug):
95
+ log_prefix = "s2idle" if tool_debug else None
96
+ super().__init__(log_prefix)
97
+
98
+ self.kernel_log = get_kernel_log()
99
+ if not is_root():
100
+ raise PermissionError("not root user")
101
+ self.cpu_family = None
102
+ self.cpu_model = None
103
+ self.cpu_model_string = None
104
+ self.pyudev = pyudev.Context()
105
+ self.failures = []
106
+ self.db = SleepDatabase()
107
+ self.db.start_cycle(datetime.now())
108
+ self.debug = tool_debug
109
+ self.distro = get_distro()
110
+ self.cmdline = read_file(os.path.join("/proc", "cmdline"))
111
+ self.irqs = []
112
+ self.smu_version = ""
113
+ self.smu_program = ""
114
+
115
+ def capture_once(self):
116
+ """Capture the prerequisites once"""
117
+ if not self.db.get_last_prereq_ts():
118
+ self.run()
119
+
120
+ def check_amdgpu(self):
121
+ """Check for the AMDGPU driver"""
122
+ for device in self.pyudev.list_devices(subsystem="pci"):
123
+ klass = device.properties.get("PCI_CLASS")
124
+ if klass not in ["30000", "38000"]:
125
+ continue
126
+ pci_id = device.properties.get("PCI_ID")
127
+ if not pci_id.startswith("1002"):
128
+ continue
129
+ if device.properties.get("DRIVER") != "amdgpu":
130
+ self.db.record_prereq("GPU driver `amdgpu` not loaded", "❌")
131
+ self.failures += [MissingAmdgpu()]
132
+ return False
133
+ slot = device.properties.get("PCI_SLOT_NAME")
134
+
135
+ self.db.record_prereq(f"GPU driver `amdgpu` bound to {slot}", "✅")
136
+ return True
137
+
138
+ def check_amdgpu_parameters(self):
139
+ """Check for AMDGPU parameters"""
140
+ p = os.path.join("/", "sys", "module", "amdgpu", "parameters", "ppfeaturemask")
141
+ if os.path.exists(p):
142
+ v = read_file(p)
143
+ if v != "0xfff7bfff":
144
+ self.db.record_prereq(f"AMDGPU ppfeaturemask overridden to {v}", "❌")
145
+ self.failures += [AmdgpuPpFeatureMask()]
146
+ return False
147
+ if not self.kernel_log:
148
+ message = "Unable to test for amdgpu from kernel log"
149
+ self.db.record_prereq(message, "🚦")
150
+ return True
151
+ self.kernel_log.seek()
152
+ match = self.kernel_log.match_pattern("Direct firmware load for amdgpu.*failed")
153
+ if match and not "amdgpu/isp" in match:
154
+ self.db.record_prereq("GPU firmware missing", "❌")
155
+ self.failures += [MissingAmdgpuFirmware([match])]
156
+ return False
157
+ return True
158
+
159
+ def check_wcn6855_bug(self):
160
+ """Check if WCN6855 WLAN is affected by a bug that causes spurious wakeups"""
161
+ if not self.kernel_log:
162
+ message = "Unable to test for wcn6855 bug from kernel log"
163
+ self.db.record_prereq(message, "🚦")
164
+ return True
165
+ wcn6855 = False
166
+ self.kernel_log.seek()
167
+ if self.kernel_log.match_pattern("ath11k_pci.*wcn6855"):
168
+ match = self.kernel_log.match_pattern("ath11k_pci.*fw_version")
169
+ if match:
170
+ self.db.record_debug("WCN6855 version string: %s" % match)
171
+ objects = match.split()
172
+ for i in range(0, len(objects)):
173
+ if objects[i] == "fw_build_id":
174
+ wcn6855 = objects[i + 1]
175
+
176
+ if wcn6855:
177
+ components = wcn6855.split(".")
178
+ if int(components[-1]) >= 37 or int(components[-1]) == 23:
179
+ self.db.record_prereq(
180
+ f"WCN6855 WLAN (fw build id {wcn6855})",
181
+ "✅",
182
+ )
183
+ else:
184
+ self.db.record_prereq(
185
+ f"WCN6855 WLAN may cause spurious wakeups (fw version {wcn6855})",
186
+ "❌",
187
+ )
188
+ self.failures += [WCN6855Bug()]
189
+
190
+ return True
191
+
192
+ def check_storage(self):
193
+ """Check if storage devices are supported"""
194
+ has_sata = False
195
+ has_ahci = False
196
+ valid_nvme = {}
197
+ invalid_nvme = {}
198
+ valid_sata = False
199
+ valid_ahci = False
200
+
201
+ if not self.kernel_log:
202
+ message = "Unable to test storage from kernel log"
203
+ self.db.record_prereq(message, "🚦")
204
+ return True
205
+
206
+ for dev in self.pyudev.list_devices(subsystem="pci", DRIVER="nvme"):
207
+ # https://git.kernel.org/torvalds/c/e79a10652bbd3
208
+ if minimum_kernel(6, 10):
209
+ self.db.record_debug("New enough kernel to avoid NVME check")
210
+ break
211
+ pci_slot_name = dev.properties["PCI_SLOT_NAME"]
212
+ vendor = dev.properties.get("ID_VENDOR_FROM_DATABASE", "")
213
+ model = dev.properties.get("ID_MODEL_FROM_DATABASE", "")
214
+ message = f"{vendor} {model}"
215
+ self.kernel_log.seek()
216
+ pattern = f"{pci_slot_name}.*{Headers.NvmeSimpleSuspend}"
217
+ if self.kernel_log.match_pattern(pattern):
218
+ valid_nvme[pci_slot_name] = message
219
+ if pci_slot_name not in valid_nvme:
220
+ invalid_nvme[pci_slot_name] = message
221
+
222
+ for dev in self.pyudev.list_devices(subsystem="pci", DRIVER="ahci"):
223
+ has_ahci = True
224
+ break
225
+
226
+ for dev in self.pyudev.list_devices(subsystem="block", ID_BUS="ata"):
227
+ has_sata = True
228
+ break
229
+
230
+ # Test AHCI
231
+ if has_ahci:
232
+ self.kernel_log.seek()
233
+ pattern = "ahci.*flags.*sadm.*sds"
234
+ if self.kernel_log.match_pattern(pattern):
235
+ valid_ahci = True
236
+ # Test SATA
237
+ if has_sata:
238
+ self.kernel_log.seek()
239
+ pattern = "ata.*Features.*Dev-Sleep"
240
+ if self.kernel_log.match_pattern(pattern):
241
+ valid_sata = True
242
+
243
+ if invalid_nvme:
244
+ for disk, _name in invalid_nvme.items():
245
+ message = f"NVME {invalid_nvme[disk].strip()} is not configured for s2idle in BIOS"
246
+ self.db.record_prereq(message, "❌")
247
+ num = len(invalid_nvme) + len(valid_nvme)
248
+ self.failures += [AcpiNvmeStorageD3Enable(invalid_nvme[disk], num)]
249
+ if valid_nvme:
250
+ for disk, _name in valid_nvme.items():
251
+ message = (
252
+ f"NVME {valid_nvme[disk].strip()} is configured for s2idle in BIOS"
253
+ )
254
+ self.db.record_prereq(message, "✅")
255
+ if has_sata:
256
+ if valid_sata:
257
+ message = "SATA supports DevSlp feature"
258
+ self.db.record_prereq(message, "✅")
259
+ else:
260
+ message = "SATA does not support DevSlp feature"
261
+ self.db.record_prereq(message, "❌")
262
+ self.failures += [DevSlpDiskIssue()]
263
+ if has_ahci:
264
+ if valid_ahci:
265
+ message = "AHCI is configured for DevSlp in BIOS"
266
+ self.db.record_prereq(message, "✅")
267
+ else:
268
+ message = "AHCI is not configured for DevSlp in BIOS"
269
+ self.db.record_prereq(message, "🚦")
270
+ self.failures += [DevSlpHostIssue()]
271
+
272
+ return (
273
+ (len(invalid_nvme) == 0)
274
+ and (valid_sata or not has_sata)
275
+ and (valid_ahci or not has_sata)
276
+ )
277
+
278
+ def check_amd_hsmp(self):
279
+ """Check for AMD HSMP driver"""
280
+ # not needed to check in newer kernels
281
+ # see https://github.com/torvalds/linux/commit/77f1972bdcf7513293e8bbe376b9fe837310ee9c
282
+ if minimum_kernel(6, 10):
283
+ self.db.record_debug("New enough kernel to avoid HSMP check")
284
+ return True
285
+ f = os.path.join("/", "boot", f"config-{platform.uname().release}")
286
+ if os.path.exists(f):
287
+ kconfig = read_file(f)
288
+ if "CONFIG_AMD_HSMP=y" in kconfig:
289
+ self.db.record_prereq(
290
+ "HSMP driver `amd_hsmp` driver may conflict with amd_pmc",
291
+ "❌",
292
+ )
293
+ self.failures += [AmdHsmpBug()]
294
+ return False
295
+
296
+ cmdline = read_file(os.path.join("/proc", "cmdline"))
297
+ blocked = "initcall_blacklist=hsmp_plt_init" in cmdline
298
+
299
+ p = os.path.join("/", "sys", "module", "amd_hsmp")
300
+ if os.path.exists(p) and not blocked:
301
+ self.db.record_prereq("`amd_hsmp` driver may conflict with amd_pmc", "❌")
302
+ self.failures += [AmdHsmpBug()]
303
+ return False
304
+
305
+ self.db.record_prereq(
306
+ f"HSMP driver `amd_hsmp` not detected (blocked: {blocked})",
307
+ "✅",
308
+ )
309
+ return True
310
+
311
+ def check_amd_pmc(self):
312
+ """Check if the amd_pmc driver is loaded"""
313
+ for device in self.pyudev.list_devices(subsystem="platform", DRIVER="amd_pmc"):
314
+ message = "PMC driver `amd_pmc` loaded"
315
+ p = os.path.join(device.sys_path, "smu_program")
316
+ v = os.path.join(device.sys_path, "smu_fw_version")
317
+ if os.path.exists(v):
318
+ try:
319
+ self.smu_version = read_file(v)
320
+ self.smu_program = read_file(p)
321
+ except TimeoutError:
322
+ self.db.record_prereq(
323
+ "failed to communicate using `amd_pmc` driver", "❌"
324
+ )
325
+ return False
326
+ message += f" (Program {self.smu_program} Firmware {self.smu_version})"
327
+ self.db.record_prereq(message, "✅")
328
+ return True
329
+ self.failures += [MissingAmdPmc()]
330
+ self.db.record_prereq(
331
+ "PMC driver `amd_pmc` did not bind to any ACPI device", "❌"
332
+ )
333
+ return False
334
+
335
+ def check_wlan(self):
336
+ """Checks for WLAN device"""
337
+ for device in self.pyudev.list_devices(subsystem="pci", PCI_CLASS="28000"):
338
+ slot = device.properties["PCI_SLOT_NAME"]
339
+ driver = device.properties.get("DRIVER")
340
+ if not driver:
341
+ self.db.record_prereq(f"WLAN device in {slot} missing driver", "🚦")
342
+ self.failures += [MissingDriver(slot)]
343
+ return False
344
+ self.db.record_prereq(f"WLAN driver `{driver}` bound to {slot}", "✅")
345
+ return True
346
+
347
+ def check_usb3(self):
348
+ """Check for the USB4 controller"""
349
+ for device in self.pyudev.list_devices(subsystem="pci", PCI_CLASS="C0330"):
350
+ slot = device.properties["PCI_SLOT_NAME"]
351
+ if device.properties.get("DRIVER") != "xhci_hcd":
352
+ self.db.record_prereq(
353
+ f"USB3 controller for {slot} not using `xhci_hcd` driver", "❌"
354
+ )
355
+ self.failures += [MissingXhciHcd()]
356
+ return False
357
+ self.db.record_prereq(f"USB3 driver `xhci_hcd` bound to {slot}", "✅")
358
+ return True
359
+
360
+ def check_usb4(self):
361
+ """Check if the thunderbolt driver is loaded"""
362
+ for device in self.pyudev.list_devices(subsystem="pci", PCI_CLASS="C0340"):
363
+ slot = device.properties["PCI_SLOT_NAME"]
364
+ if device.properties.get("DRIVER") != "thunderbolt":
365
+ self.db.record_prereq("USB4 driver `thunderbolt` missing", "❌")
366
+ self.failures += [MissingThunderbolt()]
367
+ return False
368
+ self.db.record_prereq(f"USB4 driver `thunderbolt` bound to {slot}", "✅")
369
+ return True
370
+
371
+ def check_sleep_mode(self):
372
+ """Check if the system is configured for s2idle"""
373
+ fn = os.path.join("/", "sys", "power", "mem_sleep")
374
+ if not os.path.exists(fn):
375
+ self.db.record_prereq("Kernel doesn't support sleep", "❌")
376
+ return False
377
+
378
+ cmdline = read_file(os.path.join("/proc", "cmdline"))
379
+ if "mem_sleep_default=deep" in cmdline:
380
+ self.db.record_prereq(
381
+ "Kernel command line is configured for 'deep' sleep", "❌"
382
+ )
383
+ self.failures += [DeepSleep()]
384
+ return False
385
+ if "[s2idle]" not in read_file(fn):
386
+ self.failures += [SleepModeWrong()]
387
+ self.db.record_prereq(
388
+ "System isn't configured for s2idle in firmware setup", "❌"
389
+ )
390
+ return False
391
+ self.db.record_prereq("System is configured for s2idle", "✅")
392
+ return True
393
+
394
+ def capture_smbios(self):
395
+ """Capture the SMBIOS (DMI) information"""
396
+ p = os.path.join("/", "sys", "class", "dmi", "id")
397
+ if not os.path.exists(p):
398
+ self.db.record_prereq("DMI data was not setup", "🚦")
399
+ self.failures += [DmiNotSetup()]
400
+ return False
401
+ else:
402
+ keys = {}
403
+ filtered = [
404
+ "product_serial",
405
+ "board_serial",
406
+ "board_asset_tag",
407
+ "chassis_asset_tag",
408
+ "chassis_serial",
409
+ "modalias",
410
+ "uevent",
411
+ "product_uuid",
412
+ ]
413
+ for root, _dirs, files in os.walk(p, topdown=False):
414
+ files.sort()
415
+ for fname in files:
416
+ if "power" in root:
417
+ continue
418
+ if fname in filtered:
419
+ continue
420
+ contents = read_file(os.path.join(root, fname))
421
+ keys[fname] = contents
422
+ if (
423
+ "sys_vendor" not in keys
424
+ or "product_name" not in keys
425
+ or "product_family" not in keys
426
+ ):
427
+ self.db.record_prereq("DMI data not found", "❌")
428
+ self.failures += [DmiNotSetup()]
429
+ return False
430
+ self.db.record_prereq(
431
+ f"{keys['sys_vendor']} {keys['product_name']} ({keys['product_family']})",
432
+ "💻",
433
+ )
434
+ debug_str = "DMI data:\n"
435
+ for key, value in keys.items():
436
+ if (
437
+ "product_name" in key
438
+ or "sys_vendor" in key
439
+ or "product_family" in key
440
+ ):
441
+ continue
442
+ debug_str += f"{key}: {value}\n"
443
+ self.db.record_debug(debug_str)
444
+ return True
445
+
446
+ def check_lps0(self):
447
+ """Check if LPS0 is enabled"""
448
+ for m in ["acpi", "acpi_x86"]:
449
+ p = os.path.join("/", "sys", "module", m, "parameters", "sleep_no_lps0")
450
+ if not os.path.exists(p):
451
+ continue
452
+ fail = read_file(p) == "Y"
453
+ if fail:
454
+ self.db.record_prereq("LPS0 _DSM disabled", "❌")
455
+ else:
456
+ self.db.record_prereq("LPS0 _DSM enabled", "✅")
457
+ return not fail
458
+ self.db.record_prereq("LPS0 _DSM not found", "👀")
459
+ return False
460
+
461
+ def get_cpu_vendor(self) -> str:
462
+ """Fetch information about the CPU vendor"""
463
+ p = os.path.join("/", "proc", "cpuinfo")
464
+ vendor = ""
465
+ cpu = read_file(p)
466
+ for line in cpu.split("\n"):
467
+ if "vendor_id" in line:
468
+ vendor = line.split()[-1]
469
+ continue
470
+ elif "cpu family" in line:
471
+ self.cpu_family = int(line.split()[-1])
472
+ continue
473
+ elif "model name" in line:
474
+ self.cpu_model_string = line.split(":")[-1].strip()
475
+ continue
476
+ elif "model" in line:
477
+ self.cpu_model = int(line.split()[-1])
478
+ continue
479
+ if self.cpu_family and self.cpu_model and self.cpu_model_string:
480
+ self.db.record_prereq(
481
+ "%s (family %x model %x)"
482
+ % (self.cpu_model_string, self.cpu_family, self.cpu_model),
483
+ "💻",
484
+ )
485
+ break
486
+ return vendor
487
+
488
+ # See https://github.com/torvalds/linux/commit/ec6c0503190417abf8b8f8e3e955ae583a4e50d4
489
+ def check_fadt(self):
490
+ """Check the kernel emitted a message specific to 6.0 or later indicating FADT had a bit set."""
491
+ found = False
492
+ if not self.kernel_log:
493
+ message = "Unable to test FADT from kernel log"
494
+ self.db.record_prereq(message, "🚦")
495
+ else:
496
+ self.kernel_log.seek()
497
+ matches = ["Low-power S0 idle used by default for system suspend"]
498
+ found = self.kernel_log.match_line(matches)
499
+ # try to look at FACP directly if not found (older kernel compat)
500
+ if not found:
501
+ self.db.record_debug("Fetching low power idle bit directly from FADT")
502
+ target = os.path.join("/", "sys", "firmware", "acpi", "tables", "FACP")
503
+ try:
504
+ with open(target, "rb") as r:
505
+ r.seek(0x70)
506
+ found = struct.unpack("<I", r.read(4))[0] & BIT(21)
507
+ except PermissionError:
508
+ self.db.record_prereq("FADT check unavailable", "🚦")
509
+ return True
510
+ if found:
511
+ message = "ACPI FADT supports Low-power S0 idle"
512
+ self.db.record_prereq(message, "✅")
513
+ else:
514
+ message = "ACPI FADT doesn't support Low-power S0 idle"
515
+ self.db.record_prereq(message, "❌")
516
+ self.failures += [FadtWrong()]
517
+ return found
518
+
519
+ def capture_kernel_version(self):
520
+ """Log the kernel version used"""
521
+ self.db.record_prereq(f"{get_pretty_distro()}", "🐧")
522
+ self.db.record_prereq(f"Kernel {platform.uname().release}", "🐧")
523
+
524
+ def capture_irq(self):
525
+ """Capture the IRQs to the log"""
526
+ p = os.path.join("/sys", "kernel", "irq")
527
+ for directory in os.listdir(p):
528
+ if os.path.isdir(os.path.join(p, directory)):
529
+ wake = WakeIRQ(directory, self.pyudev)
530
+ self.irqs.append([int(directory), str(wake)])
531
+ self.irqs.sort()
532
+ self.db.record_debug("Interrupts")
533
+ for irq in self.irqs:
534
+ # set prefix if last IRQ
535
+ prefix = "│ " if irq != self.irqs[-1] else "└─"
536
+ self.db.record_debug(f"{prefix}{irq[0]}: {irq[1]}")
537
+ return True
538
+
539
+ def capture_disabled_pins(self):
540
+ """Capture disabled pins from pinctrl-amd"""
541
+ base = os.path.join("/", "sys", "module", "gpiolib_acpi", "parameters")
542
+ debug_str = ""
543
+ for parameter in ["ignore_wake", "ignore_interrupt"]:
544
+ f = os.path.join(base, parameter)
545
+ if not os.path.exists(f):
546
+ continue
547
+ with open(f, "r", encoding="utf-8") as r:
548
+ d = r.read().rstrip()
549
+ if d != "(null)":
550
+ debug_str += f"{f} is configured to {d}\n"
551
+ if debug_str:
552
+ debug_str = "Disabled pins:\n" + debug_str
553
+ self.db.record_debug(debug_str)
554
+
555
+ def check_logger(self):
556
+ """Check the source for kernel logs"""
557
+ if isinstance(self.kernel_log, SystemdLogger):
558
+ self.db.record_prereq("Logs are provided via systemd", "✅")
559
+ if isinstance(self.kernel_log, DmesgLogger):
560
+ self.db.record_prereq(
561
+ "Logs are provided via dmesg, timestamps may not be accurate over multiple cycles",
562
+ "🚦",
563
+ )
564
+ header = self.kernel_log.capture_header()
565
+ if not re.search(r"Linux version .*", header):
566
+ self.db.record_prereq(
567
+ "Kernel ring buffer has wrapped, unable to accurately validate pre-requisites",
568
+ "❌",
569
+ )
570
+ self.failures += [KernelRingBufferWrapped()]
571
+ return False
572
+ return True
573
+
574
+ def check_permissions(self):
575
+ """Check if the user has permissions to write to /sys/power/state"""
576
+ p = os.path.join("/", "sys", "power", "state")
577
+ try:
578
+ with open(p, "w") as w:
579
+ pass
580
+ except PermissionError:
581
+ self.db.record_prereq("%s" % Headers.RootError, "👀")
582
+ return False
583
+ except FileNotFoundError:
584
+ self.db.record_prereq("Kernel doesn't support power management", "❌")
585
+ return False
586
+ return True
587
+
588
+ def capture_linux_firmware(self):
589
+ """Capture the linux-firmware package version"""
590
+ for num in range(0, 2):
591
+ p = os.path.join(
592
+ "/", "sys", "kernel", "debug", "dri", f"{num}", "amdgpu_firmware_info"
593
+ )
594
+ if os.path.exists(p):
595
+ self.db.record_debug_file(p)
596
+
597
+ def check_amd_cpu_hpet_wa(self):
598
+ """Check if the CPU offers the HPET workaround"""
599
+ show_warning = False
600
+ if self.cpu_family == 0x17:
601
+ if self.cpu_model in [0x68, 0x60]:
602
+ show_warning = True
603
+ elif self.cpu_family == 0x19:
604
+ if self.cpu_model == 0x50:
605
+ if self.smu_version:
606
+ show_warning = version.parse(self.smu_version) < version.parse(
607
+ "64.53.0"
608
+ )
609
+ if show_warning:
610
+ self.db.record_prereq(
611
+ "Timer based wakeup doesn't work properly for your ASIC/firmware, please manually wake the system",
612
+ "🚦",
613
+ )
614
+ return True
615
+
616
+ def check_pinctrl_amd(self):
617
+ """Check if the pinctrl_amd driver is loaded"""
618
+ debug_str = ""
619
+ for _device in self.pyudev.list_devices(
620
+ subsystem="platform", DRIVER="amd_gpio"
621
+ ):
622
+ self.db.record_prereq("GPIO driver `pinctrl_amd` available", "✅")
623
+ p = os.path.join("/", "sys", "kernel", "debug", "gpio")
624
+ try:
625
+ contents = read_file(p)
626
+ except PermissionError:
627
+ self.db.record_debug(f"Unable to capture {p}")
628
+ contents = None
629
+ header = False
630
+ if contents:
631
+ for line in contents.split("\n"):
632
+ if "WAKE_INT_MASTER_REG:" in line:
633
+ val = "en" if int(line.split()[1], 16) & BIT(15) else "dis"
634
+ self.db.record_debug("Windows GPIO 0 debounce: %sabled" % val)
635
+ continue
636
+ if not header and re.search("trigger", line):
637
+ debug_str += line + "\n"
638
+ header = True
639
+ if re.search("edge", line) or re.search("level", line):
640
+ debug_str += line + "\n"
641
+ if "🔥" in line:
642
+ self.failures += [UnservicedGpio()]
643
+ return False
644
+
645
+ if debug_str:
646
+ self.db.record_debug(debug_str)
647
+ return True
648
+ self.db.record_prereq("GPIO driver `pinctrl_amd` not loaded", "❌")
649
+ return False
650
+
651
+ def check_network(self):
652
+ """Check network devices for s2idle support"""
653
+ for device in self.pyudev.list_devices(subsystem="net", ID_NET_DRIVER="r8169"):
654
+ interface = device.properties.get("INTERFACE")
655
+ cmd = ["ethtool", interface]
656
+ wol_supported = False
657
+ output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode(
658
+ "utf-8"
659
+ )
660
+ for line in output.split("\n"):
661
+ if "Supports Wake-on" in line:
662
+ val = line.split(":")[1].strip()
663
+ if "g" in val:
664
+ self.db.record_debug(f"{interface} supports WoL")
665
+ wol_supported = True
666
+ else:
667
+ self.db.record_debug(f"{interface} doesn't support WoL ({val})")
668
+ elif "Wake-on" in line and wol_supported:
669
+ val = line.split(":")[1].strip()
670
+ if "g" in val:
671
+ self.db.record_prereq(f"{interface} has WoL enabled", "✅")
672
+ else:
673
+ self.db.record_prereq(
674
+ f"Platform may have low hardware sleep residency with Wake-on-lan disabled. Run `ethtool -s {interface} wol g` to enable it if necessary.",
675
+ "🚦",
676
+ )
677
+ return True
678
+
679
+ def check_asus_rog_ally(self):
680
+ """Check for MCU version on ASUS ROG Ally devices"""
681
+ for dev in self.pyudev.list_devices(subsystem="hid", DRIVER="asus_rog_ally"):
682
+ p = os.path.join(dev.sys_path, "mcu_version")
683
+ if not os.path.exists(p):
684
+ continue
685
+ v = int(read_file(p))
686
+ hid_id = get_property_pyudev(dev.properties, "HID_ID", "")
687
+ if "1ABE" in hid_id:
688
+ minv = 319
689
+ elif "1B4C" in hid_id:
690
+ minv = 313
691
+ else:
692
+ minv = None
693
+ if minv and v < minv:
694
+ self.db.record_prereq("ROG Ally MCU firmware too old", "❌")
695
+ self.failures += [RogAllyOldMcu(minv, v)]
696
+ return False
697
+ else:
698
+ self.db.record_debug("ASUS ROG MCU found with MCU version %d", v)
699
+ for dev in self.pyudev.list_devices(subsystem="firmware-attributes"):
700
+ p = os.path.join(
701
+ dev.sys_path, "attributes", "mcu_powersave", "current_value"
702
+ )
703
+ if not os.path.exists(p):
704
+ continue
705
+ v = int(read_file(p))
706
+ if v < 1:
707
+ self.db.record_prereq(
708
+ "Rog Ally doesn't have MCU powersave enabled", "❌"
709
+ )
710
+ self.failures += [RogAllyMcuPowerSave()]
711
+ return False
712
+
713
+ return True
714
+
715
+ def check_device_firmware(self):
716
+ """Check for device firmware issues"""
717
+ if not FWUPD:
718
+ self.db.record_debug(
719
+ "Device firmware checks unavailable without gobject introspection"
720
+ )
721
+ return True
722
+
723
+ client = Fwupd.Client()
724
+ devices = client.get_devices()
725
+ for device in devices:
726
+ # Dictionary of instance id to firmware version mappings that
727
+ # have been "reported" to be problematic
728
+ device_map = {
729
+ "8c36f7ee-cc11-4a36-b090-6363f54ecac2": "0.1.26", # https://gitlab.freedesktop.org/drm/amd/-/issues/3443
730
+ }
731
+ interesting_plugins = ["nvme", "tpm", "uefi_capsule"]
732
+ if device.get_plugin() in interesting_plugins:
733
+ logging.debug(
734
+ "%s %s firmware version: '%s'",
735
+ device.get_vendor(),
736
+ device.get_name(),
737
+ device.get_version(),
738
+ )
739
+ logging.debug("| %s", device.get_guids())
740
+ logging.debug("└─%s", device.get_instance_ids())
741
+ for item, ver in device_map.items():
742
+ if (
743
+ item in device.get_guids() or item in device.get_instance_ids()
744
+ ) and ver in device.get_version():
745
+ self.db.record_prereq(
746
+ f"Platform may have problems resuming. Upgrade the firmware for '{device.get_name()}' if you have problems.",
747
+ "🚦",
748
+ )
749
+ return True
750
+
751
+ def check_aspm(self):
752
+ """Check if ASPM has been overridden"""
753
+ p = os.path.join("/", "sys", "module", "pcie_aspm", "parameters", "policy")
754
+ contents = read_file(p)
755
+ policy = ""
756
+ for word in contents.split(" "):
757
+ if word.startswith("["):
758
+ policy = word
759
+ break
760
+ if policy != "[default]":
761
+ self.db.record_prereq(f"ASPM policy set to {policy}", "❌")
762
+ self.failures += [ASpmWrong()]
763
+ return False
764
+ self.db.record_prereq("ASPM policy set to 'default'", "✅")
765
+ return True
766
+
767
+ def check_i2c_hid(self):
768
+ """Check for I2C HID devices"""
769
+ devices = []
770
+ for dev in self.pyudev.list_devices(subsystem="input"):
771
+ if "NAME" not in dev.properties:
772
+ continue
773
+ parent = dev.find_parent(subsystem="i2c")
774
+ if parent is None:
775
+ continue
776
+ devices.append(dev)
777
+ if not devices:
778
+ return True
779
+ ret = True
780
+ debug_str = "I2C HID devices:\n"
781
+ for dev in devices:
782
+ name = dev.properties["NAME"]
783
+ parent = dev.find_parent(subsystem="i2c")
784
+ p = os.path.join(parent.sys_path, "firmware_node", "path")
785
+ if os.path.exists(p):
786
+ acpi_path = read_file(p)
787
+ else:
788
+ acpi_path = ""
789
+ p = os.path.join(parent.sys_path, "firmware_node", "hid")
790
+ if os.path.exists(p):
791
+ acpi_hid = read_file(p)
792
+ else:
793
+ acpi_hid = ""
794
+ # set prefix if last device
795
+ prefix = "| " if dev != devices[-1] else "└─"
796
+ debug_str += "{prefix}{name} [{acpi_hid}] : {acpi_path}\n".format(
797
+ prefix=prefix, name=name, acpi_hid=acpi_hid, acpi_path=acpi_path
798
+ )
799
+ if "IDEA5002" in name:
800
+ remediation = f"echo {parent.sys_path.split('/')[-1]} | sudo tee /sys/bus/i2c/drivers/{parent.driver}/unbind"
801
+
802
+ self.db.record_prereq(f"{name} may cause spurious wakeups", "❌")
803
+ self.failures += [I2CHidBug(name, remediation)]
804
+ ret = False
805
+ self.db.record_debug(debug_str)
806
+ return ret
807
+
808
+ def capture_pci_acpi(self):
809
+ """Map ACPI to PCI devices"""
810
+ devices = []
811
+ for dev in self.pyudev.list_devices(subsystem="pci"):
812
+ devices.append(dev)
813
+ debug_str = "PCI devices\n"
814
+ for dev in devices:
815
+ pci_id = dev.properties["PCI_ID"].lower()
816
+ pci_slot_name = dev.properties["PCI_SLOT_NAME"]
817
+ database_class = get_property_pyudev(
818
+ dev.properties, "ID_PCI_SUBCLASS_FROM_DATABASE", ""
819
+ )
820
+ database_vendor = get_property_pyudev(
821
+ dev.properties, "ID_VENDOR_FROM_DATABASE", ""
822
+ )
823
+ if dev.parent.subsystem != "pci":
824
+ if dev == devices[-1]:
825
+ prefix = "└─"
826
+ else:
827
+ prefix = "│ "
828
+ else:
829
+ if dev == devices[-1]:
830
+ prefix = "└─"
831
+ else:
832
+ prefix = "├─ "
833
+ p = os.path.join(dev.sys_path, "firmware_node", "path")
834
+ if os.path.exists(p):
835
+ acpi = read_file(p)
836
+ debug_str += (
837
+ "{prefix}{pci_slot_name} : {vendor} {cls} [{id}] : {acpi}\n".format(
838
+ prefix=prefix,
839
+ pci_slot_name=pci_slot_name,
840
+ vendor=database_vendor,
841
+ cls=database_class,
842
+ id=pci_id,
843
+ acpi=acpi,
844
+ )
845
+ )
846
+ else:
847
+ debug_str += "{prefix}{pci_slot_name} : {vendor} {cls} [{id}]\n".format(
848
+ prefix=prefix,
849
+ vendor=database_vendor,
850
+ pci_slot_name=pci_slot_name,
851
+ cls=database_class,
852
+ id=pci_id,
853
+ )
854
+ if debug_str:
855
+ self.db.record_debug(debug_str)
856
+
857
+ def map_acpi_path(self):
858
+ """Map of ACPI devices to ACPI paths"""
859
+ devices = []
860
+ for dev in self.pyudev.list_devices(subsystem="acpi"):
861
+ p = os.path.join(dev.sys_path, "path")
862
+ if not os.path.exists(p):
863
+ continue
864
+ p = os.path.join(dev.sys_path, "status")
865
+ if os.path.exists(p):
866
+ status = int(read_file(p))
867
+ if status == 0:
868
+ continue
869
+ devices.append(dev)
870
+ debug_str = "ACPI name: ACPI path [driver]\n"
871
+ for dev in devices:
872
+ if dev == devices[-1]:
873
+ prefix = "└─"
874
+ else:
875
+ prefix = "│ "
876
+ p = os.path.join(dev.sys_path, "path")
877
+ pth = read_file(p)
878
+ p = os.path.join(dev.sys_path, "physical_node", "driver")
879
+ if os.path.exists(p):
880
+ driver = os.path.basename(os.readlink(p))
881
+ else:
882
+ driver = None
883
+ debug_str += f"{prefix}{dev.sys_name}: {pth} [{driver}]\n"
884
+ if debug_str:
885
+ self.db.record_debug(debug_str)
886
+ return True
887
+
888
+ def capture_acpi(self):
889
+ """Capture ACPI tables"""
890
+ base = os.path.join("/", "sys", "firmware", "acpi", "tables")
891
+ for root, _dirs, files in os.walk(base, topdown=False):
892
+ for fname in files:
893
+ target = os.path.join(root, fname)
894
+ if "SSDT" in fname:
895
+ with open(target, "rb") as f:
896
+ s = f.read()
897
+ if s.find(b"_AEI") < 0:
898
+ continue
899
+ elif "IVRS" in fname:
900
+ pass
901
+ else:
902
+ continue
903
+ try:
904
+ tmpd = tempfile.mkdtemp()
905
+ prefix = os.path.join(tmpd, "acpi")
906
+ subprocess.check_call(
907
+ ["iasl", "-p", prefix, "-d", target],
908
+ stdout=subprocess.DEVNULL,
909
+ stderr=subprocess.DEVNULL,
910
+ )
911
+ self.db.record_debug_file("%s.dsl" % prefix)
912
+ except subprocess.CalledProcessError as e:
913
+ self.db.record_prereq(
914
+ f"Failed to capture ACPI table: {e.output}", "👀"
915
+ )
916
+ finally:
917
+ shutil.rmtree(tmpd)
918
+ return True
919
+
920
+ def capture_battery(self):
921
+ """Capture battery information"""
922
+ obj = Batteries()
923
+ for bat in obj.get_batteries():
924
+ desc = obj.get_description_string(bat)
925
+ self.db.record_prereq(desc, "🔋")
926
+
927
+ def capture_logind(self):
928
+ base = os.path.join("/", "etc", "systemd", "logind.conf")
929
+ if not os.path.exists(base):
930
+ return True
931
+ import configparser
932
+
933
+ config = configparser.ConfigParser()
934
+ config.read(base)
935
+ section = config["Login"]
936
+ if not section.keys():
937
+ self.db.record_debug("LOGIND: no configuration changes")
938
+ return True
939
+ self.db.record_debug("LOGIND: configuration changes:")
940
+ for key in section.keys():
941
+ self.db.record_debug(f"\t{key}: {section[key]}")
942
+
943
+ def check_cpu(self):
944
+ """Check if the CPU is supported"""
945
+
946
+ def read_cpuid(cpu, leaf, subleaf):
947
+ """Read CPUID using kernel userspace interface"""
948
+ p = os.path.join("/", "dev", "cpu", f"{cpu}", "cpuid")
949
+ if not os.path.exists(p):
950
+ os.system("modprobe cpuid")
951
+ with open(p, "rb") as f:
952
+ position = (subleaf << 32) | leaf
953
+ f.seek(position)
954
+ data = f.read(16)
955
+ return struct.unpack("4I", data)
956
+
957
+ valid = True
958
+
959
+ # check for supported models
960
+ if self.cpu_family == 0x17:
961
+ if self.cpu_model in range(0x30, 0x3F):
962
+ valid = False
963
+ if self.cpu_family == 0x19:
964
+ if self.cpu_model in [0x08, 0x18]:
965
+ valid = False
966
+
967
+ if not valid:
968
+ self.failures += [UnsupportedModel()]
969
+ self.db.record_prereq(
970
+ "This CPU model does not support hardware sleep over s2idle",
971
+ "❌",
972
+ )
973
+ return False
974
+
975
+ # check for artificially limited CPUs
976
+ p = os.path.join("/", "sys", "devices", "system", "cpu", "kernel_max")
977
+ max_cpus = int(read_file(p)) + 1 # 0 indexed
978
+ # https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/programmer-references/24594.pdf
979
+ # Extended Topology Enumeration (NumLogCores)
980
+ # CPUID 0x80000026 subleaf 1
981
+ try:
982
+ _, cpu_count, _, _ = read_cpuid(0, 0x80000026, 1)
983
+ if cpu_count > max_cpus:
984
+ self.db.record_prereq(
985
+ f"The kernel has been limited to {max_cpus} CPU cores, but the system has {cpu_count} cores",
986
+ "❌",
987
+ )
988
+ self.failures += [LimitedCores(cpu_count, max_cpus)]
989
+ return False
990
+ self.db.record_debug(f"CPU core count: {cpu_count} max: {max_cpus}")
991
+ except FileNotFoundError:
992
+ self.db.record_prereq(
993
+ "Unable to check CPU topology: cpuid kernel module not loaded", "❌"
994
+ )
995
+ return False
996
+ except PermissionError:
997
+ self.db.record_prereq("CPUID checks unavailable", "🚦")
998
+
999
+ if valid:
1000
+ self.db.record_prereq(
1001
+ f"{self.cpu_model_string} (family {self.cpu_family:x} model {self.cpu_model:x})",
1002
+ "✅",
1003
+ )
1004
+
1005
+ return True
1006
+
1007
+ def check_msr(self):
1008
+ """Check if PC6 or CC6 has been disabled"""
1009
+
1010
+ def check_bits(value, mask):
1011
+ return value & mask
1012
+
1013
+ expect = {
1014
+ 0xC0010292: BIT(32), # PC6
1015
+ 0xC0010296: (BIT(22) | BIT(14) | BIT(6)), # CC6
1016
+ }
1017
+ try:
1018
+ for reg, expect_val in expect.items():
1019
+ val = read_msr(reg, 0)
1020
+ if not check_bits(val, expect_val):
1021
+ self.failures += [MSRFailure()]
1022
+ return False
1023
+ self.db.record_prereq("PC6 and CC6 enabled", "✅")
1024
+ except FileNotFoundError:
1025
+ self.db.record_prereq(
1026
+ "Unable to check MSRs: MSR kernel module not loaded", "❌"
1027
+ )
1028
+ return False
1029
+ except PermissionError:
1030
+ self.db.record_prereq("MSR checks unavailable", "🚦")
1031
+ return True
1032
+
1033
+ def check_smt(self):
1034
+ """Check if SMT is enabled"""
1035
+ p = os.path.join("/", "sys", "devices", "system", "cpu", "smt", "control")
1036
+ v = read_file(p)
1037
+ self.db.record_debug(f"SMT control: {v}")
1038
+ if v == "notsupported":
1039
+ return True
1040
+ p = os.path.join("/", "sys", "devices", "system", "cpu", "smt", "active")
1041
+ v = read_file(p)
1042
+ if v == "0":
1043
+ self.failures += [SMTNotEnabled()]
1044
+ self.db.record_prereq("SMT is not enabled", "❌")
1045
+ return False
1046
+ self.db.record_prereq("SMT enabled", "✅")
1047
+ return True
1048
+
1049
+ def check_iommu(self):
1050
+ """Check IOMMU configuration"""
1051
+ affected_1a = (
1052
+ list(range(0x20, 0x2F)) + list(range(0x60, 0x6F)) + list(range(0x70, 0x7F))
1053
+ )
1054
+ debug_str = ""
1055
+ if self.cpu_family == 0x1A and self.cpu_model in affected_1a:
1056
+ found_iommu = False
1057
+ found_acpi = False
1058
+ found_dmar = False
1059
+ for dev in self.pyudev.list_devices(subsystem="iommu"):
1060
+ found_iommu = True
1061
+ debug_str += f"Found IOMMU {dev.sys_path}\n"
1062
+ break
1063
+ if not found_iommu:
1064
+ self.db.record_prereq("IOMMU disabled", "✅")
1065
+ return True
1066
+ debug_str += "DMA protection:\n"
1067
+ for dev in self.pyudev.list_devices(
1068
+ subsystem="thunderbolt", DEVTYPE="thunderbolt_domain"
1069
+ ):
1070
+ p = os.path.join(dev.sys_path, "iommu_dma_protection")
1071
+ v = int(read_file(p))
1072
+ debug_str += f"\t{p}: {v}\n"
1073
+ found_dmar = v == 1
1074
+ self.db.record_debug(debug_str)
1075
+ if not found_dmar:
1076
+ self.db.record_prereq(
1077
+ "IOMMU is misconfigured: Pre-boot DMA protection not enabled", "❌"
1078
+ )
1079
+ self.failures += [DMArNotEnabled()]
1080
+ return False
1081
+ for dev in self.pyudev.list_devices(subsystem="acpi"):
1082
+ if "MSFT0201" in dev.sys_path:
1083
+ found_acpi = True
1084
+ if not found_acpi:
1085
+ self.db.record_prereq(
1086
+ "IOMMU is misconfigured: missing MSFT0201 ACPI device", "❌"
1087
+ )
1088
+ self.failures += [MissingIommuACPI("MSFT0201")]
1089
+ return False
1090
+ # check that policy is bound to it
1091
+ for dev in self.pyudev.list_devices(subsystem="platform"):
1092
+ if "MSFT0201" in dev.sys_path:
1093
+ p = os.path.join(dev.sys_path, "iommu")
1094
+ if not os.path.exists(p):
1095
+ self.failures += [MissingIommuPolicy("MSFT0201")]
1096
+ return False
1097
+ self.db.record_prereq("IOMMU properly configured", "✅")
1098
+ return True
1099
+
1100
+ def check_port_pm_override(self):
1101
+ """Check for PCIe port power management override"""
1102
+ if self.cpu_family != 0x19:
1103
+ return True
1104
+ if self.cpu_model not in [0x74, 0x78]:
1105
+ return True
1106
+ if version.parse(self.smu_version) > version.parse("76.60.0"):
1107
+ return True
1108
+ if version.parse(self.smu_version) < version.parse("76.18.0"):
1109
+ return True
1110
+ cmdline = read_file(os.path.join("/proc", "cmdline"))
1111
+ if "pcie_port_pm=off" in cmdline:
1112
+ return True
1113
+ self.db.record_prereq(
1114
+ "Platform may hang resuming. Upgrade your firmware or add pcie_port_pm=off to kernel command line if you have problems.",
1115
+ "🚦",
1116
+ )
1117
+ return False
1118
+
1119
+ def check_taint(self):
1120
+ """Check if the kernel is tainted"""
1121
+ fn = os.path.join("/", "proc", "sys", "kernel", "tainted")
1122
+ taint = int(read_file(fn))
1123
+ # ignore kernel warnings
1124
+ taint &= ~BIT(9)
1125
+ if taint != 0:
1126
+ self.db.record_prereq(f"Kernel is tainted: {taint}", "❌")
1127
+ self.failures += [TaintedKernel()]
1128
+ return False
1129
+ return True
1130
+
1131
+ def run(self):
1132
+ """Run the prerequisites check"""
1133
+ msg = "Checking prerequisites, please wait"
1134
+ print_temporary_message(msg)
1135
+ info = [
1136
+ self.capture_smbios,
1137
+ self.capture_kernel_version,
1138
+ self.capture_battery,
1139
+ self.capture_linux_firmware,
1140
+ self.capture_logind,
1141
+ self.capture_pci_acpi,
1142
+ ]
1143
+ checks = []
1144
+
1145
+ vendor = self.get_cpu_vendor()
1146
+ if vendor == "AuthenticAMD":
1147
+ info += [
1148
+ self.capture_disabled_pins,
1149
+ ]
1150
+ checks += [
1151
+ self.check_aspm,
1152
+ self.check_i2c_hid,
1153
+ self.check_pinctrl_amd,
1154
+ self.check_amd_hsmp,
1155
+ self.check_amd_pmc,
1156
+ self.check_amd_cpu_hpet_wa,
1157
+ self.check_port_pm_override,
1158
+ self.check_usb3,
1159
+ self.check_usb4,
1160
+ self.check_sleep_mode,
1161
+ self.check_storage,
1162
+ self.check_wcn6855_bug,
1163
+ self.check_amdgpu,
1164
+ self.check_amdgpu_parameters,
1165
+ self.check_cpu,
1166
+ self.check_msr,
1167
+ self.check_smt,
1168
+ self.check_iommu,
1169
+ self.check_asus_rog_ally,
1170
+ ]
1171
+
1172
+ checks += [
1173
+ self.check_fadt,
1174
+ self.check_logger,
1175
+ self.check_lps0,
1176
+ self.check_permissions,
1177
+ self.check_wlan,
1178
+ self.check_taint,
1179
+ self.capture_acpi,
1180
+ self.map_acpi_path,
1181
+ self.check_device_firmware,
1182
+ self.check_network,
1183
+ ]
1184
+
1185
+ for i in info:
1186
+ i()
1187
+
1188
+ result = True
1189
+ for check in checks:
1190
+ if not check():
1191
+ result = False
1192
+ if not result:
1193
+ self.db.record_prereq(Headers.BrokenPrerequisites, "💯")
1194
+ self.db.sync()
1195
+ clear_temporary_message(len(msg))
1196
+ return result
1197
+
1198
+ def report(self) -> None:
1199
+ """Print a report of the results of the checks."""
1200
+ ts = self.db.get_last_prereq_ts()
1201
+ t0 = datetime.strptime(str(ts), "%Y%m%d%H%M%S")
1202
+ for row in self.db.report_prereq(t0):
1203
+ print_color(row[2], row[3])
1204
+ for row in self.db.report_debug(t0):
1205
+ for line in row[0].split("\n"):
1206
+ if self.debug:
1207
+ print_color(line, "🦟")
1208
+ else:
1209
+ logging.debug(line)
1210
+
1211
+ if len(self.failures) == 0:
1212
+ return True
1213
+ print_color(Headers.ExplanationReport, "🗣️")
1214
+ for item in self.failures:
1215
+ item.get_failure()