Hi-jack an Unraid server power button to initiate S3 sleep

Repurpose the Unraid power button to gracefully stop GPU passthrough VMs and put the server into S3 sleep, with clean GPU re-initialization after wake.

I run an Unraid server with a CachyOS virtual machine that has my GPU, SSD, and USB controller passed through. I wanted a simple way to put the whole system to sleep just by pressing the power button. The catch is that I can't just sleep the host directly. If I do, the VM comes back with a black screen on wake, because the host can't properly send the PCI reset signal to the passed-through GPU. So instead, I had to override the power button's behavior to gracefully shut down any running VMs first, then put the system to sleep. This lets the GPU be cleanly reinitialized after resume.

Prerequisites

Initialization

  1. Log into the Unraid GUI.
  2. Navigate to Settings > User Scripts.

Create the "Update ACPI Handler" script

  1. Click Add new script.
  2. Type update_acpi_handler, click OK.
  3. Click the Cog next to update_acpi_handler, click Edit Name.
  4. Type Update ACPI Handler, click the check mark.
  5. Click the Cog next to Update ACPI Handler, click Edit Description.
  6. Type Repurpose the power button to stop running VMs and put the system to sleep. The power button can be used to wake the system and start VMs set to autostart., click the check mark.
  7. Click the Cog next to Update ACPI Handler, click Update Script, type:
#!/bin/bash

sed -i -e \
    '/power) \/sbin\/init 0/c\
      power)\
        case "$3" in\
          PBTN) bash /boot/config/plugins/user.scripts/scripts/acpi_handler_override/script ;;\
          LNXPWRBN:00) logger -t "acpi_handler" "Ignoring Linux Power Button virtual event" ;;\
          *) logger -t "acpi_handler" "ACPI group $1 / action $2 / event $3 is not defined" ;;\
        esac' \
/etc/acpi/acpi_handler.sh
/etc/rc.d/rc.acpid restart

mkdir -p /etc/elogind/logind.conf.d

    # sed -i -e '/#HandlePowerKey=poweroff/aHandlePowerKey=ignore' /etc/elogind/logind.conf
    echo -e '[Login]\nHandlePowerKey=ignore' > /etc/elogind/logind.conf.d/90-powerkey.conf

/etc/rc.d/rc.elogind restart
  1. Click Save Changes.
  2. Click the drop-down next to Update ACPI Handler, select At First Array Start Only.
  3. Click Run Script, then Done.

Create the "ACPI Handler Override" script

  1. Click Add new script.
  2. Type acpi_handler_override, click OK.
  3. Click the Cog next to acpi_handler_override, click Edit Name.
  4. Type ACPI Handler Override, click the check mark.
  5. Click the Cog next to ACPI Handler Override, click Edit Description.
  6. Type Invoked by /etc/acpi/acpi_handler.sh, click the check mark.
  7. Click the Cog next to ACPI Handler Override, click Update Script, type:
#!/bin/bash

script="s3_sleep"
plugin="dynamix.s3.sleep"

config="/boot/config/plugins/$plugin/$plugin.cfg"

if [ ! -e $config ]; then
    config="/usr/local/emhttp/plugins/$plugin/default.cfg"
fi

execute="/usr/local/emhttp/plugins/$plugin/scripts/$script"
options=$(grep '^options=' $config | cut -d '"' -f2 | tr -d '\n')

$execute -S $options  # use WoL, Pre-Run, Post-Run, and Debug from config
  1. Click Save Changes.

Create the "Stop Running Virtual Machines" script

  1. Click Add new script.
  2. Type stop_running_vms, click OK.
  3. Click the Cog next to stop_running_vms, click Edit Name.
  4. Type Stop Running Virtual Machines, click the check mark.
  5. Click the Cog next to Stop Running Virtual Machines, click Edit Description.
  6. Type Invoked by /boot/config/plugins/user.scripts/scripts/acpi_handler_override, click the check mark.
  7. Click the Cog next to Stop Running Virtual Machines, click Update Script, type:
#!/bin/bash

# Configuration
TIMEOUT=120  # Seconds to wait for graceful shutdown before forcing kill

logger -t "stop_running_vms" "=== Starting VM Shutdown Sequence ==="
logger -t "stop_running_vms" "Timestamp: $(date)"

