#!/bin/bash
# Keeps the kvm_amd cpuid_passthrough switch correct across a guest's whole life,
# in-guest reboots included.
#
# The switch must be OFF (N) while the guest enumerates CPUID at boot or Windows
# hangs, and ON (Y) once it is up, where it clears the timer detection. The libvirt
# hook only fires at VM start and stop, so a guest-initiated reboot would otherwise
# re-enumerate with the switch still Y and hang. This watcher drops it to N on every
# QEMU RESET and raises it again once the guest's NIC is back up.
#
# Readiness signal: the guest tap's rx_packets counter growing past a baseline. It
# only moves once the guest NIC driver has loaded, well past CPU enumeration, and it
# needs nothing enabled inside the guest. The counter is cumulative and does NOT
# reset on an in-guest reboot, so readiness is growth past the value captured at the
# reset, not an absolute threshold.
#
# Launched as a transient systemd unit by the cpuid-passthrough hook, so it is free
# to call virsh (the hook itself must not - that deadlocks libvirtd).

DOMAIN=$1
BRAND=$2
PARAM=/sys/module/kvm_amd/parameters
V="virsh -c qemu:///system"

running() { [ "$($V domstate "$DOMAIN" 2>/dev/null)" = running ]; }
rx() { cat "$RX" 2>/dev/null || echo 0; }

set_N() { echo N > "$PARAM/cpuid_passthrough"; }
set_Y() { printf '%s' "$BRAND" > "$PARAM/brand_string"; echo Y > "$PARAM/cpuid_passthrough"; }

# Wait until the tap rx counter grows at least 4 past $1 (guest NIC driver up again).
# The iteration floor keeps a stray pre-OS packet (a UEFI netboot attempt) from
# tripping the flip before the guest is even past its interrupt and timer setup.
wait_net_up() {
    local base=$1 i=0
    for _ in $(seq 1 100); do            # ~300 s cap, then raise anyway if still up
        i=$((i + 1))
        running || return 1
        [ "$i" -ge 4 ] && [ -n "$tap" ] && [ -r "$RX" ] &&
            [ "$(rx)" -ge "$((base + 4))" ] && return 0
        sleep 3
    done
    return 0
}

# The hook arms this watcher at prepare, before QEMU starts, so the domain is not
# yet running and its tap does not exist. Wait for the domain, then read the tap;
# otherwise the first running check in wait_net_up bails and the flip never happens.
for _ in $(seq 1 120); do running && break; sleep 1; done
running || exit 0
tap=$($V domiflist "$DOMAIN" 2>/dev/null | awk '$1 ~ /^(vnet|tap|macvtap)/ {print $1; exit}')
RX="/sys/class/net/$tap/statistics/rx_packets"

# initial cold boot: wait for the network, then harden
wait_net_up 0
running || exit 0
set_Y
logger -t vfio-cpuid "$DOMAIN network up: cpuid_passthrough=Y"

# every in-guest reboot fires a QEMU RESET: drop to N for the re-enumeration, then
# raise it again once the guest's NIC is back. --loop streams one line per reset.
$V qemu-monitor-event --domain "$DOMAIN" --event RESET --loop 2>/dev/null | while read -r _; do
    running || continue
    base=$(rx)
    set_N
    logger -t vfio-cpuid "$DOMAIN reset: cpuid_passthrough=N for re-enumeration"
    wait_net_up "$base" || continue
    running || continue
    set_Y
    logger -t vfio-cpuid "$DOMAIN back up: cpuid_passthrough=Y"
done
