Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d405bf7caf | |||
|
9bb0f9a684
|
@@ -5,6 +5,14 @@ All notable changes to RigDoctor are recorded here. Format follows
|
|||||||
(`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git
|
(`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git
|
||||||
release tag (so the auto-updater, D18, can compare versions).
|
release tag (so the auto-updater, D18, can compare versions).
|
||||||
|
|
||||||
|
## [0.38.0] - 2026-05-22
|
||||||
|
### Added
|
||||||
|
- **PCIe link in the Inventory.** Each NVMe drive now shows its negotiated PCIe link next to the
|
||||||
|
model — e.g. `Samsung SSD 980 PRO 1TB (931.5G) · PCIe Gen4 x4` — read from sysfs
|
||||||
|
(`current/max_link_speed` + width). If a drive negotiates below its capability (a slower M.2
|
||||||
|
slot, lane-sharing, or a downtrain) it's flagged: `PCIe Gen3 x4 (capable of Gen4 x4)`. So you
|
||||||
|
can confirm a Gen4 SSD is actually in a Gen4 slot. (SATA disks show no PCIe link.)
|
||||||
|
|
||||||
## [0.37.1] - 2026-05-22
|
## [0.37.1] - 2026-05-22
|
||||||
### Fixed
|
### Fixed
|
||||||
- **`rigdoctor update` now uses the right method for how RigDoctor was installed.** It detects
|
- **`rigdoctor update` now uses the right method for how RigDoctor was installed.** It detects
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "rigdoctor"
|
name = "rigdoctor"
|
||||||
version = "0.37.1"
|
version = "0.38.0"
|
||||||
description = "Modular hardware monitoring & crash diagnostics for Linux gamers."
|
description = "Modular hardware monitoring & crash diagnostics for Linux gamers."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
|
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
|
||||||
|
|
||||||
__version__ = "0.37.1"
|
__version__ = "0.38.0"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -123,6 +124,46 @@ def _gpu() -> Section:
|
|||||||
return Section("GPU", [("Device", g) for g in gpus] or [("Device", "unknown")])
|
return Section("GPU", [("Device", g) for g in gpus] or [("Device", "unknown")])
|
||||||
|
|
||||||
|
|
||||||
|
# PCIe link speed (GT/s) → generation.
|
||||||
|
_PCIE_GEN = {"2.5": 1, "5": 2, "5.0": 2, "8": 3, "8.0": 3, "16": 4, "16.0": 4, "32": 5, "32.0": 5}
|
||||||
|
|
||||||
|
|
||||||
|
def _gen(speed: str) -> int | None:
|
||||||
|
"""Map a sysfs link speed like '16.0 GT/s PCIe' to its PCIe generation (4)."""
|
||||||
|
tok = speed.strip().split()[0] if speed.strip() else ""
|
||||||
|
return _PCIE_GEN.get(tok)
|
||||||
|
|
||||||
|
|
||||||
|
def _link_desc(dev: Path) -> str:
|
||||||
|
"""Describe a PCI device's negotiated PCIe link, noting if it's below its max.
|
||||||
|
|
||||||
|
e.g. 'PCIe Gen4 x4', or 'PCIe Gen3 x4 (capable of Gen4 x4)' when downtrained / in a
|
||||||
|
slower slot.
|
||||||
|
"""
|
||||||
|
def rd(name: str) -> str:
|
||||||
|
try:
|
||||||
|
return (dev / name).read_text().strip()
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
cur_g, cur_w = _gen(rd("current_link_speed")), rd("current_link_width")
|
||||||
|
max_g, max_w = _gen(rd("max_link_speed")), rd("max_link_width")
|
||||||
|
if not cur_g or not cur_w:
|
||||||
|
return ""
|
||||||
|
desc = f"PCIe Gen{cur_g} x{cur_w}"
|
||||||
|
if max_g and (cur_g < max_g or (max_w and cur_w != max_w)):
|
||||||
|
desc += f" (capable of Gen{max_g} x{max_w})"
|
||||||
|
return desc
|
||||||
|
|
||||||
|
|
||||||
|
def _nvme_link(block_name: str) -> str:
|
||||||
|
"""PCIe link for an NVMe block device (nvme0n1 → controller nvme0); '' for non-NVMe."""
|
||||||
|
m = re.match(r"(nvme\d+)", block_name)
|
||||||
|
if not m:
|
||||||
|
return ""
|
||||||
|
return _link_desc(Path("/sys/class/nvme") / m.group(1) / "device")
|
||||||
|
|
||||||
|
|
||||||
def _storage() -> Section:
|
def _storage() -> Section:
|
||||||
items: list[tuple[str, str]] = []
|
items: list[tuple[str, str]] = []
|
||||||
# TYPE first so MODEL (which can contain spaces) is the trailing field.
|
# TYPE first so MODEL (which can contain spaces) is the trailing field.
|
||||||
@@ -133,7 +174,11 @@ def _storage() -> Section:
|
|||||||
continue
|
continue
|
||||||
name, size = parts[1], parts[2]
|
name, size = parts[1], parts[2]
|
||||||
model = parts[3] if len(parts) > 3 else ""
|
model = parts[3] if len(parts) > 3 else ""
|
||||||
items.append((name, f"{model} ({size})".strip()))
|
desc = f"{model} ({size})".strip()
|
||||||
|
link = _nvme_link(name) # NVMe PCIe gen/width (e.g. Gen4 x4), flags downtrains
|
||||||
|
if link:
|
||||||
|
desc += f" · {link}"
|
||||||
|
items.append((name, desc))
|
||||||
return Section("Storage", items or [("Disks", "unknown")])
|
return Section("Storage", items or [("Disks", "unknown")])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"""Tests for the M5 system inventory (render + dict round-trip; collect on real system)."""
|
"""Tests for the M5 system inventory (render + dict round-trip; collect on real system)."""
|
||||||
|
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from rigdoctor.core import inventory
|
from rigdoctor.core import inventory
|
||||||
from rigdoctor.core.inventory import Section
|
from rigdoctor.core.inventory import Section
|
||||||
@@ -26,5 +28,31 @@ class InventoryTests(unittest.TestCase):
|
|||||||
self.assertIn("- **Model:** Test CPU", md)
|
self.assertIn("- **Model:** Test CPU", md)
|
||||||
|
|
||||||
|
|
||||||
|
class PcieLinkTests(unittest.TestCase):
|
||||||
|
def test_gen_mapping(self):
|
||||||
|
self.assertEqual(inventory._gen("16.0 GT/s PCIe"), 4)
|
||||||
|
self.assertEqual(inventory._gen("8.0 GT/s PCIe"), 3)
|
||||||
|
self.assertIsNone(inventory._gen(""))
|
||||||
|
|
||||||
|
def _fake_dev(self, cur_s, cur_w, max_s, max_w) -> Path:
|
||||||
|
d = Path(tempfile.mkdtemp())
|
||||||
|
(d / "current_link_speed").write_text(cur_s)
|
||||||
|
(d / "current_link_width").write_text(cur_w)
|
||||||
|
(d / "max_link_speed").write_text(max_s)
|
||||||
|
(d / "max_link_width").write_text(max_w)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_link_at_full_speed(self):
|
||||||
|
dev = self._fake_dev("16.0 GT/s PCIe", "4", "16.0 GT/s PCIe", "4")
|
||||||
|
self.assertEqual(inventory._link_desc(dev), "PCIe Gen4 x4")
|
||||||
|
|
||||||
|
def test_link_downtrained_flags_capability(self):
|
||||||
|
dev = self._fake_dev("8.0 GT/s PCIe", "4", "16.0 GT/s PCIe", "4")
|
||||||
|
self.assertEqual(inventory._link_desc(dev), "PCIe Gen3 x4 (capable of Gen4 x4)")
|
||||||
|
|
||||||
|
def test_non_nvme_has_no_link(self):
|
||||||
|
self.assertEqual(inventory._nvme_link("sda"), "")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user