diff --git a/images/riov-indicator.png b/images/riov-indicator.png new file mode 100644 index 00000000..a1af122d Binary files /dev/null and b/images/riov-indicator.png differ 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 }