#!/bin/bash

#Usage: /usr/bin/qwdctl controls the availability of virtual machines for the qemu-web-desktop.
#
#  Each entry in the configuration file '/etc/qemu-web-desktop/machines.conf' 
#  spans on 1 to 3 lines:
#
#  -  [name.ext or URL or file path] 
#  -  url=[URL to ISO, QCOW2, VDI, VMDK, RAW, VHD/VHDX, QED virtual machine disk, optional]
#  -  description=[description to be shown in the service page] 
#
#  Images listed in the configuration file without a 'url=' parameter are
#  expected to be downloaded by hand and installed into
#  '/var/lib/qemu-web-desktop/machines' by the local administrator. Then, just 
#  specify the [name.ext] and description.
#  When the description is missing, the VM file name is used.
#  Additional machine descriptions can be given in '/etc/qemu-web-desktop/config.d/*.ini' files.
# 
# use: 'qwdctl help' to get help about usage and commands/options.

set -eo pipefail

bin=$(dirname $0)
p=$(basename $0)

if command -v brew 2>&1 >/dev/null; then
  # using brew / Apple Mac
  PREFIX=`brew --prefix`
  GREP=$PREFIX/opt/grep/libexec/gnubin/grep
  EDITOR=nano
else
  GREP=grep
fi

# check required binaries
for cmd in wget sed awk "$GREP" "${EDITOR:-editor}"; do
    command -v "$cmd" >/dev/null || { echo "$p: ERROR: $cmd not found"; exit 1; }
done

# After setting PREFIX
if [[ -z "$PREFIX" ]]; then
    PREFIX=""
elif [[ ! "$PREFIX" =~ ^/ ]]; then
    echo "$p: ERROR: Invalid PREFIX: $PREFIX" >&2
    exit 1
fi

# files to process
F="$PREFIX/etc/qemu-web-desktop/machines.conf"
machine_conf="$F"
if [ ! -f "$F" ]; then
  echo "$p: WARNING: can not find QWD machine.conf file ($F)."
fi
conf_d="$PREFIX/etc/qemu-web-desktop/config.d"

F="$PREFIX/usr/share/qemu-web-desktop/html/desktop/index.html"
index_html="$F"
if [ -f       $PREFIX/var/www/qemu-web-desktop/html/desktop/index.html ]; then
  index_html="$PREFIX/var/www/qemu-web-desktop/html/desktop/index.html"
elif [ ! -f  "$F" ]; then
  echo "$p: WARNING: can not find QWD index.html file ($F)."
fi

# set to yes if apache/httpd include_module does not properly show list in index.html
for i in $*; do 
  case "$i" in
    --machine-insert|--machine_insert|machine-insert|machine_insert)
      QWDCTL_INSERT=yes # for refresh/download
    ;;
    --yes|-y)
      QWDCTL_YES=1  # assume Yes for gpu and purge
    ;;
  esac
done

# generated files, should be linked into /usr/share/qemu-web-desktop/html/desktop/
F="$PREFIX/var/lib/qemu-web-desktop"
qwdprefix="$F"
if [ ! -d "$F" ]; then
  echo "$p: WARNING: can not find QWD machines directory ($F)."
fi
machine_html="$qwdprefix/machines.html"

# search for local config
if [ -f "$bin/../config.pl" ]; then
  # using local configuration
  echo "$bin/../config.pl"
  config_pl=$(realpath "$bin/../config.pl")
else
  F="$PREFIX/etc/qemu-web-desktop/config.pl"
  config_pl="$F"
  if [ ! -f "$F" ]; then
    echo "$p: WARNING: can not find QWD config.pl file ($F)."
  fi
fi

# search for local cgi/.pl script
if [ -x "$bin/../cgi-bin/qemu-web-desktop.pl" ]; then
  cgi=$(realpath "$bin/../cgi-bin/qemu-web-desktop.pl")
elif [ -x "$cgi/qemu-web-desktop.pl" ]; then
  # local (git)
  cgi="$cgi/qemu-web-desktop.pl"
elif [ -x "$PREFIX/srv/http/cgi-bin/qemu-web-desktop.pl" ]; then
  # Arch/httpd default
  cgi="$PREFIX/srv/http/cgi-bin/qemu-web-desktop.pl"
elif [ -x "$PREFIX/var/www/cgi-bin/qemu-web-desktop.pl" ]; then
  # RedHat/httpd and Brew default
  cgi="$PREFIX/var/www/cgi-bin/qemu-web-desktop.pl"
else
  # Debian default
  cgi="$PREFIX/usr/lib/cgi-bin/qemu-web-desktop.pl"
fi

# identify type of system
if [ -e "$PREFIX/etc/initramfs-tools/modules" ]; then
  # Debian-class
  DIST=Debian
else
  # Arch-class
  DIST=Arch
fi