# Get list of running VMs
RUNNING_VMS=$(virsh list --name --state-running)

if [ -z "$RUNNING_VMS" ]; then
    logger -t "stop_running_vms" "No VMs are currently running."
    exit 0
fi

logger -t "stop_running_vms" "Found running VMs:"
logger -t "stop_running_vms" "$RUNNING_VMS"
logger -t "stop_running_vms" "========================================"

# Loop through each running VM
while IFS= read -r vm_name; do
    if [ -z "$vm_name" ]; then continue; fi

    logger -t "stop_running_vms" "Attempting graceful shutdown for: $vm_name"

    # Send ACPI shutdown signal (graceful)
    virsh shutdown "$vm_name"

    # Wait for the VM to stop
    START_TIME=$(date +%s)
    while true; do
        CURRENT_STATE=$(virsh domstate "$vm_name" 2>/dev/null)

        # Check if VM is dead/shutdown
        if [[ "$CURRENT_STATE" == "shut off" ]] || [[ "$CURRENT_STATE" == "crashed" ]]; then
            logger -t "stop_running_vms" "  -> $vm_name stopped successfully."
            break
        fi

        # Check timeout
        CURRENT_TIME=$(date +%s)
        ELAPSED=$((CURRENT_TIME - START_TIME))

        if [ $ELAPSED -ge $TIMEOUT ]; then
            logger -t "stop_running_vms" "  -> Timeout reached ($TIMEOUT seconds). Force killing $vm_name..."
            virsh destroy "$vm_name"
            logger -t "stop_running_vms" "  -> $vm_name force killed."
            break
        fi

        sleep 5
    done
done <<< "$RUNNING_VMS"

logger -t "stop_running_vms" "========================================"
logger -t "stop_running_vms" "Shutdown sequence completed."
  1. Click Save Changes.

Create the "Stop Running GPU Passthrough VMs " script

  1. Click Add new script.
  2. Type stop_gpu_vms, click OK.
  3. Click the Cog next to stop_gpu_vms, click Edit Name.
  4. Type Stop Running GPU Passthrough VMs, click the check mark.
  5. Click the Cog next to Stop Running GPU Passthrough VMs, click Edit Description.
  6. Type Alternative to /boot/config/plugins/user.scripts/scripts/stop_running_vms, click the check mark.
  7. Click the Cog next to Stop Running GPU Passthrough VMs, click Update Script, type:
#!/bin/bash

# Configuration
SHUTDOWN_TIMEOUT=120 

echo "Starting GPU VM Shutdown Process..."
echo "-----------------------------------"

# Get list of all active (running) VMs
ACTIVE_VMS=$(virsh list --name --state-running)

if [ -z "$ACTIVE_VMS" ]; then
    echo "No VMs are currently running."
    exit 0
fi

for VM_NAME in $ACTIVE_VMS; do
    [ -z "$VM_NAME" ] && continue

    echo "Checking VM: $VM_NAME"

    # Fetch the XML definition of the VM
    VM_XML=$(virsh dumpxml "$VM_NAME")

    # Check for GPU Passthrough
    # Look for hostdev with vfio driver AND function='0x0' (main GPU) or function='0x1'/'0x6' (GPU audio)
    # Using grep -E for extended regex to match the actual XML structure
    if echo "$VM_XML" | grep -q "<hostdev mode='subsystem' type='pci'.*managed='yes'" && \
       echo "$VM_XML" | grep -q "<driver name='vfio'/>" && \
       echo "$VM_XML" | grep -E "function='0x0'" > /dev/null; then  # (0x0|0x1|0x6)

        echo "  -> GPU Passthrough detected."

        echo "  -> Initiating graceful shutdown for $VM_NAME..."
        virsh shutdown "$VM_NAME"

        # Wait for the VM to stop gracefully
        START_TIME=$(date +%s)
        while true; do
            CURRENT_STATUS=$(virsh domstate "$VM_NAME" 2>/dev/null)

            if [[ "$CURRENT_STATUS" != *"running"* ]]; then
                echo "  -> $VM_NAME has stopped."
                break
            fi

            CURRENT_TIME=$(date +%s)
            ELAPSED=$((CURRENT_TIME - START_TIME))

            if [ $ELAPSED -ge $SHUTDOWN_TIMEOUT ]; then
                echo "  -> Timeout reached ($SHUTDOWN_TIMEOUT s). Force stopping $VM_NAME..."
                virsh destroy "$VM_NAME"
                break
            fi

            sleep 5
        done
    else
        echo "  -> No GPU passthrough detected. Skipping."
    fi
