module for recovering shitty realtek controller when it gets stuck in bugged power state
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
{ config, lib, pkgs, namespace, ... }:
|
||||
let
|
||||
cfg = config.${namespace}.rtl8125-recovery;
|
||||
recovery = pkgs.writeShellApplication {
|
||||
name = "rtl8125-recover";
|
||||
runtimeInputs = with pkgs; [ coreutils gnugrep pciutils ethtool systemd ];
|
||||
text = builtins.readFile ./recover.sh;
|
||||
};
|
||||
in {
|
||||
options.${namespace}.rtl8125-recovery = {
|
||||
enable = lib.mkEnableOption "RTL8125 PCI recovery after boot and resume";
|
||||
pciAddress = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "0000:0a:00.0";
|
||||
description = "PCI address of the onboard RTL8125 controller.";
|
||||
};
|
||||
bridgeAddress = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "0000:06:03.0";
|
||||
description = "Immediate parent PCI bridge of the controller.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
systemd.services.rtl8125-recovery = {
|
||||
description = "Recover RTL8125 from failed PCI power restoration";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "NetworkManager.service" ];
|
||||
environment = {
|
||||
NIC_PCI = cfg.pciAddress;
|
||||
NIC_BRIDGE = cfg.bridgeAddress;
|
||||
};
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = lib.getExe recovery;
|
||||
TimeoutStartSec = "90s";
|
||||
StateDirectory = "rtl8125-recovery";
|
||||
StateDirectoryMode = "0700";
|
||||
UMask = "0077";
|
||||
};
|
||||
};
|
||||
|
||||
# sleep.target is stopped after resume. Queue recovery separately so its
|
||||
# grace period does not hold up the sleep transaction or user sessions.
|
||||
systemd.services.rtl8125-resume = {
|
||||
description = "Queue RTL8125 recovery after waking";
|
||||
wantedBy = [ "sleep.target" ];
|
||||
before = [ "sleep.target" ];
|
||||
unitConfig.StopWhenUnneeded = true;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart = "${pkgs.coreutils}/bin/true";
|
||||
ExecStop = "${pkgs.systemd}/bin/systemctl --no-block start rtl8125-recovery.service";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
# Called once after boot or resume. No network access is required.
|
||||
set -euo pipefail
|
||||
nic="/sys/bus/pci/devices/$NIC_PCI"
|
||||
bridge="/sys/bus/pci/devices/$NIC_BRIDGE"
|
||||
log() { echo "rtl8125-recovery: $*"; }
|
||||
|
||||
# Let normal PCI and network resume finish before diagnosing anything.
|
||||
sleep 10
|
||||
if [[ ! -d "$bridge" ]]; then
|
||||
log "Parent bridge $NIC_BRIDGE is absent; refusing recovery."
|
||||
exit 1
|
||||
fi
|
||||
if [[ $(cat "$bridge/vendor") != 0x1022 || $(cat "$bridge/device") != 0x43f5 ]]; then
|
||||
log "Parent bridge identity changed; refusing recovery."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -d "$nic" ]]; then
|
||||
if [[ $(cat "$nic/vendor") != 0x10ec || $(cat "$nic/device") != 0x8125 ]]; then
|
||||
log "Device at $NIC_PCI is not an RTL8125; refusing recovery."
|
||||
exit 1
|
||||
fi
|
||||
if [[ $(dirname "$(readlink -f "$nic")") != "$(readlink -f "$bridge")" ]]; then
|
||||
log "NIC is no longer directly below the configured bridge; refusing recovery."
|
||||
exit 1
|
||||
fi
|
||||
power=$(cat "$nic/power_state")
|
||||
command=$(timeout 5 setpci -s "$NIC_PCI" COMMAND 2>/dev/null || true)
|
||||
# Both memory decoding and bus mastering should be enabled on this host.
|
||||
# Disconnected cables do not disable these PCI command bits.
|
||||
if [[ "$power" != D3cold && "$command" =~ ^[[:xdigit:]]{4}$ ]] &&
|
||||
(( (16#$command & 6) == 6 )); then
|
||||
log "PCI state healthy ($power, COMMAND=$command); no recovery needed."
|
||||
exit 0
|
||||
fi
|
||||
# An administratively disabled NIC should not be automatically reset.
|
||||
netdirs=("$nic"/net/*)
|
||||
if [[ -e "${netdirs[0]}/flags" ]]; then
|
||||
flags=$(cat "${netdirs[0]}/flags")
|
||||
if (( (flags & 1) == 0 )); then
|
||||
log "Interface is administratively down; leaving it alone."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
log "Failed PCI state: power=$power COMMAND=$command."
|
||||
else
|
||||
log "NIC missing; attempting one rescan of its known bridge."
|
||||
fi
|
||||
|
||||
# Retain evidence from the latest failure without accumulating unlimited logs.
|
||||
evidence="${STATE_DIRECTORY:-/var/lib/rtl8125-recovery}/last-failure.log"
|
||||
mkdir -p "$(dirname "$evidence")"
|
||||
{
|
||||
date -Is
|
||||
timeout 5 lspci -s "$NIC_PCI" -vv || true
|
||||
timeout 5 lspci -s "$NIC_BRIDGE" -vv || true
|
||||
journalctl -b -k --since '10 minutes ago' -n 250 --no-pager || true
|
||||
} > "$evidence" 2>&1
|
||||
|
||||
# This is the remove/rescan recovery that worked on this machine. Never reset
|
||||
# or remove the parent bridge, which could affect other hardware.
|
||||
if [[ -d "$nic" ]]; then
|
||||
echo 1 > "$nic/remove"
|
||||
fi
|
||||
echo 1 > "$bridge/rescan"
|
||||
|
||||
for ((attempt=0; attempt<20; attempt++)); do
|
||||
netdirs=("$nic"/net/*)
|
||||
if [[ -e "${netdirs[0]}/ifindex" ]]; then
|
||||
iface=$(basename "${netdirs[0]}")
|
||||
power=$(cat "$nic/power_state")
|
||||
command=$(timeout 5 setpci -s "$NIC_PCI" COMMAND 2>/dev/null || true)
|
||||
if [[ "$power" == D0 && "$command" =~ ^[[:xdigit:]]{4}$ ]] &&
|
||||
(( (16#$command & 6) == 6 )); then
|
||||
# NetworkManager also reapplies the saved profile. Explicitly
|
||||
# restore WoL here so recovery does not depend on its timing.
|
||||
ethtool -s "$iface" wol g
|
||||
if ! ethtool "$iface" | grep -Eq '^[[:space:]]*Wake-on: g$'; then
|
||||
log "PCI recovered, but magic-packet WoL could not be confirmed."
|
||||
exit 1
|
||||
fi
|
||||
log "Recovered $iface in D0; magic-packet WoL enabled. Evidence: $evidence"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
log "Recovery did not restore operational PCI state; no further attempt this invocation."
|
||||
exit 1
|
||||
Reference in New Issue
Block a user