# List all section names in an INI file (one per line)
# Replaces: confget -f $F -q sections
# Replaces: crudini --get $F
ini_sections() {
    local f="$1"
    awk '
        /^[ \t]*[;#]/ { next }                 # skip comment lines
        /^[ \t]*\[.*\][ \t]*$/ {
            line=$0
            gsub(/^[ \t]*\[[ \t]*/, "", line)
            gsub(/[ \t]*\][ \t]*$/, "", line)
            print line
        }
    ' "$f"
}

# Get the value of key "$3" in section "$2" of file "$1"
# Replaces: confget -f $F -s $i url
# Replaces: crudini --get $F $i url
ini_get() {
    local f="$1" section="$2" key="$3"
    awk -v section="$section" -v key="$key" '
        function trim(s) {
            gsub(/^[ \t]+|[ \t]+$/, "", s)
            return s
        }
        /^[ \t]*[;#]/ { next }                 # skip comment lines
        /^[ \t]*\[.*\][ \t]*$/ {
            line=$0
            gsub(/^[ \t]*\[[ \t]*/, "", line)
            gsub(/[ \t]*\][ \t]*$/, "", line)
            in_sec = (line == section)
            next
        }
        in_sec {
            line = $0
            eq = index(line, "=")
            if (eq > 0) {
                k = trim(substr(line, 1, eq-1))
                if (k == key) {
                    v = trim(substr(line, eq+1))
                    # strip surrounding quotes if present, like crudini does
                    if (v ~ /^".*"$/ || v ~ /^\x27.*\x27$/) {
                        v = substr(v, 2, length(v)-2)
                    }
                    print v
                    exit
                }
            }
        }
    ' "$f"
}

escape_html() {
    local s="$1"
    s="${s//&/&amp;}"
    s="${s//</&lt;}"
    s="${s//>/&gt;}"
    s="${s//\"/&quot;}"
    s="${s//\'/&#39;}"
    printf '%s' "$s"
}

MAX_INPUT_LEN=256
validate_input() {
    [[ ${#1} -le $MAX_INPUT_LEN ]] || { echo "ERROR: Input too long" >&2; return 1; }
    return 0
}

qwdctl_status() {
  # list running sessions.
    echo "$p status"
    echo "  config:   $config_pl"
    echo "            $conf_d/"
    echo "  machines: $machine_conf"
    echo "            $machine_html"
    echo "            $qwdprefix/machines/"
    echo "  landing:  $index_html"
    ip_address=$(hostname -I | awk '{print $1}')
    echo "  service:  http://$ip_address/qemu-web-desktop/"
    echo " "
    # Loop through machine lists
    active_sections=()
    inactive_sections=()
    files=("$machine_conf" "$conf_d"/*.ini "$conf_d"/*.conf)
    for F in "${files[@]}"; do
        # Skip if the file does not exist (e.g., no .ini or .conf files)
        [ -f "$F" ] || continue
        # Extract active sections
            while IFS= read -r section; do
                active_sections+=("$section")
            done < <(grep -E '^\[[^]]+\]' "$F" | sed -E 's/^\[([^]]+)\]/\1/')

            # Extract inactive sections
            while IFS= read -r section; do
                inactive_sections+=("$section")
            done < <(grep -E '^;\[[^]]+\]' "$F" | sed -E 's/^;\[([^]]+)\]/\1/')
    done
    
    if [ -z "${active_sections[*]}" ]; then
      echo "Enabled machines (use '$p enable VM|FILE|URL' to add one):"
      echo "  None"
    else
      echo "Enabled machines (disable with '$p disable VM'):"
      printf '  %s\n' "${active_sections[@]}"
    fi
    echo 
    if [ ! -z "${inactive_sections[*]}" ]; then
      echo "Disabled machines (enable with '$p enable VM')"
      printf '  %s\n' "${inactive_sections[@]}"
      echo 
    fi
    orphan=0
    echo "Orphan machine files (not active/inactive, purge with '$p purge VM'):"
    for file in "$qwdprefix/machines"/*; do
        [ -f "$file" ] || continue
        filename=$(basename "$file")
        # Check if filename is not in active or inactive lists
        if ! printf '%s\n' "${active_sections[@]}" "${inactive_sections[@]}" | grep -q "^${filename}$"; then
            orphan=1
            echo "  $filename"
        fi
    done
    if [ $orphan == 0 ]; then echo "  None"; fi
    echo " "
    echo "Running sessions:"
    # Header for the table
    printf "%-8s | %5s | %-15s | %4s | %6s | %-7s | %s\n" "PID" "CPU/%" "User name" "#CPU" "MEM/GB" "GPU" "Full name"
    printf "%s\n" "------------------------------------------------------------------------"

    # Get PIDs for qemu-system-x86_64, excluding those launched via sh -c
    ps aux | grep 'qemu-system' | grep -v 'grep' | grep -v 'sh -c' | while read -r line; do
        # Extract PID and CPU usage
        pid=$(echo "$line" | awk '{print $2}')
        cpu_usage=$(echo "$line" | awk '{print $3"%"}')

        # Extract the full command
        cmd=$(echo "$line" | awk '{for(i=11;i<=NF;i++) printf $i" "; print ""}')

        # Extract full name (after -name)
        full_name=$(echo "$cmd" | grep -oP '(?<=-name ).*?(?= -|$)')

        # Extract number of CPUs (after -smp)
        nb_cpu=$(echo "$cmd" | grep -oP '(?<=-smp ).*?(?= -|$)')

        # Extract memory (after -m) and convert to GB
        mem_mb=$(echo "$cmd" | grep -oP '(?<=-m ).*?(?= -|$)')
        mem_gb=$(echo "scale=2; $mem_mb / 1024" | bc)

        # Extract user name (between ':' in full_name)
        user_name=$(echo "$full_name" | grep -oP '(?<=:)[^:]+(?=:)')

        # Extract attached PCI (after -device vfio-pci,host=)
        pci=$(echo "$cmd" | grep -oP '(?<=host=)[^, ]+' || echo "")

        # Print the row
        printf "%-8s | %5s | %-15s | %4s | %6s | %-7s | %s\n" "$pid" "$cpu_usage" "$user_name" "$nb_cpu" "$mem_gb" "$pci" "$full_name"
    done
}

validate_gpu_id() {
    [[ "$1" =~ ^([0-9a-fA-F]{4}:[0-9a-fA-F]{4},?)+$ ]] || return 1
}

if [ -z "$1" ]; then
  qwdctl_status
  exit 0
fi

# user level actions
case "$1" in
  # ------------------------------------------------------------------------------
    --help|help|-h)
      # show help.
      
      echo "usage: $p [help|download|refresh|status|start VM|stop|gpu|gpu-unlock|enable|disable|purge|edit] ..."
      echo " "
      echo "  controls the availability of virtual machines for the "
      echo "  qemu-web-desktop."
      echo "  In addition, the status of the running sessions can be displayed"
      echo "  and it is possible to start virtual machines manually and display"
      echo "  them in a browser."
      echo 
      echo "OPTIONS:"
      echo "  --help|help|-h"
      echo "      show this help."
      echo 
      echo "  --download|download|update"
      echo "      scan the $machine_conf file for [name.ext] and download them when URL are given."
      echo "      a 'refresh' is then performed. Virtual machine images are stored into $qwdprefix/machines."
      echo "      Additional machine descriptions can be given in $conf_d/*.ini files."
      echo " "
      echo "  --edit|edit [machines|config|web]"
      echo "      edit the VM/machine list, the service configuration file or the service web page."
      echo "      In the case of the VM list, the '$p download' command is triggered automatically after edit."
      echo "      Set the \$EDITOR variable to select the text editor to use."
      echo "      In case the '$p edit machines' has no effect on the index.html service landing page,"
      echo "      specify set 'QWDCTL_INSERT=yes' or option '--machine-insert'."
      echo "      It is recommended to use the '--enable' and '--disable' to manage the VM/machine list."
      echo 
      echo "  --gpu|gpu VENDOR:MODEL"
      echo "      configure GPU for pass-through. The GPU_ids are e.g. '10de:1d01'."
      echo " "
      echo "  --gpu-unlock|gpu-unlock"
      echo "      unlock/re-attach all GPU's to server (uninstall pass-through)."
      echo " "
      echo "  --machines-insert"
      echo "      used with download/refresh, force to modify the index.html service"
      echo "      landing page instead of using an include."
      echo "      Same as QWDCTL_INSERT=yes shell variable."
      echo 
      echo "  --refresh|refresh"
      echo "      scan the $machine_conf file, and generate the $machine_html that lists"
      echo "      available images to show in the qemu-web-desktop main form."
      echo "      Additional machine descriptions can be given in $conf_d/*.ini files."
      echo " "
      echo "  --status|status"
      echo "      list running sessions, as well as enabled and disabled machines."
      echo " "
      echo "  --start|start VM ..."
      echo "      start the given VM or ISO file (full path) in a browser. Connect to it with"
      echo "      the displayed URL. Further arguments are passed to the service (see config.pl)"
      echo "      e.g. --snapshot_alloc_mem=1 (in GB) and --snapshot_alloc_cpu=2."
      echo "      Changes are lost except when specifying option --snapshot_use_master=1,"
      echo "      requiring write access. When running ISO's you may also specify"
      echo "      --snapshot_alloc_disk=40 (in GB)."
      echo "      Use '$p --start' to get the full list of default VM settings."
      echo " "
      echo "  --stop|stop TOKEN"
      echo "      stop sessions matching TOKEN. Some snapshot files may be left-over."
      echo " "
      echo "  --enable|enable VMNAME|FILE|URL {'DESCRIPTION'}"
      echo "      add or (re)activate a VM name.ext, file or URL. Optional description is given as a string."
      echo 
      echo "  --disable|disable VMNAME"
      echo "      deactivate a VM."
      echo 
      echo "  --purge|purge {VMNAME}"
      echo "      remove VM files which are inactive and not used. When VM argument is "
      echo "      given, only files starting by 'VM' are checked."
      echo 
      echo "  --version|version|-v"
      echo "      show $p version."
      echo 
      echo "  --yes|-y"
      echo "      assume yes to questions. Same as QWDCTL_YES=1 shell variable."
      echo 
      echo "AUTHOR:"
      echo "  Written by Roland Mas and Emmanuel Farhi."
      echo 
      echo "FILES:"
      echo "  - $config_pl"
      echo 
      echo "  - $conf_d/*.pl"
      echo 
      echo "  - $machine_conf"
      echo "  - $conf_d/*.ini"
      echo "  Entries should contain lines:"
      echo "    [name.ext or URL]"
      echo "    description=<name of machine to appear in the form>"
      echo "  In addition, any line with:"
      echo "    url=<link>"
      echo "  will retrieve the given file with: $p download"
      echo "  When the description is missing, the VM file name is used."
      echo "  Supported virtual machine formats: ISO, QCOW2, VDI, VMDK, RAW, VHD/VHDX, QED" 
      echo 
      echo "  - $qwdprefix/machines"
      echo "  - $machine_html"
      echo 
      echo "  - /usr/share/qemu-web-desktop/html/desktop"
      echo " "
      echo "  - $index_html"
      echo 
      echo "  - https://gitlab.com/soleil-data-treatment/soleil-software-projects/qemu-web-desktop"
      echo " "
      echo "ENVIRONMENT VARIABLES:"
      echo "  EDITOR            Set the text editor to use, e.g. nano. Default is to use 'editor' or \$EDITOR or 'edit'."
      echo "  QWDCTL_INSERT=yes is equivalent to setting --machine-insert."
      echo "  QWDCTL_YES=1      is equivalent to setting --yes (skip confirm)."
      echo " "

      exit 0
      ;;
  # ------------------------------------------------------------------------------
    --status|status)
      qwdctl_status
      exit 0
      ;;
  # ------------------------------------------------------------------------------
    --start|start)
      # start the given VM or ISO file (full path) in a browser.
    
      # 2nd arg is the VM
      if [ "x$2" = "x" ]; then
        echo "Usage: $p $1 VM [options]"
        echo "Configuration: $config_pl"
        echo "Script:        $cgi"
        echo "Availabe options are:"
        $cgi -h
        exit 0
      fi
      validate_input "$2" || exit 1
      DIR=$(dirname "$2")
      DIR=$(realpath "$DIR")
      VM=$(basename "$2")
      shift 2
      if [ ! -f "$DIR/$VM" -a -f "$qwdprefix/machines/$VM" ]; then
        DIR=$qwdprefix/machines
      fi
      
      # snapshot_alloc_cpu snapshot_alloc_mem
      echo "$p: Launching $DIR/$VM $@"
      log_file=$(mktemp -t "qwdctl_${VM}_XXXXXX.log")
      chmod 644 "$t" 
      echo "Connect to (log in $log_file):"
      $cgi --dir_snapshots=/tmp --dir_cfg=/tmp --dir_machines="$DIR" --machine="$VM" --oneshot=1 --service_max_mem_fraction_nb_per_user=0.8 --service_max_cpu_fraction_nb_per_user=0.8 --snapshot_alloc_disk=30 $@ 2>&1 | tee $log_file | awk '/URL:/ { print $4 " " $5 }'
      echo "Cleaning remaining sessions..."
      $cgi --service_purge=1 --dir_snapshots=/tmp --dir_cfg=/tmp 2>&1 | $GREP -i "stop"
      echo "Done $VM"
      exit 0
      ;;
  # ------------------------------------------------------------------------------
    --version|version|-v)
      # show qemu-web-desktop version.
    
      $cgi -v
      exit 0
      ;;
esac

# require root for further actions
if [[ $(id -u) -ne 0 ]]; then echo "$p $1: Please run as root."; exit 1; fi

case "$1" in
# ------------------------------------------------------------------------------
    --download|download|update)
      # scan the machines.conf + INI file for [name.ext] and download them when URL are given.
    
      mkdir -p $qwdprefix/machines
      
      for F in "$machine_conf" "$conf_d/*.ini" "$conf_d/*.conf"; do
        if [ ! -f "$F" ]; then continue; fi
        sections=$(ini_sections "$F")
        for i in $sections ; do
            b=$(basename "$i")
            if [ -f "$i" ]; then
              if [ ! -f "$qwdprefix/machines/downloads/$b" ] && [ ! -f "$qwdprefix/machines/$b" ]; then
                # copy local VM file
                echo "$p: Copy $i into $qwdprefix/machines/downloads/$b"
                mkdir -p "$qwdprefix/machines/downloads/$b"
                cp "$i" $qwdprefix/machines/downloads/$b/ || { echo "ERROR: Failed to copy $i" >&2; exit 1; }
                i="$b"
              fi
            elif [[ "$i" =~ ^"http" ]]; then
              # identify distant VM given as "name.ext"
              u="$i"
              i=$(basename "$u")
            else
              u=$(ini_get "$F" "$i" url)
            fi
            mkdir -p $qwdprefix/machines/downloads/$i
            # download http stuff
            if [ "$u" ] ; then
              echo "$p: Download $u"
              (cd $qwdprefix/machines/downloads/$i; wget --timeout=30 --tries=3 --no-check-certificate --no-cookies -N -- "$u")
            fi
            # create link if relevant
            vm=$(ls -t $qwdprefix/machines/downloads/$i/* 2>/dev/null | head -1)
            if [ -e "$vm" ] ; then
              ln -sf -- "$vm" "$qwdprefix/machines/$i"
            fi
        done
      done
      $p refresh
      ;;
# ------------------------------------------------------------------------------
    --edit|edit)
      # edit the VM/machine list, the service configuration file or the service web page.
    
      validate_input "$2" || exit 1
      case "$2" in
        machine|machines|vm|list)
        FILE=$machine_conf
        echo "$p: Edit machines: WARNING: rather use '$p enable|disable VM'"
        ;;
        landing|web|index|html)
        FILE=$index_html
        ;;
        config|service)
        echo "$p: Edit config: WARNING: rather edit local files in $conf_d"
        FILE=$config_pl
        ;;
        *)
        echo "$p: ERROR: Nothing to edit." >&2; exit 1;
        ;;
      esac
      echo "$p: Edit $FILE"
      "${EDITOR:-editor}" "$FILE"
      # trigger 'download' command
      $p download
      ;;
# ------------------------------------------------------------------------------
    --refresh|refresh)
      # scan the machines.conf file, and generate the machines.html that lists
      # available images to show in the qemu-web-desktop main form.
    
      mkdir -p $qwdprefix/snapshots
      if getent passwd _qemu-web-desktop > /dev/null 2>&1; then
        chown _qemu-web-desktop $qwdprefix/snapshots # when using MPM ITK
      fi
      mkdir -p $qwdprefix/machines
      cd $qwdprefix/machines
      echo "$p: $1 -> $machine_html"
      
      # list of machines
      t=$(mktemp -t "machine_html.XXXXXX")
      has_vm=no
      chmod 644 "$t" 
      chmod 755 "$qwdprefix"
      files=("$machine_conf" "$conf_d"/*.ini "$conf_d"/*.conf)
      for F in "${files[@]}"; do
        if [ ! -f "$F" ]; then continue; fi
        echo "$p: Reading  $F" 
        sections=$(ini_sections "$F")
        for i in $sections; do
            d=$(ini_get "$F" "$i" description)
            if [ "x$d" = "x" ]; then  # when no description is given, use file name
              d="$i"
            fi
            u="$i"
            i=$(basename "$i")
            if [ -e "$i" ] ; then
              if [ "$d" ]; then
                # add entry when VM file and descr are given
                if [ "$i" = "$u" ]; then
                  echo "Found    $i '$d'"
                else
                  echo "Found    $i '$d' (from $u)"
                fi
                i_escaped=$(escape_html "$i")
                d_escaped=$(escape_html "$d")
                echo "<option value='$i_escaped'>$d_escaped</option>" >> "$t"
                has_vm=yes
              fi
            else
              echo "$p: WARNING: $F [$i] is not available."
            fi
        done
      done
      if [ "$has_vm" = no ]; then
        echo "$p: WARNING: No VM is configured. Use 'qwdctl enable VM|FILE|URL' to add entries."
        echo '<option value="monitor">[ No VM has been configured. Use "qwdctl enable VM" as admin ]</option>' > $t
      fi
      mv "$t" "$machine_html" || { echo "ERROR: Failed to move $t" >&2; exit 1; }
      # handle case when apache mod_include does not work...
      if [ "x$QWDCTL_INSERT" = "xyes" ]; then
        echo "$p: Updating $index_html"
        t=$(mktemp -t "index_html.XXXXXX")
        chmod 644 "$t" 
        lead="<\!--BEGIN_MACHINE_LIST-->"
        tail="<\!--END_MACHINE_LIST-->"
        sed -e "/$lead/,/$tail/{ /$lead/{p; r $machine_html
            }; /$tail/p; d }" $index_html > $t
        mv "$t" "$index_html" || { echo "ERROR: Failed to move $t" >&2; exit 1; }
        chmod a+r "$index_html"
        rm $t
      fi
      ;;

# ------------------------------------------------------------------------------
    --stop|stop)
      # stop sessions matching TOKEN. Some snapshot files may be left-over.
    
      if [ "x$2" = "x" ]; then
        echo "$p: Stopping all local VMs"
        $cgi --service_purge=1 --dir_snapshots=/tmp --dir_cfg=/tmp 2>&1 | $GREP -i "stop"
        exit 0
      fi
      validate_input "$2" || exit 1
      t=$(ps aux | $GREP -F qemu-web-desktop | $GREP -F -- "$2")
      name=$(echo "$t" | $GREP -oP '(?<=\-name )[^ ]*' )
      pid=$( echo "$t" | awk '{ print $2 }' )
      [[ -n "$pid" ]] || { echo "$p: ERROR: No matching process found" >&2; exit 1; }
      table=$(printf '%s\n' "$name" | pr -1 -Ts'\t')
      u=$(echo "$table" | uniq )
      echo "$p: Stopping:"
      echo "$u"
      kill -- "$pid"
      ;; 
      
# ------------------------------------------------------------------------------
    --gpu|gpu|gpu-lock)
      # configure GPU for pass-through. The GPU_ids are e.g. '10de:1d01'.
      validate_input "$2" || exit 1
      GPU_ids="$2"
        
      # get CPU model
      CPU_type=
      $GREP -qi "intel"     /proc/cpuinfo && CPU_type=intel_iommu
      $GREP -qi "amd"       /proc/cpuinfo && CPU_type=amd_iommu
      if [ "x$CPU_type" = "x" ]; then
        echo "$p: ERROR: GPU support can currently only be configured for AMD and Intel CPUs." >&2
        exit 1
      fi
        
      # list available GPUs
      echo "$p: Available GPUs. The GPU_ids are usually shown as [vendor:model]"
      GPU_avail=`lspci -nnv | $GREP -i "VGA\|3d controller"`
      echo "$GPU_avail"
      GPU_nb=$(echo "$GPU_avail" | wc -l)
      if [ "$GPU_nb" -lt "2" ]; then
        echo "$p: WARNING: you need at least 2 GPU models, and keep one for your display. PROCEEDING MAY LEAD TO NO-DISPLAY."
      fi
      echo "$p: NOTE: Make sure to keep one GPU for your current display."
      if [ "x$GPU_ids" = "x" ]; then
        read -p ">> Enter vendor:model ID e.g. 10de:13bc,10de:0fbc (all parts of IOMMU group. Ctrl-C to abort): " GPU_ids
      fi
      if [ "x$GPU_ids" = "x" ]; then
        echo "Usage: $p gpu GPU_ids";
        echo "  The GPU_ids should be e.g. '10de:1d01' or '10de:13bc,10de:0fbc'"
        exit 1
      fi

      GPU_firstID=`echo $GPU_ids | cut -d ',' -f1`
      echo $GPU_avail | $GREP -q "$GPU_firstID" || GPU_avail=no
      if [ "x$GPU_avail" = "xno" ]; then
        echo "$p: ERROR: GPU $GPU_ids model not available." >&2
        exit 1
      fi

      # display message and wait for confirmation
      echo "Ready to configure GPU $GPU_ids for $CPU_type (set VFIO pass-through)."
      echo "All these GPU models will be detached from the server and made usable for virtualization."
      echo "WARNING: Make sure you keep a display for your system."
      echo "The following files will be modified ($DIST):"
      echo "  /etc/default/grub      (GRUB_CMDLINE_LINUX_DEFAULT) update"
      echo "  /etc/security/limits.conf                           append"
      echo "  /etc/modprobe.d/vfio.conf                           create"
      echo "  /etc/udev/rules.d/10-qemu-hw-users.rules            create"
      if [ "x$DIST" = "xDebian" ]; then
        echo "  /etc/initramfs-tools/modules                        append"
        echo "  /etc/systemd/system/apache2.service.d/override.conf create"
      else
        echo "  /etc/mkinitcpio.conf                                append"
        echo "  /etc/systemd/system/httpd.service.d/override.conf   create"
        echo "  NOTE: require package 'mkinitcpio'"
      fi
      if [ -z "$QWDCTL_YES" ]; then
        read -p ">> Continue (y/N)?" choice
        case "$choice" in 
          y|Y ) echo "Proceeding";;
          *) echo "Aborting." && exit 1;;
        esac
      fi

      # GRUB
      FILE=$PREFIX/etc/default/grub
      if $GREP -i "^GRUB_CMDLINE_LINUX_DEFAULT" "$FILE" | $GREP "_iommu"; then
          echo "$p; WARNING: $FILE seems to already contain IOMMU keyword."
          echo "$p:          Please check content and/or clean above files. You may use: $p gpu_unlock"
          cat $FILE
          read -p ">> Press 'N' to ignore this step, 'y' to proceed anyway, or Ctrl-C to ABORT." choice
      else 
        choice=y
      fi
      case "$choice" in 
        y|Y )
          validate_gpu_id "$GPU_ids" || { echo "ERROR: Invalid GPU ID format" >&2; exit 1; }
          DATE=$(date +"%Y-%m-%d-%H-%M")
          echo "$p: Updating  $FILE (old stored as $FILE.$DATE)"
          cp $FILE $FILE.$DATE || { echo "ERROR: Failed to copy $FILE" >&2; exit 1; }
          sed -i "s|^GRUB_CMDLINE_LINUX_DEFAULT=|&$CPU_type=on iommu=pt vfio-pci.ids=$GPU_ids|g" "$FILE"
        ;;
      esac
      
      # VFIO modules
      FILE=$PREFIX/etc/modprobe.d/vfio.conf
      echo "$p: Creating  $FILE"
      dd status=none of=${FILE} << EOF
# $FILE
options vfio-pci ids=$GPU_ids disable_vga=1
EOF
  
      # Initramfs modules
      if [ "x$DIST" = "xDebian" ]; then
        FILE=$PREFIX/etc/initramfs-tools/modules
        MODULES=$(cat << EOF
# $FILE for qemu-web-desktop
vfio
vfio_iommu_type1
vfio_pci
vfio_virqfd
vhost-netdev
EOF
        )
      else
        FILE=$PREFIX/etc/mkinitcpio.conf
        if ! command -v mkinitcpio 2>&1 >/dev/null; then
          echo "$p: ERROR: Missing mkinitcpio. Install it, and then retry." >&2
          exit 1
        fi
        MODULES=$(cat << EOF
# $FILE for qemu-web-desktop
MODULES+=(vfio vfio_iommu_type1 vfio_pci)
HOOKS+=(modconf)
EOF
        )
      fi
      # append only if token not found in FILE
      echo "$p: Appending $FILE"
      if [ -e "$FILE"  ]; then cp $FILE $FILE.$DATE || { echo "ERROR: Failed to copy $FILE" >&2; exit 1; }; fi
      # append only if token not found in FILE
      $GREP -q "vfio_pci" "$FILE" || echo "$MODULES" >> "$FILE"

      # udev Rules
      FILE=$PREFIX/etc/udev/rules.d/10-qemu-hw-users.rules
      if [ -e "$FILE" ]; then
        echo "$p: WARNING:  $FILE is kept (already there)."
      else
        echo "$p: Creating  $FILE"
        dd status=none of=${FILE} << EOF
# $FILE for qemu-web-desktop
SUBSYSTEM=="vfio", OWNER="root", GROUP="kvm"
EOF
      fi
  
      # limits: append at end
      FILE=$PREFIX/etc/security/limits.conf
      echo "$p: Appending $FILE"
      LIMITS=$(cat << EOF
# $FILE for qemu-web-desktop
*    soft memlock 20000000
*    hard memlock 20000000
@kvm soft memlock unlimited
@kvm hard memlock unlimited
EOF
      )
      # append only if token not found in FILE
      $GREP -q "kvm soft memlock unlimited" "$FILE" || echo "$LIMITS" >> "$FILE"

      # update apache2 mem allocation limit
      if [ -e "$PREFIX/etc/systemd/system/httpd.service" ]; then
        DIR=$PREFIX/etc/systemd/system/httpd.service.d/
      else
        DIR=$PREFIX/etc/systemd/system/apache2.service.d/
      fi
      mkdir -p $DIR
      FILE=$DIR/override.conf
      echo "$p: Creating  $FILE"
      dd status=none of=${FILE} << EOF
# $FILE for qemu-web-desktop
[Service]
LimitMEMLOCK=infinity
EOF

      echo "$p: Updating  GRUB boot, kernel modules, rules"
      if [ "x$DIST" = "xDebian" ]; then
        update-initramfs -u
        update-grub
      else
        mkinitcpio -p linux
        grub-mkconfig -o /boot/grub/grub.cfg || echo "WARNING: Failed to update GRUB"
      fi
      udevadm control --reload-rules
      udevadm trigger
      systemctl daemon-reload

      # request to uncomment GPU section in index.html
      echo "-----------------------------------------------------------------"
      echo "Check the above files, then UNCOMMENT the GPU section in the file"
      echo "  $index_html"
      echo "You may use 'sudo $p edit web' to do so."
      echo "To release the GPU back to the server use: 'sudo $p gpu_unlock'"
      echo "Now please reboot to activate changes."
      echo "-----------------------------------------------------------------"
      ;;
      
# ------------------------------------------------------------------------------
    --gpu-unlock|gpu-unlock)
      # unlock/re-attach all GPU's to server (uninstall pass-through).
  
      echo "$p: Cleaning existing GPU virtualization (remove VFIO pass-through)."
      echo "All detached/locked GPUs will be returned to the server."
      echo "The following files will be modified:"
      echo "  /etc/default/grub      (GRUB_CMDLINE_LINUX_DEFAULT) update"
      echo "  /etc/security/limits.conf                           kept"
      echo "  /etc/modprobe.d/vfio.conf                           remove"
      echo "  /etc/udev/rules.d/10-qemu-hw-users.rules            remove"
      if [ "x$DIST" = "xDebian" ]; then
      echo "  /etc/initramfs-tools/modules                        update"
      echo "  /etc/systemd/system/apache2.service.d/override.conf remove"
      else
      echo "  /etc/mkinitcpio.conf                                update"
      echo "  /etc/systemd/system/httpd.service.d/override.conf   remove"
      echo "  NOTE: require package 'mkinitcpio'"
      fi
      if [ -z "$QWDCTL_YES" ]; then
        read -p ">> Continue (y/N)?" choice
        case "$choice" in 
          y|Y ) echo "Proceeding";;
          *) echo "Aborting." && exit 1;;
        esac
      fi
      
      # restore default GRUB options
      FILE=$PREFIX/etc/default/grub
      DATE=$(date +"%Y-%m-%d-%H-%M")
      echo "$p: Updating  $FILE (old stored as $FILE.$DATE)"
      cp $FILE $FILE.$DATE || { echo "ERROR: Failed to copy $FILE" >&2; exit 1; }
      sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT/#GRUB_CMDLINE_LINUX_DEFAULT/g" $FILE
      echo "GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash\"" >> $FILE
      
      # comment modules
      if [ "x$DIST" = "xDebian" ]; then
        FILE=$PREFIX/etc/initramfs-tools/modules
      else
        if ! command -v mkinitcpio 2>&1 >/dev/null; then
          echo "$p: ERROR: Missing mkinitcpio to proceed. Install it, and then retry." >&2
          exit 1
        fi
        FILE=$PREFIX/etc/mkinitcpio.conf
      fi
      if [ -e "$FILE" ]; then
        echo "$p: Updating  $FILE (old stored as $FILE.$DATE)"
        cp $FILE $FILE.$DATE || { echo "ERROR: Failed to copy $FILE" >&2; exit 1; }
        if [ "x$DIST" = "xDebian" ]; then
          sed -i "s/^vfio/#vfio/g"                 $FILE
          sed -i "s/^vhost-netdev/#vhost-netdev/g" $FILE
        else
          sed -i "s/^MODULES+=/#MODULES+=/g"       $FILE
          sed -i "s/^HOOKS+=/#HOOKS+=/g"           $FILE
        fi
      fi

      rm -f $PREFIX/etc/modprobe.d/vfio.conf
      rm -f $PREFIX/etc/udev/rules.d/10-qemu-hw-users.rules
      if [ "x$DIST" = "xDebian" ]; then
      rm -f $PREFIX/etc/systemd/system/apache2.service.d/override.conf
      else
      rm -f $PREFIX/etc/systemd/system/httpd.service.d/override.conf
      fi
      
      if [ "x$DIST" = "xDebian" ]; then
        update-initramfs -u
        update-grub
      else
        mkinitcpio -p linux
        grub-mkconfig -o $PREFIX/boot/grub/grub.cfg  || echo "$p: WARNING: Failed to update GRUB"
      fi
      udevadm control --reload-rules
      udevadm trigger
      systemctl daemon-reload
      
      # request to uncomment GPU section in index.html
      echo "-----------------------------------------------------------------"
      echo "Check the above files, then COMMENT the GPU section in the file"
      echo "  $index_html"
      echo "You may use 'sudo $p edit web' to do so."
      echo "Now please reboot to activate changes."
      echo "-----------------------------------------------------------------"
      ;;
# ------------------------------------------------------------------------------
    --enable|enable|add)
      # add/enable/uncomment a VM as a config.d/ini file.
      VM="$2"
      validate_input "$2" || exit 1
      if [ "$VM" = "" ]; then
        echo "ERROR: usage: $p enable VMNAME|FILE|URL {'DESCRIPTION'}" >&2
        exit 1
      fi
      if [[ ! "$VM" =~ ^[a-zA-Z0-9._-]+$ ]]; then
        echo "ERROR: $p: Invalid VM name" >&2
        exit 1
      fi
      DESCR="$3"
      b=$(basename "$VM")
      if [ "$DESCR" = "" ]; then
        DESCR="$b"
      fi
      FILE=
      
      # search for existing entry
      files=("$machine_conf" "$conf_d"/*.ini "$conf_d"/*.conf)
      for F in "${files[@]}"; do
        [ -f "$F" ] || continue
        if $GREP -q "\[$VM\]" "$F"; then
          # uncomment found $VM in $FILE
          FILE="$F"
          echo "$p: Uncomment [$VM] in $FILE"
          sed -i "/^;\[$VM\]/,/^\(\[.*\]\|;\[.*\]\)/{
    /^;\[$VM\]/s/^;//;
    /^;\(description\|url\)=/s/^;//;
}" "$FILE"
        fi # found
      done
      
      u=  # remote URL if relevant
      if [ -z "$FILE" ]; then
        # new file if does not exist
        FILE="$conf_d/$b.ini"
        # check for remote URL
        if [[ "$VM" =~ ^"http" ]]; then
          # identify distant VM given as "name.ext"
          u="$VM"
          VM=$(basename "$u")
        elif [ -f "$VM" ]; then
          # copy local VM file
          echo "$p: Copy $VM into $qwdprefix/machines/"
          cp -- "$VM" "$qwdprefix/machines/" || { echo "ERROR: Failed to copy $VM" >&2; exit 1; }
          VM=$(basename "$VM")
        fi
        echo "$p: Create $FILE"
        dd status=none of=${FILE} << EOF
; $FILE              
; This file lists virtual machines to be inserted in the qemu-web-desktop login page.
;
;  Supported virtual machine formats include: ISO, QCOW2, VDI, VMDK, RAW, VHD/VHDX, QED
;
;  Each entry in the configuration file '/etc/qemu-web-desktop/machines.conf'
;  spans on 3 lines:
;  -  [name.ext or FILEPATH or URL] 
;  -  url=[URL to virtual machine disk, optional]
;  -  description=[description to be shown in the service page] 
;
;  Images listed in the configuration file without a 'url=' parameter are
;  expected to be downloaded by hand and installed into
;  '/var/lib/qemu-web-desktop/machines' by the local administrator. Then, just 
;  specify the [name.ext] and description.
;
; Then run 'qwdctl download' or 'qwdctl refresh' commands.
[$VM]
description=$DESCR
EOF
        if [ "$u" ]; then
          # add URL
          echo "url=$u" >> $FILE
        fi
      fi
      $p download
      ;;
  
# ------------------------------------------------------------------------------
    --disable|disable|remove)
      # remove (comment) a VM entry.
      VM="$2"
      validate_input "$2" || exit 1
      if [ "$VM" = "" ]; then
        echo "ERROR: usage: $p disable VM" >&2
        exit 1
      fi
      # search for existing entry
      files=("$machine_conf" "$conf_d"/*.ini "$conf_d"/*.conf)
      for F in "${files[@]}"; do
        [ -f "$F" ] || continue
        echo "$p: Reading  $F" 
        b=$(basename "$VM")
        v=
        if   $GREP -q "\[$b\]" "$F"; then
          v=$b
        elif $GREP -q "\[$VM\]" "$F"; then
          v=$VM
        fi
        if [ ! "$v" = "" ]; then
          echo "$p: Comment $v in $F"
          sed -i "/^\[$v\]/,/^\(\[.*\]\|;\[.*\]\)/{
    /^\[$v\]/s/^/;/;
    /^\(description\|url\)=/s/^/;/;
}" "$F"
        fi
      done
      $p download
      ;;
      
# ------------------------------------------------------------------------------
    --purge|purge)
      # clean unused and disabled VM files.
      VM="$2" # optional name of VM file to match
      validate_input "$2" || exit 1
      # scan VM files in $qwdprefix/machines
      for F in "$qwdprefix/machines"/*; do
        b=$(basename "$F")
        if [ ! -f "$F" ]; then continue; fi
        # when VM is given, we must only check when F == VM
        if [[ -n "$VM" && ! "$b" == "$VM"* ]]; then
          continue
        fi
        # continue $F is currently running
        t=$(ps -u _qemu-web-desktop -o comm= | $GREP -qF -- "$b")
        if [ "$t" ]; then
          echo "$p: VM $b is running as: $t"
          continue
        fi
        # or $F if is active (not commented) in machine conf
        FOUND=no
        for C in $machine_conf $conf_d/*.ini $conf_d/*.conf; do
          if [ ! -f "$C" ]; then continue; fi
          l=$($GREP -v '^;' $C)
          if [[ "$l" == *"$b"* ]];then
            FOUND=yes
            echo "$p: VM $b is enabled in $C"
            continue
          fi
        done
        if [ "$FOUND" = "yes" ]; then
          continue
        fi
        # now remove $F
        if [ -z "$QWDCTL_YES" ]; then
          read -p ">> Remove inactive $F (y/N)?" choice
          case "$choice" in 
            y|Y ) echo "Proceeding";;
            *) echo "Keeping." && continue;;
          esac
        fi
        rm -f -- "$F"
      done
      ;;
      
# ------------------------------------------------------------------------------
    *)
    
      echo "Usage: $p [help|download|refresh|status|start VM|stop|gpu|gpu-unlock|enable VM|disable VM|purge|edit] ..."
      exit 0
esac

# ------------------------------------------------------------------------------