done

echo "-----------------------------------"
echo "Process complete."
  1. Click Save.

Create the "Start Autostart Virtual Machines" script

  1. Click Add new script.
  2. Type start_autostart_vms, click OK.
  3. Click the Cog next to start_autostart_vms, click Edit Name.
  4. Type Start Autostart Virtual Machines, click the check mark.
  5. Click the Cog next to Start Autostart Virtual Machines, click Edit Description.
  6. Type Invoked by /boot/config/plugins/user.scripts/scripts/acpi_handler_override, click the check mark.
  7. Click the Cog next to Start Running Virtual Machines, click Update Script, type:
#!/bin/bash

# Configuration
STARTUP_DELAY=10
AUTOSTART_DIR="/etc/libvirt/qemu/autostart"

logger -t "start_autostart_vms" "=== Starting Auto-Start VM Sequence (Symlink Scan) ==="
logger -t "start_autostart_vms" "Timestamp: $(date)"

# Check if the autostart directory exists
if [ ! -d "$AUTOSTART_DIR" ]; then
    logger -t "start_autostart_vms" "Error: Autostart directory not found at $AUTOSTART_DIR"
    logger -t "start_autostart_vms" "Ensure 'Start on boot' is enabled for at least one VM in the Unraid GUI."
    exit 1
fi

logger -t "start_autostart_vms" "Scanning $AUTOSTART_DIR for autostart symlinks..."
logger -t "start_autostart_vms" "========================================"

# Check if there are any files in the directory
if [ -z "$(ls -A "$AUTOSTART_DIR")" ]; then
    logger -t "start_autostart_vms" "No VMs are configured for Auto Start (directory is empty)."
    exit 0
fi

# Loop through every file in the autostart directory
for link in "$AUTOSTART_DIR"/*; do
    # Skip if no files found (safety check)
    [ -e "$link" ] || continue

    # Get the filename (e.g., "MyVM.xml")
    filename=$(basename "$link")

    # Strip the .xml extension to get the VM name (e.g., "MyVM")
    # This handles cases where the name might have dots, but ends in .xml
    vm_name="${filename%.xml}"

    # Check current state
    STATE=$(virsh domstate "$vm_name" 2>/dev/null)

    # If already running, skip
    if [[ "$STATE" == "running" ]]; then
        logger -t "start_autostart_vms" "Skipping '$vm_name': Already running."
        continue
    fi

    # If the VM is not running, start it
    logger -t "start_autostart_vms" "Found Autostart VM (not running): $vm_name"
    logger -t "start_autostart_vms" "  -> Starting: $vm_name"

    if virsh start "$vm_name" > /dev/null 2>&1; then
        logger -t "start_autostart_vms" "  -> Command sent. Waiting for state change..."
        sleep 5

        NEW_STATE=$(virsh domstate "$vm_name" 2>/dev/null)
        if [[ "$NEW_STATE" == "running" ]]; then
            logger -t "start_autostart_vms" "  -> $vm_name is now running."
        else
            logger -t "start_autostart_vms" "  -> Warning: $vm_name failed to start (Current state: $NEW_STATE)"
        fi
    else
        logger -t "start_autostart_vms" "  -> Error: Failed to send start command for $vm_name."
    fi

    sleep $STARTUP_DELAY
done

logger -t "start_autostart_vms" "========================================"
logger -t "start_autostart_vms" "Auto-start sequence completed."
  1. Click Save Changes.

Configuration

Start Autostart Virtual Machines

  1. Log into the Unraid GUI.
  2. Navigate to Settings > Sleep Settings.
  3. Set Set WOL options before sleep to g to allow Wake-on-LAN (WoL).
  4. Set Custom commands before sleep to bash /boot/config/plugins/user.scripts/scripts/stop_running_vms/script.
  5. Set Custom commands after wake-up to bash /boot/config/plugins/user.scripts/scripts/start_autostart_vms/script.
  6. Set Enable DEBUG mode to Syslog.
  7. Click Apply.
  8. Click Done.