From 77eb8c7b789f1bd3777fd5985013c3d26e7d7476 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Tue, 21 Apr 2026 21:06:22 +0200 Subject: [PATCH] update pci_passthrough_helpers.sh --- images/riov-indicator.png | Bin 0 -> 55123 bytes scripts/global/pci_passthrough_helpers.sh | 199 +++++++++++++++ scripts/gpu_tpu/add_gpu_vm.sh | 280 +++++++++++++++++++--- scripts/gpu_tpu/switch_gpu_mode.sh | 103 +++++++- scripts/gpu_tpu/switch_gpu_mode_direct.sh | 68 +++++- 5 files changed, 614 insertions(+), 36 deletions(-) create mode 100644 images/riov-indicator.png diff --git a/images/riov-indicator.png b/images/riov-indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..a1af122d18d968dca78e9f706b5d3190aa7ba951 GIT binary patch literal 55123 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 CHu!x2 literal 0 HcmV?d00001 diff --git a/scripts/global/pci_passthrough_helpers.sh b/scripts/global/pci_passthrough_helpers.sh index 101f0660..1317ff32 100644 --- a/scripts/global/pci_passthrough_helpers.sh +++ b/scripts/global/pci_passthrough_helpers.sh @@ -11,6 +11,205 @@ function _pci_is_iommu_active() { find /sys/kernel/iommu_groups -mindepth 1 -maxdepth 1 -type d -print -quit 2>/dev/null | grep -q . } +# Audio-companion cascade helpers (Part 2 of the SR-IOV / audio rework). +# +# When a GPU is detached from a VM (user chooses "Remove GPU from VM +# config" during a mode switch), the historic sed-based cleanup only +# removes hostpci lines that match the GPU's PCI slot (e.g. 00:02). +# That leaves any "companion" audio that lives at a different slot — +# typically the chipset audio at 00:1f.X, which add_gpu_vm.sh now adds +# alongside an Intel iGPU via the checklist from Part 1 — stranded in +# the VM config. On the next VM start, vfio-pci is no longer claiming +# that audio device (its vendor:device was pulled from vfio.conf +# during the switch-back) and either QEMU fails to rebind it or it +# breaks host audio. +# +# _vm_list_orphan_audio_hostpci reports those stranded entries; each +# caller uses its own UI (dialog, whiptail, hybrid_msgbox) to confirm +# removal and then calls _vm_remove_hostpci_index per selected entry. + +# Usage: _vm_list_orphan_audio_hostpci +# gpu_slot_base: the GPU's PCI slot WITHOUT function suffix, e.g. "00:02". +# Output: one line per orphan entry, in the form "idx|bdf|human_name". +# Empty output when the VM has no audio passthrough outside the GPU slot. +# +# A hostpci audio entry is reported as "orphan" ONLY if the same VM has +# no display/3D-class hostpci at the same slot base. Rationale: the +# audio at e.g. 02:00.1 is the HDMI codec of a dGPU at 02:00.0 — if +# that dGPU is still being passed through to this VM (as a separate +# hostpciN), the audio belongs to it and must not be touched when +# detaching an unrelated GPU (e.g. an Intel iGPU at 00:02.0) from the +# same VM. Without this filter we would strip the HDMI audio of every +# other GPU in the VM, leaving them silent on next start. +function _vm_list_orphan_audio_hostpci() { + local vmid="$1" gpu_slot="$2" + [[ -n "$vmid" && -n "$gpu_slot" ]] || return 1 + local conf="/etc/pve/qemu-server/${vmid}.conf" + [[ -f "$conf" ]] || return 1 + + # ── Pass 1 ── collect the slot bases of hostpci entries whose target + # device is display/3D (class 03xx). These slots "own" any audio at + # the same slot base (the .1 HDMI codec pattern). + local -a display_slots=() + local line raw_bdf bdf class_hex slot_base + while IFS= read -r line; do + raw_bdf=$(printf '%s' "$line" \ + | grep -oE '(0000:)?[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-7]' \ + | head -1) + [[ -z "$raw_bdf" ]] && continue + bdf="$raw_bdf" + [[ "$bdf" =~ ^0000: ]] || bdf="0000:$bdf" + class_hex=$(cat "/sys/bus/pci/devices/${bdf}/class" 2>/dev/null | sed 's/^0x//') + if [[ "${class_hex:0:2}" == "03" ]]; then + slot_base="${bdf#0000:}" + slot_base="${slot_base%.*}" + display_slots+=("$slot_base") + fi + done < <(grep -E '^hostpci[0-9]+:' "$conf") + + # ── Pass 2 ── classify audio entries. + local idx raw name + local has_display_sibling ds + while IFS= read -r line; do + idx=$(printf '%s' "$line" | sed -nE 's/^hostpci([0-9]+):.*/\1/p') + [[ -z "$idx" ]] && continue + + raw=$(printf '%s' "$line" \ + | grep -oE '(0000:)?[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-7]' \ + | head -1) + [[ -z "$raw" ]] && continue + bdf="$raw" + [[ "$bdf" =~ ^0000: ]] || bdf="0000:$bdf" + slot_base="${bdf#0000:}" + slot_base="${slot_base%.*}" + + # Skip entries that match the GPU slot — those go through the + # caller's primary sed/qm-set cleanup, not through this helper. + [[ "$slot_base" == "$gpu_slot" ]] && continue + + # Only audio class devices (PCI class 04xx) are candidates. + class_hex=$(cat "/sys/bus/pci/devices/${bdf}/class" 2>/dev/null | sed 's/^0x//') + [[ "${class_hex:0:2}" == "04" ]] || continue + + # Display-sibling guard: skip audio that is the HDMI/DP codec of a + # still-present dGPU in this VM. + has_display_sibling=false + for ds in "${display_slots[@]}"; do + if [[ "$ds" == "$slot_base" ]]; then + has_display_sibling=true + break + fi + done + $has_display_sibling && continue + + name=$(lspci -nn -s "${bdf#0000:}" 2>/dev/null \ + | sed 's/^[^ ]* //' \ + | cut -c1-52) + [[ -z "$name" ]] && name="PCI audio device" + + printf '%s|%s|%s\n' "$idx" "$bdf" "$name" + done < <(grep -E '^hostpci[0-9]+:' "$conf") +} + +# Returns 0 if the given PCI BDF still appears as a hostpci passthrough +# target in any VM config, optionally excluding one or more VM IDs. +# Usage: _pci_bdf_in_any_vm [excluded_vmid]... +# +# Used by the switch-mode cascade to decide whether a companion audio +# device's vendor:device pair is safe to remove from /etc/modprobe.d/ +# vfio.conf (only if no other VM still references it). +function _pci_bdf_in_any_vm() { + local bdf="$1"; shift + [[ -n "$bdf" ]] || return 1 + local short_bdf="${bdf#0000:}" + local conf vmid ex skip + for conf in /etc/pve/qemu-server/*.conf; do + [[ -f "$conf" ]] || continue + vmid=$(basename "$conf" .conf) + skip=false + for ex in "$@"; do + if [[ "$vmid" == "$ex" ]]; then + skip=true + break + fi + done + $skip && continue + if grep -qE "^hostpci[0-9]+:.*(0000:)?${short_bdf}([,[:space:]]|$)" "$conf" 2>/dev/null; then + return 0 + fi + done + return 1 +} + +# Usage: _vm_remove_hostpci_index [log_file] +# Removes hostpci from the VM config via `qm set --delete` so the +# change goes through Proxmox's own validation path (running VMs get a +# staged update). Returns the exit code of qm set. +function _vm_remove_hostpci_index() { + local vmid="$1" idx="$2" + local log="${3:-${LOG_FILE:-/dev/null}}" + [[ -n "$vmid" && -n "$idx" ]] || return 1 + qm set "$vmid" --delete "hostpci${idx}" >>"$log" 2>&1 +} + +# Robust LXC stop for switch-mode / passthrough flows. +# +# A plain `pct stop` can hang indefinitely when: +# - the container has a stale lock from a previous aborted operation, +# - processes inside the container (Plex, Jellyfin, databases) ignore +# the initial TERM and sit in uninterruptible-sleep (D state) while +# the GPU they were using is being yanked out, +# - the host is under load and Proxmox's state polling stalls, +# - `pct shutdown --timeout` is not always enforced by pct itself +# (observed field reports of 5+ min waits despite --timeout 30). +# +# Strategy: +# 1) return 0 immediately if the container is not running, +# 2) clear any stale lock (most common cause of hangs), +# 3) try `pct shutdown --forceStop 1 --timeout 30`, wrapped in an +# external `timeout 45` as belt-and-braces in case pct itself +# blocks on backend I/O, +# 4) verify actual status via `pct status` — do not trust exit codes, +# pct can return non-zero while the container is actually stopped, +# 5) if still running, fall back to `pct stop` wrapped in `timeout 60`, +# 6) verify again and return 1 if the container is truly stuck +# (only happens when processes are in D state — requires manual +# intervention, but the wizard moves on instead of hanging). +# +# Usage: _pmx_stop_lxc [log_file] +# log_file defaults to $LOG_FILE if set, otherwise /dev/null. +# Returns 0 on stopped / already-stopped, non-zero if every attempt failed. +function _pmx_stop_lxc() { + local ctid="$1" + local log="${2:-${LOG_FILE:-/dev/null}}" + + _pmx_lxc_running() { + pct status "$1" 2>/dev/null | grep -q "status: running" + } + + _pmx_lxc_running "$ctid" || return 0 + + # Best-effort unlock — silent on failure because most containers aren't + # actually locked; we only care about the cases where they are. + pct unlock "$ctid" >>"$log" 2>&1 || true + + # Graceful shutdown with forced kill after 30 s. The external `timeout 45` + # guarantees we never wait longer than that for this step, even if pct + # itself is stuck (the cushion over 30 s is to let the internal timeout + # cleanly unwind before we kill pct). + timeout 45 pct shutdown "$ctid" --forceStop 1 --timeout 30 >>"$log" 2>&1 || true + sleep 1 + _pmx_lxc_running "$ctid" || return 0 + + # Fallback: abrupt stop, also externally capped so the wizard does not + # hang the user indefinitely if lxc-stop blocks on D-state processes. + timeout 60 pct stop "$ctid" >>"$log" 2>&1 || true + sleep 1 + _pmx_lxc_running "$ctid" || return 0 + + return 1 +} + function _pci_next_hostpci_index() { local vmid="$1" local idx=0 diff --git a/scripts/gpu_tpu/add_gpu_vm.sh b/scripts/gpu_tpu/add_gpu_vm.sh index 16608435..19906686 100644 --- a/scripts/gpu_tpu/add_gpu_vm.sh +++ b/scripts/gpu_tpu/add_gpu_vm.sh @@ -71,6 +71,7 @@ SELECTED_GPU_NAME="" declare -a IOMMU_DEVICES=() # all PCI addrs in IOMMU group (endpoint devices) declare -a IOMMU_VFIO_IDS=() # vendor:device for vfio-pci ids= declare -a EXTRA_AUDIO_DEVICES=() # sibling audio function(s), typically *.1 +declare -a EXTRA_AUDIO_INFO=() # parallel to EXTRA_AUDIO_DEVICES — "BDF|current_driver" pairs for the summary dialog IOMMU_GROUP="" IOMMU_PENDING_REBOOT=false @@ -212,28 +213,32 @@ _strip_colors() { printf '%s' "$1" | sed 's/\\Z[0-9a-zA-Z]//g' } -# Msgbox: dialog in standalone mode, whiptail in wizard mode +# Msgbox: dialog in standalone mode, whiptail in wizard mode. +# I/O pinned to /dev/tty so the dialog renders reliably regardless of +# how the caller redirected stdin/stdout, and immune to the SIGTTOU +# trap that fires when this script is resumed as a background job. _pmx_msgbox() { local title="$1" msg="$2" h="${3:-10}" w="${4:-72}" if [[ "$WIZARD_CALL" == "true" ]]; then whiptail --backtitle "ProxMenux" --title "$title" \ - --msgbox "$(_strip_colors "$msg")" "$h" "$w" + --msgbox "$(_strip_colors "$msg")" "$h" "$w" < /dev/tty > /dev/tty else dialog --backtitle "ProxMenux" --colors \ - --title "$title" --msgbox "$msg" "$h" "$w" + --title "$title" --msgbox "$msg" "$h" "$w" < /dev/tty > /dev/tty fi } -# Yesno: dialog in standalone mode, whiptail in wizard mode -# Returns 0 for yes, 1 for no (same as dialog/whiptail) +# Yesno: dialog in standalone mode, whiptail in wizard mode. +# Returns 0 for yes, 1 for no (same as dialog/whiptail). +# I/O pinned to /dev/tty — see the note on _pmx_msgbox. _pmx_yesno() { local title="$1" msg="$2" h="${3:-10}" w="${4:-72}" if [[ "$WIZARD_CALL" == "true" ]]; then whiptail --backtitle "ProxMenux" --title "$title" \ - --yesno "$(_strip_colors "$msg")" "$h" "$w" + --yesno "$(_strip_colors "$msg")" "$h" "$w" < /dev/tty > /dev/tty else dialog --backtitle "ProxMenux" --colors \ - --title "$title" --yesno "$msg" "$h" "$w" + --title "$title" --yesno "$msg" "$h" "$w" < /dev/tty > /dev/tty fi return $? } @@ -265,6 +270,27 @@ _pmx_menu() { return $? } +# Checklist: dialog in standalone mode, whiptail in wizard mode. +# Usage: _pmx_checklist title msg h w list_h tag1 desc1 state1 tag2 desc2 state2 ... +# state is "on" or "off". Returns the space-separated list of selected +# tags on stdout (one line). Returns non-zero if the user cancels. +_pmx_checklist() { + local title="$1" msg="$2" h="$3" w="$4" lh="$5" + shift 5 + if [[ "$WIZARD_CALL" == "true" ]]; then + whiptail --backtitle "ProxMenux" \ + --title "$title" \ + --checklist "$(_strip_colors "$msg")" "$h" "$w" "$lh" \ + "$@" 3>&1 1>&2 2>&3 + else + dialog --backtitle "ProxMenux" --colors \ + --title "$title" \ + --checklist "$msg" "$h" "$w" "$lh" \ + "$@" 2>&1 >/dev/tty + fi + return $? +} + _file_has_exact_line() { local line="$1" local file="$2" @@ -1109,30 +1135,39 @@ analyze_iommu_group() { } -detect_optional_gpu_audio() { - EXTRA_AUDIO_DEVICES=() - - local sibling_audio="${SELECTED_GPU_PCI%.*}.1" - local dev_path="/sys/bus/pci/devices/${sibling_audio}" - [[ -d "$dev_path" ]] || return 0 - +# Returns 0 if the BDF at $1 is a real PCI audio device (class 04xx). +_pci_is_audio_device() { + local bdf="$1" + [[ -n "$bdf" ]] || return 1 + local dev_path="/sys/bus/pci/devices/${bdf}" + [[ -d "$dev_path" ]] || return 1 local class_hex class_hex=$(cat "${dev_path}/class" 2>/dev/null | sed 's/^0x//') - [[ "${class_hex:0:2}" == "04" ]] || return 0 + [[ "${class_hex:0:2}" == "04" ]] +} - local already_in_group=false dev +# Registers an audio BDF for passthrough alongside the GPU. +# Idempotent: skips if the BDF was already recorded by analyze_iommu_group +# (IOMMU_DEVICES) or by a previous call here (EXTRA_AUDIO_DEVICES). +# Updates EXTRA_AUDIO_DEVICES, EXTRA_AUDIO_INFO, and IOMMU_VFIO_IDS. +_register_gpu_audio_device() { + local bdf="$1" + [[ -n "$bdf" ]] || return 1 + local dev_path="/sys/bus/pci/devices/${bdf}" + [[ -d "$dev_path" ]] || return 1 + + local dev for dev in "${IOMMU_DEVICES[@]}"; do - if [[ "$dev" == "$sibling_audio" ]]; then - already_in_group=true - break - fi + [[ "$dev" == "$bdf" ]] && return 0 + done + for dev in "${EXTRA_AUDIO_DEVICES[@]}"; do + [[ "$dev" == "$bdf" ]] && return 0 done - if [[ "$already_in_group" == "true" ]]; then - return 0 - fi - - EXTRA_AUDIO_DEVICES+=("$sibling_audio") + EXTRA_AUDIO_DEVICES+=("$bdf") + local drv + drv=$(_get_pci_driver "$bdf") + EXTRA_AUDIO_INFO+=("${bdf}|${drv}") local vid did new_id vid=$(cat "${dev_path}/vendor" 2>/dev/null | sed 's/0x//') @@ -1143,6 +1178,98 @@ detect_optional_gpu_audio() { IOMMU_VFIO_IDS+=("$new_id") fi fi + return 0 +} + +# Scans the host for all class-04 PCI audio devices and lets the user +# pick which ones to pass to the VM. Only invoked when the selected GPU +# has no .1 sibling audio function — the dGPU fast path continues to +# auto-include that sibling without prompting. +# +# Devices already in the GPU's IOMMU group are excluded from the list +# (analyze_iommu_group has already queued them). The checklist defaults +# to all-OFF so nothing gets passed through silently. +_prompt_user_for_audio_devices() { + # Collect eligible audio BDFs from sysfs. + local -a candidates=() + local dev_path bdf + for dev_path in /sys/bus/pci/devices/*; do + [[ -d "$dev_path" ]] || continue + bdf=$(basename "$dev_path") + _pci_is_audio_device "$bdf" || continue + # Skip ones already queued by the IOMMU group sweep. + local skip=false dev + for dev in "${IOMMU_DEVICES[@]}"; do + [[ "$dev" == "$bdf" ]] && { skip=true; break; } + done + $skip && continue + candidates+=("$bdf") + done + + [[ ${#candidates[@]} -eq 0 ]] && return 0 + + # Build checklist items: tag=BDF, description=" (driver: X)". + local -a items=() + local name drv label + for bdf in "${candidates[@]}"; do + name=$(lspci -nn -s "${bdf#0000:}" 2>/dev/null \ + | sed 's/^[^ ]* //' \ + | sed 's/ \[0401\]//; s/ \[0403\]//; s/ \[0400\]//' \ + | cut -c1-52) + [[ -z "$name" ]] && name="PCI audio" + drv=$(_get_pci_driver "$bdf") + label="${name} (driver: ${drv})" + items+=("$bdf" "$label" "off") + done + + local prompt selection dialog_h list_h + prompt="$(translate 'The selected GPU has no dedicated .1 audio sibling function.')\n" + prompt+="$(translate 'If you want HDMI/analog audio inside the VM, select the audio controller(s) to pass through along with the GPU.')\n\n" + prompt+="$(translate 'Default is none (video-only passthrough). Use SPACE to toggle selections.')" + + # Give the list area a floor of 4 rows so a single candidate doesn't + # render cramped under the description. Overall dialog height scales + # with that floor + room for the 4-line prompt, blank line, borders + # and button row. + list_h=${#candidates[@]} + (( list_h < 4 )) && list_h=4 + dialog_h=$(( list_h + 14 )) + + selection=$(_pmx_checklist \ + "$(translate 'Add Audio Passthrough')" \ + "$prompt" \ + "$dialog_h" 82 "$list_h" \ + "${items[@]}") || return 0 + + # dialog wraps selected tags in quotes, whiptail does not — _strip them. + selection=$(echo "$selection" | tr -d '"') + [[ -z "$selection" ]] && return 0 + + local picked + for picked in $selection; do + _register_gpu_audio_device "$picked" + done +} + +detect_optional_gpu_audio() { + EXTRA_AUDIO_DEVICES=() + EXTRA_AUDIO_INFO=() + + # Fast path: dGPUs (NVIDIA / AMD discrete) and some APUs expose audio + # as function .1 of the same slot. When present, auto-include it — + # this is the unambiguous, always-safe case because such audio only + # outputs through the GPU's own ports and was never used by the host. + local sibling_audio="${SELECTED_GPU_PCI%.*}.1" + if _pci_is_audio_device "$sibling_audio"; then + _register_gpu_audio_device "$sibling_audio" + return 0 + fi + + # Slow path: no sibling audio (typical for Intel iGPUs whose HDMI + # audio lives on the PCH, or setups with an external sound card). + # Ask the user explicitly via checklist — the decision of whether to + # pass chipset audio alongside an iGPU is intentional, not automatic. + _prompt_user_for_audio_devices } @@ -1417,8 +1544,19 @@ confirm_summary() { else msg+=" • $(translate 'hostpci entries for all IOMMU group devices')\n" fi - [[ ${#EXTRA_AUDIO_DEVICES[@]} -gt 0 ]] && \ - msg+=" • $(translate 'Additional GPU audio function will be added'): ${EXTRA_AUDIO_DEVICES[*]}\n" + if [[ ${#EXTRA_AUDIO_DEVICES[@]} -gt 0 ]]; then + msg+=" • $(translate 'Additional audio function(s) to be added'):\n" + local _audio_info _audio_bdf _audio_drv + for _audio_info in "${EXTRA_AUDIO_INFO[@]}"; do + _audio_bdf="${_audio_info%%|*}" + _audio_drv="${_audio_info#*|}" + if [[ -n "$_audio_drv" && "$_audio_drv" != "none" && "$_audio_drv" != "vfio-pci" ]]; then + msg+=" • ${_audio_bdf} \Zb(${_audio_drv})\Zn\n" + else + msg+=" • ${_audio_bdf}\n" + fi + done + fi [[ "$SELECTED_GPU" == "nvidia" ]] && \ msg+=" • $(translate 'NVIDIA KVM hiding (cpu hidden=1)')\n" if [[ "$SWITCH_FROM_LXC" == "true" ]]; then @@ -1740,7 +1878,7 @@ cleanup_lxc_configs() { [[ "$SWITCH_FROM_LXC" != "true" ]] && return 0 [[ ${#LXC_AFFECTED_CTIDS[@]} -eq 0 ]] && return 0 - msg_info "$(translate 'Applying selected LXC switch action...')" + msg_info2 "$(translate 'Applying selected LXC switch action')" local i for i in "${!LXC_AFFECTED_CTIDS[@]}"; do @@ -1750,7 +1888,11 @@ cleanup_lxc_configs() { if [[ "${LXC_AFFECTED_RUNNING[$i]}" == "1" ]]; then msg_info "$(translate 'Stopping LXC') ${ctid}..." - if pct stop "$ctid" >>"$LOG_FILE" 2>&1; then + # _pmx_stop_lxc: graceful shutdown with forceStop+timeout, then + # fallback to pct stop. Avoids the indefinite hang that raw + # `pct stop` produces when the container is locked or has + # unresponsive processes (Plex, databases, etc.). + if _pmx_stop_lxc "$ctid" "$LOG_FILE"; then msg_ok "$(translate 'LXC stopped') ${ctid}" | tee -a "$screen_capture" else msg_warn "$(translate 'Could not stop LXC') ${ctid}" | tee -a "$screen_capture" @@ -1807,8 +1949,73 @@ cleanup_vm_config() { local src_conf="/etc/pve/qemu-server/${SWITCH_VM_SRC}.conf" if [[ -f "$src_conf" ]]; then msg_info "$(translate 'Removing GPU from VM') ${SWITCH_VM_SRC}..." - sed -i "/^hostpci[0-9]\+:.*${pci_slot}/d" "$src_conf" + # Precise regex: slot must be followed by "." and a + # delimiter. Kept in sync with switch_gpu_mode.sh. A looser + # ".*${pci_slot}" would match the slot as a substring and wipe + # unrelated hostpci entries (e.g. slot "00:02" matching inside + # a dGPU BDF 0000:02:00.0). + sed -E -i "/^hostpci[0-9]+:[[:space:]]*(0000:)?${pci_slot}\.[0-7]([,[:space:]]|$)/d" "$src_conf" msg_ok "$(translate 'GPU removed from VM') ${SWITCH_VM_SRC}" | tee -a "$screen_capture" + + # Cascade cleanup: detect audio companions orphaned in the + # source VM after the GPU slot is removed. Typical case: the + # source VM had an Intel iGPU at 00:02.0 paired with chipset + # audio at 00:1f.3 via the Part 1 checklist — the sed above + # only strips 00:02.* entries, leaving the chipset audio + # hostpci pointing at a device the source VM no longer uses. + # + # Unlike switch_gpu_mode (detach flow), we deliberately do NOT + # touch /etc/modprobe.d/vfio.conf here. The GPU is being moved + # to the current target VM, which may select the same audio + # companion in its own Part 1 checklist. Any vendor:device + # orphaned in vfio.conf after this move is inert — the user + # can clean it up later via switch_gpu_mode if they want. + if declare -F _vm_list_orphan_audio_hostpci >/dev/null 2>&1; then + local _orphan_audio + _orphan_audio=$(_vm_list_orphan_audio_hostpci "$SWITCH_VM_SRC" "$pci_slot") + if [[ -n "$_orphan_audio" ]]; then + local -a _orph_items=() + local _oline _o_idx _o_bdf _o_name + while IFS= read -r _oline; do + [[ -z "$_oline" ]] && continue + _o_idx="${_oline%%|*}" + _oline="${_oline#*|}" + _o_bdf="${_oline%%|*}" + _o_name="${_oline#*|}" + _orph_items+=("$_o_idx" "${_o_bdf} ${_o_name}" "on") + done <<< "$_orphan_audio" + + local _prompt + _prompt="\n$(translate 'The GPU has been moved out of VM') \Zb${SWITCH_VM_SRC}\Zn.\n\n" + _prompt+="$(translate 'The source VM also has these audio devices, likely added together with the GPU. Remove them too?')\n\n" + _prompt+="$(translate '(Checked entries will be removed. Uncheck to keep in VM.)')" + + local _selected + _selected=$(_pmx_checklist \ + "$(translate 'Associated Audio Devices')" \ + "$_prompt" \ + 20 84 "$(( ${#_orph_items[@]} / 3 ))" \ + "${_orph_items[@]}") || _selected="" + _selected=$(echo "$_selected" | tr -d '"') + + local _sel _removed="" + for _sel in $_selected; do + if declare -F _vm_remove_hostpci_index >/dev/null 2>&1; then + _vm_remove_hostpci_index "$SWITCH_VM_SRC" "$_sel" "$LOG_FILE" \ + && _removed+=" hostpci${_sel}" + else + qm set "$SWITCH_VM_SRC" --delete "hostpci${_sel}" >>"$LOG_FILE" 2>&1 \ + && _removed+=" hostpci${_sel}" + fi + done + if [[ -n "$_removed" ]]; then + show_proxmenux_logo + msg_title "${run_title}" + msg_ok "$(translate 'Associated audio removed from VM'): ${SWITCH_VM_SRC} —${_removed}" \ + | tee -a "$screen_capture" + fi + fi + fi fi } @@ -2068,10 +2275,23 @@ main() { rm -f "$screen_capture" + # Final reboot prompt. Whiptail is invoked directly (not through + # the _pmx_yesno helper) because the ProxMenux menu chain + # (menu → main_menu → hw_grafics_menu → add_gpu_vm) has been + # verified to work reliably with a bare whiptail here, while the + # dialog-based helper path hits process-group / TTY edge cases in + # that exact chain. + # + # The extra `Press Enter to continue ... read -r` between whiptail + # and `reboot` is deliberate — it gives the user a visible pause + # after the dialog closes so an accidental Enter on the yes button + # cannot trigger an immediate reboot. if [[ "$HOST_CONFIG_CHANGED" == "true" ]]; then whiptail --title "$(translate 'Reboot Required')" \ --yesno "$(translate 'A reboot is required for VFIO binding to take effect. Do you want to restart now?')" 10 68 if [[ $? -eq 0 ]]; then + msg_success "$(translate 'Press Enter to continue...')" + read -r msg_warn "$(translate 'Rebooting the system...')" reboot else diff --git a/scripts/gpu_tpu/switch_gpu_mode.sh b/scripts/gpu_tpu/switch_gpu_mode.sh index 44ea8928..6a98a95a 100644 --- a/scripts/gpu_tpu/switch_gpu_mode.sh +++ b/scripts/gpu_tpu/switch_gpu_mode.sh @@ -835,8 +835,14 @@ apply_lxc_action_for_vm_mode() { if [[ "${LXC_AFFECTED_RUNNING[$i]}" == "1" ]]; then msg_info "$(translate 'Stopping LXC') ${ctid}..." - pct stop "$ctid" >>"$LOG_FILE" 2>&1 || true - msg_ok "$(translate 'LXC stopped') ${ctid}" | tee -a "$screen_capture" + # _pmx_stop_lxc: unlock + graceful shutdown with forceStop+timeout, + # fallback to pct stop. Prevents the indefinite hang that raw + # `pct stop` triggers on locked / stuck containers. + if _pmx_stop_lxc "$ctid" "$LOG_FILE"; then + msg_ok "$(translate 'LXC stopped') ${ctid}" | tee -a "$screen_capture" + else + msg_warn "$(translate 'Could not stop LXC') ${ctid}" | tee -a "$screen_capture" + fi fi if [[ "$LXC_ACTION" == "keep_gpu_disable_onboot" && "${LXC_AFFECTED_ONBOOT[$i]}" == "1" ]]; then @@ -948,11 +954,102 @@ apply_vm_action_for_lxc_mode() { fi if [[ "$VM_ACTION" == "remove_gpu_keep_onboot" && -f "$conf" ]]; then + # Primary cleanup: strip hostpci lines whose BDF matches any of + # the GPU's selected slots. Matches both the PF function (.0) and + # any sibling audio or HDMI codec that shares the slot (typical + # for discrete NVIDIA/AMD cards where .1 is the HDMI audio). + # + # Precise regex: the slot must be followed by "." and + # either a delimiter or end-of-line. A looser ".*${slot}" would + # match by pure substring and delete unrelated hostpci entries — + # e.g. slot "00:02" would match inside "0000:02:00.0" (a dGPU at + # 02:00) and wipe both the iGPU and the unrelated dGPU. local slot for slot in "${SELECTED_PCI_SLOTS[@]}"; do - sed -i "/^hostpci[0-9]\+:.*${slot}/d" "$conf" + sed -E -i "/^hostpci[0-9]+:[[:space:]]*(0000:)?${slot}\.[0-7]([,[:space:]]|$)/d" "$conf" done msg_ok "$(translate 'GPU removed from VM config') ${vmid}" | tee -a "$screen_capture" + + # Cascade cleanup: Intel iGPU passthrough typically pairs the GPU + # at 00:02.0 with chipset audio at 00:1f.3, which lives at a + # different slot and therefore survives the sed above. If it + # stays in the VM config after the GPU is gone, the VM either + # fails to start (vfio-pci no longer claims 8086:51c8 after the + # switch-back) or it steals host audio unnecessarily. Enumerate + # orphan audio hostpci entries and ask the user what to do. + if declare -F _vm_list_orphan_audio_hostpci >/dev/null 2>&1; then + local _orphan_audio + _orphan_audio=$(_vm_list_orphan_audio_hostpci "$vmid" "${SELECTED_PCI_SLOTS[0]}") + if [[ -n "$_orphan_audio" ]]; then + local -a _orph_items=() + local _line _o_idx _o_bdf _o_name + while IFS= read -r _line; do + [[ -z "$_line" ]] && continue + _o_idx="${_line%%|*}" + _line="${_line#*|}" + _o_bdf="${_line%%|*}" + _o_name="${_line#*|}" + _orph_items+=("$_o_idx" "${_o_bdf} ${_o_name}" "on") + done <<< "$_orphan_audio" + + local _prompt _selected + _prompt="\n$(translate 'The GPU is being detached from VM') \Zb${vmid}\Zn.\n\n" + _prompt+="$(translate 'The VM also has these audio devices assigned via PCI passthrough — typically added together with the GPU. Remove them too?')\n\n" + _prompt+="$(translate '(Checked entries will be removed. Uncheck to keep in VM.)')" + + _selected=$(dialog --backtitle "ProxMenux" --colors \ + --title "$(translate 'Associated Audio Devices')" \ + --checklist "$_prompt" 20 84 "$(( ${#_orph_items[@]} / 3 ))" \ + "${_orph_items[@]}" \ + 2>&1 >/dev/tty) || _selected="" + _selected=$(echo "$_selected" | tr -d '"') + + # Cross-reference table so we can recover each selected idx's + # original BDF (we need it for vendor:device lookup below). + declare -A _orphan_bdf_by_idx=() + local _o_line _o_i _o_b + while IFS= read -r _o_line; do + [[ -z "$_o_line" ]] && continue + _o_i="${_o_line%%|*}" + _o_line="${_o_line#*|}" + _o_b="${_o_line%%|*}" + _orphan_bdf_by_idx["$_o_i"]="$_o_b" + done <<< "$_orphan_audio" + + local _sel _removed_audio="" _rem_bdf _vd_hex _dd_hex _vd_id + for _sel in $_selected; do + _rem_bdf="${_orphan_bdf_by_idx[$_sel]:-}" + if _vm_remove_hostpci_index "$vmid" "$_sel" "$LOG_FILE"; then + _removed_audio+=" hostpci${_sel}" + + # Fix B: if the removed audio BDF is not referenced by any + # OTHER VM, its vendor:device can safely come out of + # /etc/modprobe.d/vfio.conf too. Without this step, + # SELECTED_IOMMU_IDS only held the GPU's own IOMMU group + # (e.g. 8086:46a3 for Intel iGPU) and the companion audio + # id (e.g. 8086:51c8 for chipset audio) survived in + # vfio.conf, so vfio-pci kept claiming it at next boot + # even though nothing used it. + [[ -z "$_rem_bdf" ]] && continue + if ! _pci_bdf_in_any_vm "$_rem_bdf" "${VM_AFFECTED_IDS[@]}"; then + _vd_hex=$(cat "/sys/bus/pci/devices/${_rem_bdf}/vendor" 2>/dev/null | sed 's/^0x//') + _dd_hex=$(cat "/sys/bus/pci/devices/${_rem_bdf}/device" 2>/dev/null | sed 's/^0x//') + if [[ -n "$_vd_hex" && -n "$_dd_hex" ]]; then + _vd_id="${_vd_hex}:${_dd_hex}" + if ! _contains_in_array "$_vd_id" "${SELECTED_IOMMU_IDS[@]}"; then + SELECTED_IOMMU_IDS+=("$_vd_id") + fi + fi + fi + fi + done + unset _orphan_bdf_by_idx + if [[ -n "$_removed_audio" ]]; then + msg_ok "$(translate 'Associated audio removed from VM'): ${_removed_audio# }" \ + | tee -a "$screen_capture" + fi + fi + fi fi done } diff --git a/scripts/gpu_tpu/switch_gpu_mode_direct.sh b/scripts/gpu_tpu/switch_gpu_mode_direct.sh index abd6456c..20e9ce17 100644 --- a/scripts/gpu_tpu/switch_gpu_mode_direct.sh +++ b/scripts/gpu_tpu/switch_gpu_mode_direct.sh @@ -748,8 +748,14 @@ apply_lxc_action_for_vm_mode() { if [[ "${LXC_AFFECTED_RUNNING[$i]}" == "1" ]]; then msg_info "$(translate 'Stopping LXC') ${ctid}..." - pct stop "$ctid" >>"$LOG_FILE" 2>&1 || true - msg_ok "$(translate 'LXC stopped') ${ctid}" | tee -a "$screen_capture" + # _pmx_stop_lxc: unlock + graceful shutdown with forceStop+timeout, + # fallback to pct stop. Prevents the indefinite hang that raw + # `pct stop` triggers on locked / stuck containers. + if _pmx_stop_lxc "$ctid" "$LOG_FILE"; then + msg_ok "$(translate 'LXC stopped') ${ctid}" | tee -a "$screen_capture" + else + msg_warn "$(translate 'Could not stop LXC') ${ctid}" | tee -a "$screen_capture" + fi fi if [[ "$LXC_ACTION" == "keep_gpu_disable_onboot" && "${LXC_AFFECTED_ONBOOT[$i]}" == "1" ]]; then @@ -865,11 +871,67 @@ apply_vm_action_for_lxc_mode() { fi if [[ "$VM_ACTION" == "remove_gpu_keep_onboot" && -f "$conf" ]]; then + # Primary cleanup: strip hostpci lines whose BDF matches any of + # the GPU's selected slots. Matches both the PF function (.0) and + # sibling audio/HDMI codecs (.1, typical for discrete cards). + # + # Precise regex: the slot must be followed by "." and a + # delimiter. Kept in sync with switch_gpu_mode.sh — a looser + # substring match would wipe unrelated hostpci entries (e.g. slot + # "00:02" matching as a substring inside a dGPU BDF 0000:02:00.0). local slot for slot in "${SELECTED_PCI_SLOTS[@]}"; do - sed -i "/^hostpci[0-9]\+:.*${slot}/d" "$conf" + sed -E -i "/^hostpci[0-9]+:[[:space:]]*(0000:)?${slot}\.[0-7]([,[:space:]]|$)/d" "$conf" done msg_ok "$(translate 'GPU removed from VM config') ${vmid}" | tee -a "$screen_capture" + + # Cascade cleanup for the web flow: auto-remove any PCI audio + # hostpci entries at a slot DIFFERENT from the GPU (typical Intel + # iGPU case where 00:1f.3 chipset audio was paired with the iGPU + # at 00:02.0). The helper skips audio devices whose slot already + # has a display sibling in the same VM (HDMI codec of another + # still-present dGPU), so those are not touched. The web runner + # has no good way to render a multi-select checklist, so the + # eligible ones are auto-removed and reported verbatim in the log. + if declare -F _vm_list_orphan_audio_hostpci >/dev/null 2>&1; then + local _orphan_audio _line _o_idx _o_bdf _o_name _removed="" + local _vd_hex _dd_hex _vd_id + _orphan_audio=$(_vm_list_orphan_audio_hostpci "$vmid" "${SELECTED_PCI_SLOTS[0]}") + if [[ -n "$_orphan_audio" ]]; then + while IFS= read -r _line; do + [[ -z "$_line" ]] && continue + _o_idx="${_line%%|*}" + _line="${_line#*|}" + _o_bdf="${_line%%|*}" + _o_name="${_line#*|}" + if _vm_remove_hostpci_index "$vmid" "$_o_idx" "$LOG_FILE"; then + _removed+=" • hostpci${_o_idx}: ${_o_bdf} ${_o_name}\n" + + # Fix B: also surface the audio's vendor:device to the + # upcoming vfio.conf cleanup if no other VM still uses + # this BDF. Ensures e.g. 8086:51c8 (Intel chipset audio) + # is stripped from /etc/modprobe.d/vfio.conf when the + # iGPU it was paired with leaves VM mode. + if declare -F _pci_bdf_in_any_vm >/dev/null 2>&1 \ + && ! _pci_bdf_in_any_vm "$_o_bdf" "${VM_AFFECTED_IDS[@]}"; then + _vd_hex=$(cat "/sys/bus/pci/devices/${_o_bdf}/vendor" 2>/dev/null | sed 's/^0x//') + _dd_hex=$(cat "/sys/bus/pci/devices/${_o_bdf}/device" 2>/dev/null | sed 's/^0x//') + if [[ -n "$_vd_hex" && -n "$_dd_hex" ]]; then + _vd_id="${_vd_hex}:${_dd_hex}" + if ! _contains_in_array "$_vd_id" "${SELECTED_IOMMU_IDS[@]}"; then + SELECTED_IOMMU_IDS+=("$_vd_id") + fi + fi + fi + fi + done <<< "$_orphan_audio" + if [[ -n "$_removed" ]]; then + msg_ok "$(translate 'Associated audio removed from VM'): ${vmid}" \ + | tee -a "$screen_capture" + echo -e "$_removed" | tee -a "$screen_capture" + fi + fi + fi fi done }