update pci_passthrough_helpers.sh

This commit is contained in:
MacRimi
2026-04-21 21:06:22 +02:00
parent 20c1140676
commit 77eb8c7b78
5 changed files with 614 additions and 36 deletions

View File

@@ -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 <vmid> <gpu_slot_base>
# 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 <bdf> [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 <vmid> <idx> [log_file]
# Removes hostpci<idx> 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 <ctid> [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

View File

@@ -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="<name> (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 ".<function>" 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

View File

@@ -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 ".<function>" 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
}

View File

@@ -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 ".<function>" 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
}