#!/usr/bin/env bash
#
# kvmx virtual machine manager
#
# Copyright (C) 2017 Silvio Rhatto - rhatto at riseup.net
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

# Basic parameters
VERSION="0.2.0"
BASENAME="`basename $0`"
DIRNAME="`dirname $0`"
ACTION="$1"
GLOBAL_USER_CONFIG_FOLDER="$HOME/.config/kvmx"
GLOBAL_USER_CONFIG_FILE="$HOME/.config/kvmxconfig"

# Get the application base
function kvmx_app_base {
  local dest
  local base

  # Determine if we are in a local or system-wide install.
  if [ -h "$0" ]; then
    dest="$(readlink $0)"

    # Check again as the caller might be a symlink as well
    if [ -h "$dest" ]; then
      base="`dirname $dest`"
      dest="$(dirname $(readlink $dest))"
    else
      base="`dirname $0`"
      dest="`dirname $dest`"
    fi

    # Deal with relative or absolute links
    if [ "`basename $dest`" == "$dest" ]; then
      APP_BASE="$base"
    else
      APP_BASE="$dest"
    fi
  else
    APP_BASE="`dirname $0`"
  fi

  echo $APP_BASE
}

# Build a SSH command
function __kvmx_ssh_command {
  if [ ! -z "$1" ]; then
    #local ssh_key_param="-i $1"
    # See https://makandracards.com/makandra/512-how-to-fix-too-many-authentic-authentication-failures-with-ssh-and-or-capistrano
    local ssh_key_param="-o IdentityFile=$1 -o IdentitiesOnly=yes"
  fi

  # See http://blog.djm.net.au/2013/11/chacha20-and-poly1305-in-openssh.html
  SSH_OPTS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=FATAL -o ProxyCommand=none -o Ciphers=chacha20-poly1305@openssh.com $ssh_key_param"
  SSH_COMMAND="ssh $SSH_OPTS -o User=$SSH_LOGIN"
  SCP_COMMAND="scp $SSH_OPTS -o User=$SSH_LOGIN"
}

# Create a guest entry at the global user config folder
function __kvmx_create_config_entry {
  if [ -z "$FOLDER" ]; then
    return 1
  fi

  ( cd $GLOBAL_USER_CONFIG_FOLDER && ln -sf $FOLDER/kvmxfile $VM )
}

# Initialize
function __kvmx_initialize {
  if [ "$ACTION" == "app_base" ] || [ "$ACTION" == "version" ]; then
    return
  fi

  # Load basic functions
  export APP_BASE="`$DIRNAME/kvmx app_base`"
  source $APP_BASE/lib/kvmx/functions || exit 1

  # Check dependencies
  __kvmx_check_dependencies

  # Alias to be used in config files
  KVMX_BASE="$APP_BASE"

  # Initialize
  mkdir -p $GLOBAL_USER_CONFIG_FOLDER

  # Stop processing here for some actions
  if [ "$ACTION" == "init" ] || [ "$ACTION" == "list" ]; then
    return
  fi

  # Check if second argument is a VM name or option
  if [ -z "$2" ]; then
    VM="$(basename `pwd`)"
    SHIFTARGS="1"
  elif [ -e 'kvmxfile' ] && [ ! -e "$GLOBAL_USER_CONFIG_FOLDER/$2" ] && [ "$2" != "$(basename `pwd`)" ]; then
    VM="$(basename `pwd`)"
    SHIFTARGS="1"
  else
    VM="$2"
    SHIFTARGS="2"
  fi

  # Default parameters
  PORT="$(($RANDOM + 1024))"
  SSH="$(($PORT + 22))"
  GUEST_DISPLAY="$(((RANDOM % 10) + 1))"
  XDMCP_PORT="$(($RANDOM + 10000))"

  # Load the default config, providing defaults
  source $APP_BASE/kvmxfile || exit 1

  # Set hostname
  if [ "$hostname" == "kvmx" ] && [ "$VM" != "kvmx" ]; then
    hostname="$VM"
  fi

  # Set domain (this is already done with the default config)
  #domain="${domain:-example.org}"

  # Load user config
  if [ -e "$GLOBAL_USER_CONFIG_FILE" ]; then
    source $GLOBAL_USER_CONFIG_FILE
  fi

  # Load and check guest config
  if [ "$ACTION" != "ls" ] && [ "$ACTION" != "edit" ] && [ "$ACTION" != "usage" ]; then
    if [ ! -e "$GLOBAL_USER_CONFIG_FOLDER/$VM" ]; then
      if [ -e "kvmxfile" ]; then
        # Existing kvmxfile but not registered at the global user config
        FOLDER="$(pwd)"
        __kvmx_create_config_entry
        source $GLOBAL_USER_CONFIG_FOLDER/$VM
      else
        echo "$BASENAME: config not found: $GLOBAL_USER_CONFIG_FOLDER/$VM"
        exit 1
      fi
    else
      source $GLOBAL_USER_CONFIG_FOLDER/$VM
    fi

    if [ -z "$image" ]; then
      if [ -z "$image_base" ]; then
        image_base="$HOME/.local/share/kvmx"
      fi

      # There might be trouble managing the guest when project folder name is different from
      # hostname and no image param is set (kvmx-create puts image in one
      # place, kvmx expects in the other). So we try to guess first the image by hostname
      # and then by guest name. Note that it might be possible to have conflicts if there
      # are machines with hostnames set to the name of other machines. But here we hope that
      # the user is not messing that much ;)
      if [ ! -z "$hostname" ] && [ -e "$image_base/$hostname/box.img" ]; then
        image="$image_base/$hostname/box.img"
      else
        image="$image_base/$VM/box.img"
      fi
    fi

    if [ ! -h "$GLOBAL_USER_CONFIG_FOLDER/$VM" ]; then
      echo "error: $GLOBAL_USER_CONFIG_FOLDER/$VM is not a symlink"
      exit 1
    fi

    # Box and folder config
    KVMXFILE="`readlink $GLOBAL_USER_CONFIG_FOLDER/$VM`"
    KVMX_PROJECT_FOLDER="`dirname $KVMXFILE`"
    STORAGE="`dirname $image`"
    DATADIR="${datadir:-$STORAGE}"
    STATE_DIR="$DATADIR/state/$VM"
    LOG_DIR="$DATADIR/log"
    PIDFILE="$STATE_DIR/pid"
    PORTFILE="$STATE_DIR/port"
    XDMCPPORTFILE="$STATE_DIR/xdmcp"
    SSHFILE="$STATE_DIR/ssh"
    DISPLAYFILE="$STATE_DIR/display"
    SPICEFILE="$STATE_DIR/spice"
    SPICESOCKET="$STATE_DIR/spice.socket"
    XEPHYRFILE="$STATE_DIR/xephyr"
    LOGFILE="$LOG_DIR/qemu"
    SPICELOG="$LOG_DIR/spice"
    XPRALOG="$LOG_DIR/xpra"
    XDMCPLOG="$LOG_DIR/xdmcp"
    MONITORFILE="$STATE_DIR/monitor"
    CONSOLEFILE="$STATE_DIR/console"

    if [ -z "$ssh_custom_pubkey" ]; then
      if [ -e "$DATADIR/ssh/$VM.key" ]; then
        mkdir -p "$DATADIR/ssh"
        SSHKEY="$DATADIR/ssh/$VM.key"
      else
        SSHKEY="$APP_BASE/share/ssh/insecure_private_key"
      fi
    fi

    if [ ! -z "$user" ]; then
      SSH_LOGIN="$user"
    else
      SSH_LOGIN="user"
    fi

    __kvmx_ssh_command $SSHKEY

    mkdir -p $STATE_DIR $LOG_DIR

    # Additional checks
    if [ "$ACTION" != "up" ]           && [ "$ACTION" != "provision" ] && [ "$ACTION" != "purge" ]  && \
       [ "$ACTION" != "destroy" ]      && [ "$ACTION" != "install" ]   && [ "$ACTION" != "config" ] && \
       [ "$ACTION" != "config_unset" ] && [ "$ACTION" != "create" ]    && [ "$ACTION" != "shell" ]  && \
       [ "$ACTION" != "boot" ]; then
      if [ ! -e "$image" ]; then
        echo "$BASENAME: file not found: $image"
        exit 1
      fi

      # See http://www.linux-kvm.org/page/FAQ
      if ! egrep -q '^flags.*(vmx|svm)' /proc/cpuinfo; then
        echo "$BASENAME: WARNING: Intel VT or AMD-V not present at /proc/cpuinfo, expect slow performance"
      fi

      if ! lsmod | grep -q '^kvm '; then
        echo "$BASENAME: WARNING: kvm kernel module not loaded, expect slow performance"
      fi

      if ! groups `whoami` | grep -q 'kvm'; then
        echo "$BASENAME: WARNING: user `whoami` not in kvm group, expect slow performance"
      fi
    fi
  fi
}

# Run spice client
function kvmx_spice {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  if [ "$spice" == "0" ]; then
    echo "$BASENAME: spice is disabled for guest $VM"
    exit 1
  fi

  # Ensure we have the right port configuration: we can also be
  # running directly from command line.
  #PORT="`cat $PORTFILE`"

  if [ -z "$PORT" ]; then
    echo "$BASENAME: cannot get spice port for $VM"
    exit 1
  fi

  if [ "$spice_client" == "spicy" ] && which spicy &> /dev/null; then
    #spicy -h localhost -p $PORT &
    spicy --uri=spice+unix://$SPICESOCKET &
  elif [ "$spice_client" == "remote-viewer" ] && which remote-viewer &> /dev/null; then
    #remote-viewer spice://localhost:$PORT &
    remote-viewer spice+unix://$SPICESOCKET &
  # This is untested due to libvirt requirements
  elif [ "$spice_client" == "virt-viewer" ] && which virt-viewer &> /dev/null; then
    #virt-viewer spice://localhost:$PORT &
    virt-viewer spice+unix://$SPICESOCKET &
  # Unsupported as spicec was deprecated
  #elif [ "$spice_client" != "spicec" ] && which spicec &> /dev/null; then
  #  # https://lists.freedesktop.org/archives/spice-devel/2013-September/014643.html
  #  SPICE_NOGRAB=1 spicec --host localhost --port $PORT &> $SPICELOG &
  elif [ ! -z "$spice_client" ]; then
    echo "$BASENAME: spice_client $spice_client not currently supported"
    exit 1
  else
    if which spicy &> /dev/null; then
      #spicy -h localhost -p $PORT &
      spicy --uri=spice+unix://$SPICESOCKET &
    fi
  fi

  SPICEPID="$!"
  echo "$SPICEPID" > $SPICEFILE

  # Give time to connect
  sleep 2

  # Fix window title an position
  if which /usr/bin/xdotool &> /dev/null; then
    if [ ! -z "$xclient_windowmove" ]; then
      #xdotool search --name "SPICEc:0" windowmove $xclient_windowmove
      xdotool search --name "spice display 0:0" windowmove $xclient_windowmove
    fi

    #xdotool search --name "SPICEc:0" set_window --name $VM
    xdotool search --name "spice display 0:0" set_window --name $VM
  fi

  if [ "$ACTION" == "spice" ]; then
    # Set screen resolution
    if [ "$xrandr" == "1" ]; then
      kvmx_xrandr
    fi

    # Ensure vdagent is running
    if [ "$ssh_support" == "y" ]; then
      if [ "$kvmx_vdagent" != "0" ]; then
        echo "which kvmx-vdagent > /dev/null && DISPLAY=:0 kvmx-vdagent" | kvmx_ssh
      fi
    fi
  fi
}

# Bring virtual machine up
function kvmx_up {
  if kvmx_suspended; then
    $DIRNAME/$BASENAME resume $VM

    if [ "$run_spice_client" == "1" ]; then
      $DIRNAME/$BASENAME spice $VM
    fi

    if [ "$run_xpra" == "1" ]; then
      $DIRNAME/$BASENAME xpra $VM
    fi

    if [ "$run_xephyr" == "1" ]; then
      $DIRNAME/$BASENAME xephyr $VM
    fi

    exit
  elif kvmx_running; then
    echo "$BASENAME: guest $VM is already running"

    if [ "$run_spice_client" == "1" ]; then
      kvmx spice $VM
      exit
    else
      exit 1
    fi
  fi

  if [ ! -z "$shared_folder" ]; then
    # Get absolute path of shared folder relative to project path
    mkdir -p $shared_folder
    shared_folder="`cd $KVMX_PROJECT_FOLDER && cd $shared_folder &> /dev/null && pwd`"
    # Requires samba package installed in the host; see http://unix.stackexchange.com/a/183609
    #local shared="-net user,smb=$shared_folder"
    # See http://wiki.qemu-project.org/Documentation/9psetup
    local shared="-fsdev local,id=shared,path=$shared_folder,security_model=none -device virtio-9p-pci,fsdev=shared,mount_tag=shared"
  elif [ ! -z "$shared_folders" ]; then
    local old_ifs="$IFS"
    local shared_item
    local shared
    IFS=","
    for shared_item in $shared_folders; do
      local id="`echo $shared_item | cut -d ':' -f 1`"
      local shared_folder="`echo $shared_item | cut -d ':' -f 2`"
      local shared_folder_mountpoint="`echo $shared_item | cut -d ':' -f 3`"

      # Get absolute path of shared folder relative to project path
      mkdir -p $shared_folder
      shared_folder="`cd $KVMX_PROJECT_FOLDER && cd $shared_folder &> /dev/null && pwd`"

      shared="$shared -fsdev local,id=$id,path=$shared_folder,security_model=none -device virtio-9p-pci,fsdev=$id,mount_tag=$id"

      unset shared_folder
      unset shared_folder_mountpoint
      unset id
    done
    IFS="$old_ifs"
    unset shared_item
  fi

  if [ ! -z "$port_mapping" ]; then
    local hostfwd=",$port_mapping"
  fi

  # Check if image exists, create otherwise
  if [ ! -e "$image" ]; then
    if [ ! -z "$basebox" ]; then
      if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$basebox" ]; then
        baseimage="`kvmx list_image $basebox`"
        basekey="$(ls `dirname $baseimage`/ssh/$basebox.key)"

        if [ ! -e "$baseimage" ]; then
          echo "$BASENAME: could not find basebox $baseimage. Please create it first."
          exit 1
        fi

        if [ ! -z "$backing_file" ] && [ "$backing_file" == "1" ]; then
          echo "Creating image $image as an overlay of $baseimage..."
          baseimage_format="`qemu-img info $baseimage | grep "^file format: " | cut -d : -f 2 | sed -e 's/ //g'` "
          qemu-img create -o backing_file=$baseimage,backing_fmt=$baseimage_format -f $format $image
        else
          echo "Copying base image $baseimage to $image..."
          if which rsync &> /dev/null; then
            rsync -ah --sparse --progress $baseimage $image
          else
            # See https://rwmj.wordpress.com/2010/10/19/tip-making-a-disk-image-sparse/
            cp --sparse=always $baseimage $image
          fi
        fi

        if [ -e "$basekey" ]; then
          vmname="$(basename $STORAGE)"
          imagekey="$STORAGE/ssh/$vmname.key"
          mkdir -p "$STORAGE/ssh"

          cp $basekey     $imagekey
          cp $basekey.pub $imagekey.pub

          # Re-evaluate this if there's a custom SSH key.
          __kvmx_ssh_command $basekey
        fi

        local wait="y"
      else
        echo "$BASENAME: basebox $basebox not available."
        exit 1
      fi
    else
      local wait="y"
      kvmx_create
    fi

    if [ "$wait" == "y" ]; then
      echo "Waiting before starting the new guest..."
      sleep 10
    fi
  fi

  if [ -z "$graphics" ]; then
    graphics="-vga qxl"
  fi

  if [ -z "$memory" ]; then
    memory="2048"
  fi

  if [ -z "$smp" ]; then
    smp="2"
  fi

  if [ -z "$drive_interface" ]; then
    drive_interface="virtio"
  fi

  if [ -z "$nic_model" ]; then
    nic_model="virtio"
  fi

  if [ -z "$shared_folder_msize" ]; then
    shared_folders_msize="33554432"
  fi

  if [ -z "$shared_folders_cache" ]; then
    shared_folders_cache="none"
  fi

  if [ ! -z "$cdrom" ]; then
    cdrom_opts="-cdrom $cdrom"
  fi

  if [ ! -z "$boot" ]; then
    boot_opts="-boot $boot"
  fi

  if [ ! -z "$usb" ] && [ "$usb" != "0" ]; then
    # Basic USB support
    if [ "$usb" == "1" ]; then
      usb_opts="-usb"
    fi

    # USB support with 1.0 and 2.0 hub
    if [ "$usb" == "2" ]; then
      usb_opts="-usb -device usb-ehci,id=ehci -device usb-host,bus=ehci.0,vendorid=1452"
    fi

    # USB support with 1.0, 2.0 and 3.0 hubs
    if [ "$usb" == "3" ]; then
      usb_opts="-usb -device usb-ehci,id=ehci -device usb-host,bus=ehci.0,vendorid=1452 -device qemu-xhci,id=xhci"
    fi
  fi

  # Check kvm version
  if kvm --help | grep -q -- "^-balloon"; then
    local new_qemu="0"
  else
    local new_qemu="1"
  fi

  if [ -z "$net" ] || [ "$net" == "user" ]; then
    if [ "$new_qemu" == "0" ]; then
      #net_opts="user,hostfwd=tcp:127.0.0.1:$SSH-:22,hostfwd=udp:127.0.0.1:$XDMCP_PORT-:177$hostfwd -net nic,model=$nic_model"
      net_opts="user,hostfwd=tcp:127.0.0.1:$SSH-:22$hostfwd -net nic,model=$nic_model"
    else
      net_opts="user,id=net0,hostfwd=tcp:127.0.0.1:$SSH-:22$hostfwd -net nic,netdev=net0,model=$nic_model"
    fi
  elif [ "$net" == "tap" ]; then
    # Thanks to kvm-manager
    tap="${VM}0"
    # MAC address is derived from a hash of the host's name and the guest's name:
    mac_address="$(printf "02:%s" "$(printf "%s\0%s" "$(hostname)" "${VM}" | sha256sum | sed 's/\(..\)/\1:/g' | cut -f1-5 -d:)")"
    bridge="br0"

    if [ "$new_qemu" == "0" ]; then
      net_opts="tap,ifname=$tap,script=no,downscript=no,vlan=0,name=hostnet0 -device virtio-net-pci,vlan=0,id=net0,mac=$mac_address,bus=pci.0"
    else
      #net_opts="tap,ifname=$tap,script=no,downscript=no,id=n1 -device virtio-net-pci,netdev=n1,id=net0,mac=$mac_address,bus=pci.0"
      net_opts="tap,ifname=$tap,script=no,downscript=no,id=net0 -device virtio-net-pci,netdev=net0,mac=$mac_address,bus=pci.0"
    fi
  fi

  if [ ! -z "$net_dns" ] && [ "$net_dns" != "host" ]; then
    net_opts="$net_opts,dns=$net_dns"
  fi

  # Check screen version
  if screen --help | grep -q -- "-Logfile"; then
    local screen_log="-L -Logfile"
  else
    local screen_log="-L"
  fi

  # Additional net and balloon options depending on qemu version
  #if kvm --help | grep -q -- "^-balloon"; then
  if [ "$new_qemu" == "0" ]; then
    local balloon="-balloon virtio"
    net_opts="-net $net_opts"
  else
    local balloon="-device virtio-balloon"
    net_opts="-netdev $net_opts"
  fi

  # Always run spice using a socket to provide some GUI isolation between guest
  # Otherwise any guest could open a spice connection to another guest using the host local IP (10.0.2.2) and the other guest spice port
  if [ -z "$spice" ] || [ "$spice" == "1" ]; then
    #spice_opts="-spice port=$PORT,addr=127.0.0.1,disable-ticketing,streaming-video=off,jpeg-wan-compression=never,playback-compression=off,zlib-glz-wan-compression=never,image-compression=off"
    spice_opts="-spice unix=on,addr=$SPICESOCKET,disable-ticketing=on,streaming-video=off,jpeg-wan-compression=never,playback-compression=off,zlib-glz-wan-compression=never,image-compression=off"
    spice_opts="$spice_opts -device virtio-serial-pci"
    spice_opts="$spice_opts -device virtserialport,chardev=spicechannel0,name=com.redhat.spice.0"
    spice_opts="$spice_opts -chardev spicevmc,id=spicechannel0,name=vdagent"
  fi

  if [ "$sound" != "0" ]; then
    if [ -z "$sound" ]; then
      sound="AC97"
    fi

    sound_opts="-device $sound"
  fi

  if [ ! -z "$image_drive" ] && [ "$image_drive" == "cdrom" ]; then
    image_opts="-cdrom $image"
  else
    image_opts="-drive file=$image,if=$drive_interface"

    # TRIM support
    if [ "$image_discards" != "n" ]; then
      image_opts="$image_opts,discard=unmap"
    fi
  fi

  if [ ! -z "$virtio_rng" ]; then
    rng_opts="-device virtio-rng-pci,$virtio_rng"
  else
    rng_opts="-device virtio-rng-pci,max-bytes=128,period=1000"
  fi

  # USB redirect support
  # See https://people.freedesktop.org/~teuf/spice-doc/html/ch02s06.html
  #usb_opts="-device ich9-usb-ehci1,id=usb"
  #usb_opts="$usb_opts -device ich9-usb-uhci1,masterbus=usb.0,firstport=0,multifunction=on"
  #usb_opts="$usb_opts -device ich9-usb-uhci2,masterbus=usb.0,firstport=2"
  #usb_opts="$usb_opts -device ich9-usb-uhci3,masterbus=usb.0,firstport=4"
  #usb_opts="$usb_opts -chardev spicevmc,name=usbredir,id=usbredirchardev1"
  #usb_opts="$usb_opts -device usb-redir,chardev=usbredirchardev1,id=usbredirdev1"
  #usb_opts="$usb_opts -chardev spicevmc,name=usbredir,id=usbredirchardev2"
  #usb_opts="$usb_opts -device usb-redir,chardev=usbredirchardev2,id=usbredirdev2"
  #usb_opts="$usb_opts -chardev spicevmc,name=usbredir,id=usbredirchardev3"
  #usb_opts="$usb_opts -device usb-redir,chardev=usbredirchardev3,id=usbredirdev3"

  # Run virtual machine, nohup approach
  # See https://en.wikipedia.org/wiki/Nohup#Overcoming_hanging
  #nohup setsid kvm -m $memory -name $VM                                                                  \
  #  -chardev "socket,id=monitor,path=$MONITORFILE,server=on,wait=off" -mon chardev=monitor,mode=readline \
  #  -chardev "socket,id=serial0,path=$CONSOLEFILE,server=on,wait=off" -device isa-serial,chardev=serial0 \
  #  -smp $smp -cpu host                                                                                  \
  #  $balloon                                                                                             \
  #  $graphics $shared                                                                                    \
  #  $image_opts                                                                                          \
  #  $spice_opts                                                                                          \
  #  $sound_opts                                                                                          \
  #  $cdrom_opts                                                                                          \
  #  $boot_opts                                                                                           \
  #  $net_opts                                                                                            \
  #  $rng_opts                                                                                            \
  #  $qemu_opts &> $LOGFILE < /dev/null &

  # Run virtual machine, screen approach
  # This is more immune to hangups
  screen $screen_log $LOGFILE -S kvmx-qemu-$VM -d -m kvm -m $memory -name $VM                         \
    -chardev "socket,id=monitor,path=$MONITORFILE,server=on,wait=off" -mon chardev=monitor,mode=readline \
    -chardev "socket,id=serial0,path=$CONSOLEFILE,server=on,wait=off" -device isa-serial,chardev=serial0 \
    -smp $smp -cpu host                                                                               \
    $balloon                                                                                          \
    $graphics $shared                                                                                 \
    $image_opts                                                                                       \
    $spice_opts                                                                                       \
    $sound_opts                                                                                       \
    $cdrom_opts                                                                                       \
    $boot_opts                                                                                        \
    $net_opts                                                                                         \
    $rng_opts                                                                                         \
    $usb_opts                                                                                         \
    -pidfile $PIDFILE                                                                                 \
    -D $LOGFILE                                                                                       \
    $qemu_opts

  # Only if nohup approach is being used
  #PID="$!"

  # Save state
  # Save PID here only if nohup approach is being used
  #echo $PID           > $PIDFILE
  echo $PORT          > $PORTFILE
  echo $SSH           > $SSHFILE
  echo $GUEST_DISPLAY > $DISPLAYFILE
  echo $XDMCP_PORT    > $XDMCPPORTFILE

  # Give time to qemu
  sleep 1

  # Thanks kvm-manager code for that portion
  /usr/bin/screen -D -m $screen_log $LOG_DIR/servicelog \
                  -c $APP_BASE/share/screen/screenrc    \
                  -S "kvmx-$VM" -t "kvmx-$VM" socat STDIO,raw,echo=0 "UNIX:${CONSOLEFILE},retry=30" &

  if [ "$run_spice_client" == "1" ]; then
    if [ "$spice" == "0" ]; then
      echo "$BASENAME: spice is disabled for guest $VM"
    else
      kvmx_spice
    fi
  fi

  # Check if on install mode
  if [ "$install" == "1" ]; then
    kvmx_status
    return
  fi

  if [ "$ssh_support" == "y" ]; then
    let ssh_attempts="0"
    echo -n "Waiting for machine to boot..."
    while true; do
      echo true | $SSH_COMMAND -o ConnectTimeout=2 -o NumberOfPasswordPrompts=0 -p $SSH 127.0.0.1 &> /dev/null && break
      echo -n "."
      let ssh_attempts++

      if [ "$ssh_attempts" == "20" ]; then
        echo "$BASENAME: timeout or access denied when trying to SSH into $VM."
        echo "$BASENAME: please check if the image is in a good state and if it accepts passwordless ssh connections"
        #kvmx_stop
        kvmx_poweroff
        exit 1
      fi

      sleep 8
    done
    echo " done."
    #sleep 5
    #echo ""

    kvmx_hostname

    # Somehow it is starting before DBUS and then crashing, so we try to start again
    echo "Ensure spice-vdagent is running..."
    echo "sudo /usr/sbin/service spice-vdagent start" | kvmx_ssh

    # See https://www.kernel.org/doc/Documentation/filesystems/9p.txt
    if [ ! -z "$shared_folder" ] && [ ! -z "$shared_folder_mountpoint" ]; then
      echo "Mounting $shared_folder on $shared_folder_mountpoint on guest..."
      echo "sudo mkdir -p $shared_folder_mountpoint" | kvmx_ssh
      echo "sudo mount -t 9p -o trans=virtio,msize=$shared_folders_msize shared $shared_folder_mountpoint -oversion=9p2000.L,posixacl,cache=$shared_folders_cache -o sync -o dirsync" | kvmx_ssh
      #echo "sudo mount //10.0.2.4/qemu $shared_folder_mountpint" | kvmx_ssh
    elif [ ! -z "$shared_folders" ]; then
      local old_ifs="$IFS"
      local shared_item
      IFS=","
      for shared_item in $shared_folders; do
        local id="`echo $shared_item | cut -d ':' -f 1`"
        local shared_folder="`echo $shared_item | cut -d ':' -f 2`"
        local shared_folder_mountpoint="`echo $shared_item | cut -d ':' -f 3`"

        # Get absolute path of shared folder relative to project path
        shared_folder="`cd $KVMX_PROJECT_FOLDER && cd $shared_folder &> /dev/null && pwd`"

        # Restore IFS for a while or kvmx_ssh won't work
        IFS="$old_ifs"
        echo "Mounting $shared_folder on $shared_folder_mountpoint ($id) on guest using 9p..."
        echo "sudo mkdir -p $shared_folder_mountpoint" | kvmx_ssh
        echo "sudo mount -t 9p -o trans=virtio,msize=$shared_folders_msize $id $shared_folder_mountpoint -oversion=9p2000.L,posixacl,cache=$shared_folders_cache -o sync -o dirsync" | kvmx_ssh
        IFS=","

        unset shared_folder
        unset shared_folder_mountpoint
        unset id
      done
      IFS="$old_ifs"
      unset shared_item
    fi

    # Shall we add an umount hook when powering off the guest?
    if [ ! -z "$shared_folders_sshfs" ]; then
      local old_ifs="$IFS"
      local shared_item
      IFS=","
      for shared_item in $shared_folders_sshfs; do
        local id="`echo $shared_item | cut -d ':' -f 1`"
        local shared_folder="`echo $shared_item | cut -d ':' -f 2`"
        local shared_folder_mountpoint="`echo $shared_item | cut -d ':' -f 3`"

        # Temporaly reset IFS so kvmx_sshfs executes correctly
        IFS="$old_ifs"
        echo "Mounting $shared_folder on $shared_folder_mountpoint ($id) on host using SSHFS..."
        kvmx_sshfs $shared_folder $shared_folder_mountpoint
        IFS=","

        unset shared_folder
        unset shared_folder_mountpoint
        unset id
      done
      IFS="$old_ifs"
      unset shared_item
    fi

    if [ "$xrandr" == "1" ] && [ "$run_spice_client" == "1" ]; then
      echo "Waiting for X11 to come up so we can set machine resolution..."
      sleep 8
      kvmx_xrandr
    fi
  fi

  if [ "$run_xpra" == "1" ]; then
    $DIRNAME/$BASENAME xpra $VM
  fi

  if [ "$run_xephyr" == "1" ]; then
    $DIRNAME/$BASENAME xephyr $VM
  fi

  if [ ! -z "$startup_command" ] && [ "$ssh_support" == "y" ]; then
    echo "Running $startup_command..."
    echo "nohup $startup_command" | kvmx ssh $VM &> /dev/null &
  fi

  if [ ! -z "$startup_rsync_to_guest" ] && [ "$ssh_support" == "y" ]; then
    local old_ifs="$IFS"
    local item
    IFS=","
    for item in $startup_rsync_to_guest; do
      if [ -z "$item" ]; then
        continue
      fi

      local id="`echo $item | cut -d ':' -f 1`"
      local startup_rsync_to_guest_orig="`echo $item | cut -d ':' -f 2`"
      local startup_rsync_to_guest_dest="`echo $item | cut -d ':' -f 3`"

      if [ -z "$id" ] || [ -z "$startup_rsync_to_guest_orig" ] || [ -z "$startup_rsync_to_guest_dest" ]; then
        continue
      fi

      echo "Rsyncing to guest: $startup_rsync_to_guest ($id)..."
      kvmx_rsync_to $startup_rsync_to_guest_orig $startup_rsync_to_guest_dest

      unset startup_rsync_to_guest_orig
      unset startup_rsync_to_guest_dest
      unset id
    done
    IFS="$old_ifs"
    unset item
  fi

  kvmx_status
}

# Set hostname
function kvmx_hostname {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  if [ "$ssh_support" != "y" ]; then
    echo "$BASENAME: ssh_support is disabled for guest $VM"
    exit 1
  fi

  echo "Setting hostname..."
  $SSH_COMMAND -o ConnectTimeout=2 -p $SSH 127.0.0.1 <<EOF
  ##### BEGIN REMOTE SCRIPT #####
  OLD_HOST="\$(hostname)"

  # Set hosts entry
  if ! grep -q "^127.0.0.1 $hostname.$domain $hostname$" /etc/hosts; then
    echo "127.0.0.1 $hostname.$domain $hostname" | sudo tee -a /etc/hosts &> /dev/null
  fi

  echo "$hostname.$domain" | sudo tee /etc/hostname &> /dev/null
  sudo hostname $hostname.$domain 2> /dev/null

  # Remove old hostname from hosts file
  if [ "\$OLD_HOST" != "$hostname.$domain" ]; then
    if grep -q \$OLD_HOST /etc/hosts; then
      sudo sed -i -e "/\$OLD_HOST/d" /etc/hosts 2> /dev/null
    fi
  fi
  ##### END REMOTE SCRIPT #######
EOF
}

# Display usage
function kvmx_usage {
  echo "$BASENAME $VERSION - virtual machine manager"
  echo ""
  echo "usage: $BASENAME <action> [vm] [options]"
  echo ""
  echo "available actions:"
  echo ""
  grep "^function kvmx_" $0 | cut -d ' ' -f 2 | sed -e 's/kvmx_//' | sort | xargs -L 6 | column -t -c 6 | sed -e 's/^/\t/'
  echo ""
  echo "examples:"
  echo ""
  echo -e "\t$BASENAME list"
  echo -e "\t$BASENAME init  <machine>    [folder]"
  echo -e "\t$BASENAME clone <orig-guest> <dest-guest-or-folder>"
  echo -e "\t$BASENAME ssh   <machine>    -X firefox"
  echo ""

  local list="`kvmx_list`"

  if [ ! -z "$list" ]; then
    echo "available virtual machines:"
    echo ""
    echo "$list" | sed -e 's/^/\t/'
    echo ""
  fi

  exit 1
}

# Log into the guest using SSH
function kvmx_ssh {
  if [ "$ssh_support" != "y" ]; then
    echo "$BASENAME: SSH support for $VM is disabled"
    exit 1
  fi

  if ! kvmx_running || kvmx_suspended; then
    echo "$BASENAME: $VM not running, trying to start it..."
    kvmx up $VM || exit 1
    #kvmx_up || exit 1
    #echo "$BASENAME: guest $VM is not running"
    #exit 1
  fi

  # Shift params according to how the program was called:
  # either "kvmx ssh" or "kvmx ssh guest".
  #if [ "$ACTION" == "ssh" ]; then
  #  if [ ! -z "$2" ]; then
  #    shift 2
  #  else
  #    shift 1
  #  fi
  #fi

  SSH="`cat $SSHFILE`"
  #TERM=xterm $SSH_COMMAND -p $SSH 127.0.0.1 $*
  $ssh_env $SSH_COMMAND -p $SSH 127.0.0.1 $*
}

# Enhanced SSH login into the guest
function kvmx_login {
  # This allows the usage of a custom login command
  #
  # It's not implemented directly in kvmx_ssh because it conflicts with use
  # cases where SSH commands are read from stdin and not from positional
  # arguments, like `echo something | kvmx_ssh`.
  if [ -z "$1" ] && [ ! -z "$ssh_login_command" ]; then
    # This needs the -t option to request a pseudo-terminal
    kvmx_ssh -t $ssh_login_command
  else
    kvmx_ssh $*
  fi
}

# Get guest SSH key fingerprints
function kvmx_ssh_finger {
  if ls $DATADIR/ssh/*.pub.* &> /dev/null; then
    for finger in $DATADIR/ssh/*.pub.*; do
      cat $finger
    done
  else
    # Try to get list of keys in the server
    keys="$(
    echo | kvmx_ssh << EOF
    for key in /etc/ssh/*pub; do
      echo \$key
      #ssh-keygen -l        -f \$key
      #ssh-keygen -l -E md5 -f \$key
    done
EOF
)"

    # Get fingerprint for each key
    if [ ! -z "$keys" ]; then
      for key in $keys; do
        fingerprint="$(echo ssh-keygen -l -f $key | kvmx_ssh)"
        echo $fingerprint | tee $DATADIR/ssh/`basename $key`.sha256

        fingerprint="$(echo ssh-keygen -l -E md5 -f $key | kvmx_ssh)"
        echo $fingerprint | tee $DATADIR/ssh/`basename $key`.md5
      done
    else
      echo "$BASENAME: could not get SSH fingerprints for $VM"
      exit 1
    fi
  fi
}

# Mount a guest folder into the host using sshfs
function kvmx_sshfs {
  local folder="$1"
  local mountpoint="$2"

  if [ -z "$mountpoint" ]; then
    kvmx_usage
  fi

  if ! which sshfs &> /dev/null; then
    echo "$BASENAME: this action requires sshfs to be installed on your system."
    exit 1
  fi

  SSH="`cat $SSHFILE`"

  # See https://github.com/libfuse/sshfs/issues/82 about "-o writeback_cache=no"
  #     http://www.admin-magazine.com/HPC/Articles/Sharing-Data-with-SSHFS
  sshfs $SSH_LOGIN@127.0.0.1:$folder $mountpoint $SSH_OPTS -o nonempty               \
                                                           -o sshfs_sync             \
                                                           -o sync_readdir           \
                                                           -o cache=no               \
                                                           -o transform_symlinks     \
                                                           -o sync_read              \
                                                           -o workaround=none        \
                                                           -o noforget               \
                                                           -o reconnect              \
                                                           -o no_readahead           \
                                                           -o compression=no -p $SSH
}

# Get guest PID
function kvmx_pid {
  if [ -e "$PIDFILE" ]; then
    # QEMU might put weird things into pidfile, so we need a simple filter
    #cat $PIDFILE
    cut -d ' ' -f 1 $PIDFILE | head -1
  else
    return 1
  fi
}

# Suspend the virtual machine
function kvmx_suspend {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  PID="`kvmx_pid`"

  if [ -z "$PID" ]; then
    return 1
  fi

  kill -STOP $PID

  # Alternative
  #kvmx_monitor stop

  if [ -e "$SPICEFILE" ]; then
    SPICEPID="`cat $SPICEFILE`"

    if [ -z "$SPICEPID" ]; then
      return
    fi

    if ps $SPICEPID &> /dev/null; then
      kill $SPICEPID
    fi
  fi
}

# Check if a guest is running
function kvmx_running {
  if [ ! -e "$PIDFILE" ]; then
    return 1
  fi

  PID="`kvmx_pid`"

  if [ -z "$PID" ]; then
    return 1
  fi

  # Simpler check
  #ps $PID &> /dev/null

  # Better check were process should match a qemu binary
  ps -o command $PID | grep -q -E '(^kvm)|(^qemu)'

  return $?
}

# Check if a guest is running
function kvmx_suspended {
  if ! kvmx_running; then
    return 1
  else
    if ps -p $PID -o stat --no-headers | grep -q 'T'; then
      return 0
    else
      return 1
    fi
  fi
}

# Resume the guest
function kvmx_resume {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  PID="`kvmx_pid`"

  if [ -z "$PID" ]; then
    return 1
  fi

  kill -CONT $PID

  # Alternative
  #kvmx_monitor system_wakeup
}

# Poweroff the guest
function kvmx_poweroff {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  if [ ! -z "$poweroff_pre_command" ] && [ "$ssh_support" == "y" ]; then
    echo "Running $poweroff_pre_command..."
    echo "nohup $poweroff_pre_command" | kvmx ssh $VM &> /dev/null &
  fi

  if [ ! -z "$poweroff_rsync_from_guest" ] && [ "$ssh_support" == "y" ]; then
    local old_ifs="$IFS"
    local item
    IFS=","
    for item in $poweroff_rsync_from_guest; do
      if [ -z "$item" ]; then
        continue
      fi

      local id="`echo $item | cut -d ':' -f 1`"
      local poweroff_rsync_from_guest_orig="`echo $item | cut -d ':' -f 2`"
      local poweroff_rsync_from_guest_dest="`echo $item | cut -d ':' -f 3`"

      if [ -z "$id" ] || [ -z "$poweroff_rsync_from_guest_orig" ] || [ -z "$poweroff_rsync_from_guest_dest" ]; then
        continue
      fi

      echo "Rsyncing from guest: $poweroff_rsync_from_guest ($id)..."
      kvmx_rsync_from $poweroff_rsync_from_guest_orig $poweroff_rsync_from_guest_dest

      unset poweroff_rsync_from_guest_orig
      unset poweroff_rsync_from_guest_dest
      unset id
    done
    IFS="$old_ifs"
    unset item
  fi

  if [ "$run_xpra" == "1" ]; then
    $DIRNAME/$BASENAME xpra $VM stop
  fi

  if [ "$ssh_support" == "y" ]; then
    echo /usr/bin/sudo poweroff | kvmx_ssh &> /dev/null
  else
    kvmx_monitor system_powerdown
  fi

  kvmx_xephyr_stop

  let poweroff_attempts="0"
  echo -n "Waiting for machine to stop..."
  while true; do
    kvmx_running || break

    echo -n "."
    let poweroff_attempts++

    if [ "$poweroff_attempts" == "20" ]; then
      echo "$BASENAME: guest $VM is still running"
      echo "$BASENAME: please consider to stop it using \"kvmx $VM stop\""
      #kvmx_stop
      exit 1
    fi

    sleep 3
  done
  echo " done."
  #sleep 3
  #echo ""

  kvmx_status
}

# Alias for poweroff
function kvmx_down {
  kvmx_poweroff
}

# Alias for poweroff
function kvmx_shutdown {
  kvmx_poweroff
}

# Alias for stop
function kvmx_halt {
  kvmx_stop
}

# Hibernate
function kvmx_hibernate {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  if [ "$ssh_support" != "y" ]; then
    echo "$BASENAME: ssh_support is disabled for guest $VM"
    exit 1
  fi

  if ! kvmx_ssh test -s /swapfile; then
    echo "Seems like /swapfile is absent in the guest, aborting"
    exit 1
  fi

  # Currently 9p driver won't survive a reboot
  # Umount shared folders
  #if [ ! -z "$shared_folder" ] && [ ! -z "$shared_folder_mountpoint" ]; then
  #  echo "Umounting $shared_folder_mountpoint on guest..."
  #  echo "sudo umount $shared_folder_mountpoint" | kvmx_ssh

  #  if [ "$?" != "1" ]; then
  #    echo "Problem umounting $shared_folder_mountpoint, you might have errors when restoring"
  #  fi
  #elif [ ! -z "$shared_folders" ]; then
  #  local old_ifs="$IFS"
  #  local shared_item
  #  IFS=","
  #  for shared_item in $shared_folders; do
  #    local shared_folder_mountpoint="`echo $shared_item | cut -d ':' -f 3`"

  #    # Restore IFS for a while or kvmx_ssh won't work
  #    IFS="$old_ifs"
  #    echo "Umounting $shared_folder_mountpoint on guest..."
  #    echo "sudo umount $shared_folder_mountpoint" | kvmx_ssh

  #    if [ "$?" != "1" ]; then
  #      echo "Problem umounting $shared_folder_mountpoint, you might have errors when restoring"
  #    fi

  #    IFS=","
  #  done
  #  IFS="$old_ifs"
  #fi

  echo "which s2disk &> /dev/null && /usr/bin/sudo s2disk" | kvmx_ssh &> /dev/null

  echo "Checking if hibernation was successful..."
  sleep 3

  if kvmx_running; then
    echo "Unable to hibernate guest: please check guest configuration"
    exit 1
  else
    kvmx_xephyr_stop
    kvmx_status
  fi
}

# Reboot the guest
function kvmx_reboot {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  echo /usr/bin/sudo reboot | kvmx_ssh &> /dev/null
  sleep 3
  kvmx_status
}

# Rsync files to the guest
function kvmx_rsync_to {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  if [ "$ssh_support" != "y" ]; then
    echo "$BASENAME: ssh_support is disabled for guest $VM"
    exit 1
  fi

  ORIG="$1"
  DEST="$2"

  if [ -z "$ORIG" ]; then
    echo "usage $BASENAME rsync_to $GUEST <orig> [dest]"
    exit 1
  fi

  if [ -z "$DEST" ]; then
    # Error
    #exit 1

    # Assume same as origin
    DEST="$ORIG"
  fi

  # Fix ~/ path
  if echo $DEST | grep -q -e "^$HOME"; then
    DEST="$(echo $DEST | sed -e "s|^$HOME|/home/$SSH_LOGIN|")"
  fi

  SSH="`cat $SSHFILE`"
  rsync -av --delete -e "$SSH_COMMAND -o Port=$SSH" --rsync-path "sudo rsync" $ORIG/ 127.0.0.1:$DEST/
}

# Rsync files from the guest
function kvmx_rsync_from {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  if [ "$ssh_support" != "y" ]; then
    echo "$BASENAME: ssh_support is disabled for guest $VM"
    exit 1
  fi

  ORIG="$1"
  DEST="$2"

  if [ -z "$ORIG" ]; then
    echo "usage $BASENAME rsync_from $GUEST <orig> [dest]"
    exit 1
  fi

  if [ -z "$DEST" ]; then
    # Error
    #exit 1

    # Assume same as origin
    DEST="$ORIG"
  fi

  # Fix ~/ path
  if echo $ORIG | grep -q -e "^$HOME"; then
    ORIG="$(echo $ORIG | sed -e "s|^$HOME|/home/$SSH_LOGIN|")"
  fi

  SSH="`cat $SSHFILE`"
  rsync -av --delete -e "$SSH_COMMAND -o Port=$SSH" --rsync-path "sudo rsync" 127.0.0.1:$ORIG/ $DEST/
}

# Copy files from the guest
function kvmx_scp_from {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  if [ "$ssh_support" != "y" ]; then
    echo "$BASENAME: ssh_support is disabled for guest $VM"
    exit 1
  fi

  ORIG="$1"
  DEST="$2"

  if [ -z "$DEST" ]; then
    # Error
    #exit 1

    # Assume same as origin
    DEST="$ORIG"
  fi

  # Fix ~/ path
  if echo $ORIG | grep -q -e "^$HOME"; then
    ORIG="$(echo $ORIG | sed -e "s|^$HOME|/home/$SSH_LOGIN|")"
  fi

  SSH="`cat $SSHFILE`"
  $SCP_COMMAND -o Port=$SSH 127.0.0.1:$ORIG $DEST
}

# Copy files to the guest
function kvmx_scp_to {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  if [ "$ssh_support" != "y" ]; then
    echo "$BASENAME: ssh_support is disabled for guest $VM"
    exit 1
  fi

  ORIG="$1"
  DEST="$2"

  if [ -z "$DEST" ]; then
    # Error
    #exit 1

    # Assume same as origin
    DEST="$ORIG"
  fi

  # Fix ~/ path
  if echo $DEST | grep -q -e "^$HOME"; then
    DEST="$(echo $DEST | sed -e "s|^$HOME|/home/$SSH_LOGIN|")"
  fi

  SSH="`cat $SSHFILE`"
  $SCP_COMMAND -o Port=$SSH $ORIG 127.0.0.1:$DEST
}

# List guests
function kvmx_list {
  if [ -e "$GLOBAL_USER_CONFIG_FOLDER" ]; then
    ls -1 $GLOBAL_USER_CONFIG_FOLDER | xargs -L 6 | column -t -c 6
  fi
}

# Alias to list command
function kvmx_ls {
  kvmx_list
}

# Upgrade guest
function kvmx_upgrade {
  echo "sudo apt-get update && sudo apt-get dist-upgrade -y && sudo apt-get autoremove -y" | kvmx_ssh
}

# Initializes a new guest
function kvmx_init {
  FOLDER="$1"

  if [ -z "$FOLDER" ]; then
    if [ -z "$VM" ]; then
      VM="$(basename `pwd`)"
      FOLDER="$(dirname `pwd`)/$VM"
    else
      FOLDER="$(pwd)/$VM"
    fi
  else
    VM="$FOLDER"

    if [ ! -z "$2" ]; then
      FOLDER="$2"
    else
      FOLDER="$(pwd)/$VM"
    fi
  fi

  if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$VM" ]; then
    echo "$BASENAME: guest $VM already exists"
    exit 1
  fi

  if [ ! -d "$FOLDER" ]; then
    mkdir -p $FOLDER
  fi

  # Ensure we have an absolute folder name
  FOLDER="`cd $FOLDER &> /dev/null && pwd`"

  # Copy config from template
  if [ ! -e "$FOLDER/kvmxfile" ]; then
    cp $APP_BASE/kvmxfile $FOLDER/
    sed -i -e "s|hostname=\"machine\"|hostname=\"$VM\"|g" $FOLDER/kvmxfile
    sed -i -e "s|hostname=\"kvmx\"|hostname=\"$VM\"|g" $FOLDER/kvmxfile
  fi

  # Create config entry
  __kvmx_create_config_entry
}

# Clone a guest
function kvmx_clone {
  if kvmx_running; then
    echo "$BASENAME: orig $VM is running, cannot clone."
    exit 1
  fi

  FOLDER="$1"
  DEST="`basename $FOLDER`"
  OPT="$2"

  if [ -z "$FOLDER" ]; then
    kvmx_usage
  fi

  # If dest is given without a full path, clone to the same basedir
  # as the original guest.
  if [ "$FOLDER" == "$DEST" ]; then
    FOLDER="`dirname $STORAGE`/$DEST"
  fi

  # Check if dest machine exists
  if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$DEST" ]; then
    echo "$BASENAME: destination guest $DEST already exists."
    exit 1
  fi

  if [ -d "$FOLDER" ]; then
    echo "$BASENAME: destination $FOLDER already exists."
    exit 1
  fi

  # Ensure we have an absolute folder name
  mkdir -p $FOLDER
  FOLDER="`cd $FOLDER &> /dev/null && pwd`"
  rmdir $FOLDER

  # Copy image and configuration
  echo "Copying basebox..."
  if which rsync &> /dev/null; then
    if [ "$OPT" == "--skell" ]; then
      local exclude="--exclude=box.img"
    fi

    rsync -ah --sparse $exclude $STORAGE/ $FOLDER/
  else
    cp -r --sparse=always $STORAGE/ $FOLDER/
  fi

  # Remove old state folder
  rm -rf $FOLDER/state/*

  # Copy kvmxfile if not present on $FOLDER
  if [ ! -e "$FOLDER/kvmxfile" ]; then
    cat $GLOBAL_USER_CONFIG_FOLDER/$VM > $FOLDER/kvmxfile
  fi

  # Create config entry
  ( cd $GLOBAL_USER_CONFIG_FOLDER && ln -s $FOLDER/kvmxfile $DEST )

  # Update config file
  new_image="$FOLDER/`basename $image`"
  sed -i -e "s|image=\"$image\"|image=\"$new_image\"|g" $FOLDER/kvmxfile
  sed -i -e "s|hostname=\"$VM\"|hostname=\"$DEST\"|g"   $FOLDER/kvmxfile

  # Rename keypair if exists
  if [ -e "$FOLDER/ssh/$VM.key" ]; then
    mv $FOLDER/ssh/$VM.key     $FOLDER/ssh/$DEST.key
    mv $FOLDER/ssh/$VM.key.pub $FOLDER/ssh/$DEST.key.pub
  fi

  if [ "$DATADIR" != "$STORAGE" ]; then
    echo "$BASENAME: please copy datadir $DATADIR manually"
  fi
}

# Alias to clone
function kvmx_copy {
  kvmx_clone $*
}

# Get, set or edit guest config
function kvmx_config {
  if [ -z "$1" ]; then
    if [ -z "$EDITOR" ]; then
      EDITOR="vi"
    fi

    if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$VM" ]; then
      $EDITOR $GLOBAL_USER_CONFIG_FOLDER/$VM
    else
      echo "$BASENAME: $GLOBAL_USER_CONFIG_FOLDER/$VM: file not found."
    fi
  else
    #if [ -z "$2" ]; then
    #  echo "usage: $BASENAME $VM edit $1 <value>"
    #  exit 1
    #fi

    param="$1"
    shift

    if [ -z "$1" ]; then
      grep "^$param=" $KVMXFILE | \
        sed -e 's/="/=/' -e 's/"$//' -e "s/='/=/" -e "s/'$//" -e "s/^$param=//"
    elif ! grep -q "^$param=" $KVMXFILE; then
      echo "$param=\"$*\"" >> $KVMXFILE
    else
      sed -i -e "s#^$param=.*#$param=\"$*\"#" $KVMXFILE
    fi
  fi
}

# Unset a guest config by commenting it
function kvmx_config_unset {
  if [ ! -z "$1" ]; then
    sed -i -e "s/^$1=/#$1=/" $KVMXFILE
  else
    echo $BASENAME: missing config parameter
    exit 1
  fi
}

# Alias to config
function kvmx_edit {
  kvmx_config $*
}

# Stop a guest
function kvmx_stop {
  if kvmx_running; then
    PID="`kvmx_pid`"

    if [ -z "$PID" ]; then
      return 1
    fi

    kill $PID
    kvmx_xephyr_stop
  fi
}

# Kill a guest
function kvmx_kill {
  if kvmx_running; then
    PID="`kvmx_pid`"

    if [ -z "$PID" ]; then
      return 1
    fi

    kill -9 $PID
    kvmx_xephyr_stop
  fi
}

# Destroy a guest
function kvmx_destroy {
  #kvmx_stop

  if kvmx_running; then
    echo "$BASENAME: $VM is running, cannot destroy."
    exit 1
  fi

  rm -f  $image
  rm -rf $STATE_DIR

  echo "$BASENAME: removed image and state files, but not the whole $STORAGE folder."
}

# Shred a guest
function kvmx_shred {
  #kvmx_stop

  if kvmx_running; then
    echo "$BASENAME: $VM is running, cannot shred."
    exit 1
  fi

  if which shred &> /dev/null; then
    shred $image
    rm -f $image
  else
    echo "$BASENAME: error shreding $image: shred program not available."
    exit 1
  fi
}

# Wipe a guest
function kvmx_wipe {
  #kvmx_stop

  if kvmx_running; then
    echo "$BASENAME: $VM is running, cannot wipe."
    exit 1
  fi

  if which wipe &> /dev/null; then
    wipe -f $image
    rm   -f $image
  else
    echo "$BASENAME: error wiping $image: wipe program not available."
    exit 1
  fi
}

# Purge a guest and all its configuration
function kvmx_purge {
  kvmx_destroy
  rm -f $GLOBAL_USER_CONFIG_FOLDER/$VM
  echo "$BASENAME: removed $GLOBAL_USER_CONFIG_FOLDER/$VM config."
}

# Provision a machine
function kvmx_provision {
  if ! kvmx_running; then
    kvmx up $VM || exit 1
    #kvmx_up || exit 1
    #echo "$BASENAME: guest $VM is not running"
    #exit 1
  fi

  if [ "$ssh_support" != "y" ]; then
    echo "$BASENAME: ssh_support is disabled for guest $VM"
    exit 1
  fi

  if [ -z "$provision_command" ]; then
    echo "$BASENAME: error: parameter provision_command is not configured for $VM."
    exit 1
  fi

  echo "Syncing provision files into the guest..."

  # Always sync default provisioners
  SSH="`cat $SSHFILE`"
  ORIG="$KVMX_BASE/share/provision/"
  DEST="/usr/local/share/kvmx/provision/"

  echo "sudo mkdir -p `dirname $DEST`" | kvmx_ssh
  rsync -av --delete -e "$SSH_COMMAND -o Port=$SSH" $provision_rsync_opts --rsync-path "sudo rsync" $ORIG/ 127.0.0.1:$DEST/

  if [ ! -z "$provision_rsync" ]; then
    local old_ifs="$IFS"
    IFS=","

    for provision_item in $provision_rsync; do
      IFS="$old_ifs"
      ORIG="`echo $provision_item | cut -d ' ' -f 1`"
      DEST="`echo $provision_item | cut -d ' ' -f 2`"

      # Sync custom provisioners
      if [ "$ORIG" != "$KVMX_BASE/share/provision/" ] && [ "$DEST" != "/usr/local/share/kvmx/provision/" ]; then
        (
        # Go inside the project folder so a relative $ORIG works
        cd `dirname $KVMXFILE` &> /dev/null
        echo "sudo mkdir -p `dirname $DEST`" | kvmx_ssh
        rsync -av -e "$SSH_COMMAND -o Port=$SSH" $provision_rsync_opts --rsync-path "sudo rsync" $ORIG/ 127.0.0.1:$DEST/
        )
      fi
      IFS=","
    done

    IFS="$old_ifs"
  fi

  echo "Running provision command inside the guest..."
  echo "$provision_command $hostname $domain $mirror" | kvmx_ssh
}

# Print guest image file name
function kvmx_list_image {
  echo $image
}

# Print guest folder name
function kvmx_list_folder {
  echo $KVMX_PROJECT_FOLDER
}

# Print guest status
function kvmx_status {
  if kvmx_suspended; then
    echo "$BASENAME: $VM guest is suspended"
  elif kvmx_running; then
    echo "$BASENAME: $VM guest is running"
  else
    echo "$BASENAME: $VM guest is stopped"
    return
  fi

  PID="`kvmx_pid`"

  if [ -z "$PID" ]; then
    return 1
  fi

  ps $PID
}

# Print guest log
function kvmx_log {
  local logs=""

  if [ -s "$LOGFILE" ]; then
    logs="$logs $LOGFILE"
  fi

  if [ -s "$SPICELOG" ]; then
    logs="$logs $SPICELOG"
  fi

  if [ -s "$XPRALOG" ]; then
    logs="$logs $XPRALOG"
  fi

  if [ -s "$XDMCPLOG" ]; then
    logs="$logs $XDMCPLOG"
  fi

  if [ -z "$logs" ]; then
    echo "$BASENAME: $VM: all logs are empty"
    exit
  fi

  tail -F $logs
}

# Rotate SSH keys
function kvmx_rotate_sshkeys {
  # Generate new keypair
  mkdir -p "$DATADIR/ssh"
  SSHKEY="$DATADIR/ssh/$VM.key"
  __kvmx_ssh_keygen $SSHKEY.new "$user@`basename $image .img`"

  # Replace pubkey on server
  echo "touch ~/.ssh/authorized_keys.new && chmod 600 ~/.ssh/authorized_keys.new" | kvmx_ssh
  cat $SSHKEY.new.pub | kvmx_ssh "tee ~/.ssh/authorized_keys.new &> /dev/null"
  echo "mv ~/.ssh/authorized_keys.new ~/.ssh/authorized_keys" | kvmx_ssh

  # Replace keypair locally
  mv $SSHKEY.new     $SSHKEY
  mv $SSHKEY.new.pub $SSHKEY.pub
}

# Xpra integration
function kvmx_xpra {
  if ! which xpra &> /dev/null; then
    echo "$BASENAME: please install xpra package"
    exit 1
  fi

  if ! kvmx_running || kvmx_suspended; then
    echo "$BASENAME: $VM not running"
    exit 1
  fi

  local action="$1"
  shift

  SSH="`cat $SSHFILE`"

  if [ -z "$action" ]; then
    action="start"
  fi

  if [ "$action" == "start" ] || [ "$action" == "attach" ]; then
    nohup xpra $action --ssh="$SSH_COMMAND -p $SSH" ssh:127.0.0.1 $* &> $XPRALOG < /dev/null &
  else
    xpra $action --ssh="$SSH_COMMAND -p $SSH" ssh:127.0.0.1 $*
  fi
}

# X2Go integration
#function kvmx_x2go {
#  if ! which x2goclient &> /dev/null; then
#    echo "$BASENAME: please install x2goclient package"
#    exit 1
#  fi
#
#  if ! kvmx_running || kvmx_suspended; then
#    echo "$BASENAME: $VM not running"
#    exit 1
#  fi
#
#  SSH="`cat $SSHFILE`"
#
#  x2goclient --ssh-port=$SSH --ssh-key=$SSHKEY --session=$VM
#}

# Alias for up command
function kvmx_start {
  kvmx_up $*
}

# Alias for up command
function kvmx_run {
  kvmx_up $*
}

# Restart machine
function kvmx_restart {
  if ! kvmx_running; then
    echo "Guest $VM was not running, so starting it anyway..."
    kvmx_start
  else
    #if [ "$ssh_support" != "y" ]; then
    #  echo sudo reboot | kvmx_ssh
    #  exit
    #fi

    echo "Powering off guest $VM..."
    kvmx_poweroff

    echo "Starting guest $VM again..."
    kvmx_start
  fi
}

# Alias to restart
function kvmx_reboot {
  kvmx_restart $*
}

# Connect to the guest using VNC
function kvmx_vnc {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  GUEST_DISPLAY="`cat $DISPLAYFILE`"

  if [ -z "$vnc_client" ]; then
    vnc_client="virt-viewer"
  fi

  if which $vnc_client &> /dev/null; then
    if [ "$vnclient_client" == "virt-viewer" ]; then
      $vnc_client vnc://127.0.0.1:$GUEST_DISPLAY
    else
      GUEST_DISPLAY="`cat $DISPLAYFILE`"
      $vnc_client :$GUEST_DISPLAY
    fi
  else
    echo "$BASENAME: no vnc_client configured"
    exit 1
  fi
}

# Connect to the guest using XDMCP/Xephyr
# See http://jeffskinnerbox.me/posts/2014/Apr/29/howto-using-xephyr-to-create-a-new-display-in-a-window/
function kvmx_xephyr {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  # Clipboard sharing
  # https://ubuntuforums.org/showthread.php?t=1430363
  GUEST_DISPLAY="`cat $DISPLAYFILE`"
  XDMCP_PORT="`cat $XDMCPPORTFILE`"

  # Check for resolution configuration
  if [ ! -z "$resolution" ]; then
    resolution="-screen $resolution"
  else
    # Detect resolution dynamically
    if which xwininfo &> /dev/null; then
      resolution="`xwininfo -root | grep -- '-geometry' | cut -d '+' -f 1 | sed -e 's/-geometry//'`"

      # Check for resolution_x_offset and resolution_y_offset configuration
      if [ ! -z "$resolution_x_offset" ] || [ ! -z "$resolution_y_offset" ]; then
        local width="`echo $resolution  | cut -d 'x' -f 1`"
        local height="`echo $resolution | cut -d 'x' -f 2`"
        resolution="$(($width $resolution_x_offset))x$(($height $resolution_y_offset))"
      fi

      resolution="-screen $resolution"
    fi
  fi

  if ! which Xephyr &> /dev/null; then
    echo "$BASENAME: please install Xephyr"
    exit 1
  fi

  Xephyr :$GUEST_DISPLAY -to 3 -ac -port $XDMCP_PORT -query 127.0.0.1 $resolution &> $XDMCPLOG < /dev/null &

  XEPHYRPID="$!"
  echo "$XEPHYRPID" > $XEPHYRFILE

  # Give time to connect
  sleep 1

  # Set keyboard layout
  # Thanks https://unix.stackexchange.com/questions/304391/xephyr-keyboard-mapping-not-working-properly
  setxkbmap -display :0 -print | xkbcomp - :$GUEST_DISPLAY &> /dev/null

  # Fix window titles
  if which /usr/bin/xdotool &> /dev/null; then
    if [ ! -z "$xclient_windowmove" ]; then
      xdotool search --name "Xephyr on :" windowmove $xclient_windowmove
    fi

    xdotool search --name "Xephyr on :" set_window --name $VM
  fi
}

# Close Xephyr client
function kvmx_xephyr_stop {
  if [ ! -e "$XEPHYRFILE" ]; then
    if [ "$ACTION" == "xephyr_stop" ]; then
      echo "$BASENAME: Xephyr not running for guest $VM"
      exit 1
    else
      return
    fi
  fi

  XEPHYRPID="`cat $XEPHYRFILE`"

  if [ ! -z "$XEPHYRPID" ]; then
    kill $XEPHYRPID &> /dev/null
  fi
}

# Open a file inside a guest
function kvmx_open {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  ORIG="$1"

  if [ -z "$ORIG" ]; then
    echo "$BASENAME: missing file argument."
    exit 1
  elif [ ! -e "$ORIG" ]; then
    echo "$BASENAME: file not found: $ORIG"
    exit 1
  fi

  DEST_FOLDER="`kvmx ssh $VM /bin/mktemp -d`"
  DEST="$DEST_FOLDER/`basename $ORIG`"

  # Copy and open
  kvmx scp_to $VM $ORIG $DEST
  kvmx ssh    $VM DISPLAY=:0 /usr/bin/xdg-open $DEST

  # Copy back
  TMP_OPEN_FOLDER="`mktemp -d`"
  TMP_OPEN="$TMP_OPEN_FOLDER/`basename $ORIG`"
  kvmx scp_from $VM $DEST $TMP_OPEN

  # Check for changes
  if ! diff $TMP_OPEN $ORIG 2> /dev/null; then
    mv $TMP_OPEN $ORIG
  else
    rm -rf $TMP_OPEN_FOLDER
  fi

  # Remove from guest
  kvmx ssh $VM rm -rf $DEST_FOLDER
}

# Rename a guest
function kvmx_rename {
  if kvmx_running; then
    echo "$BASENAME: guest $VM is running"
    exit 1
  fi

  FOLDER="$1"
  DEST="`basename $FOLDER`"

  if [ -z "$FOLDER" ]; then
    kvmx_usage
  fi

  # If dest is given without a full path, rename to the same basedir
  # as the original guest.
  if [ "$FOLDER" == "$DEST" ]; then
    FOLDER="`dirname $STORAGE`/$DEST"
  fi

  # Check if dest machine exists
  if [ -e "$GLOBAL_USER_CONFIG_FOLDER/$DEST" ]; then
    echo "$BASENAME: destination guest $DEST already exists."
    exit 1
  fi

  if [ -d "$FOLDER" ]; then
    echo "$BASENAME: destination $FOLDER already exists."
    exit 1
  fi

  # Ensure we have an absolute folder name
  mkdir -p $FOLDER
  FOLDER="`cd $FOLDER &> /dev/null && pwd`"
  rmdir $FOLDER

  # Copy image and configuration
  echo "Renaming guest..."
  mv "$STORAGE" "$FOLDER"

  # Remove old state folder
  rm -rf $FOLDER/state/*

  # Create config entry
  ( cd $GLOBAL_USER_CONFIG_FOLDER && ln -s $FOLDER/kvmxfile $DEST )

  # Remove old kvmxfile
  rm $GLOBAL_USER_CONFIG_FOLDER/$VM

  # Update config file
  new_image="$FOLDER/`basename $image`"
  sed -i -e "s|image=\"$image\"|image=\"$new_image\"|g" $FOLDER/kvmxfile
  sed -i -e "s|hostname=\"$VM\"|hostname=\"$DEST\"|g"   $FOLDER/kvmxfile

  # Rename keypair if exists
  if [ -e "$FOLDER/ssh/$VM.key" ]; then
    mv $FOLDER/ssh/$VM.key     $FOLDER/ssh/$DEST.key
    mv $FOLDER/ssh/$VM.key.pub $FOLDER/ssh/$DEST.key.pub
  fi

  if [ "$DATADIR" != "$STORAGE" ]; then
    echo "$BASENAME: please move datadir $DATADIR manually"
  fi
}

# Alias to rename
function kvmx_move {
  kvmx_rename $*
}

# Alias to rename
function kvmx_mv {
  kvmx_rename $*
}

# Interface to QEMU monitor (thanks kvm-manager)
function kvmx_monitor {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  if ! which socat &> /dev/null; then
    echo "$BASENAME: please install socat"
    exit 1
  fi

  if [ -z "$1" ]; then
    socat $MONITORFILE STDIO
  else
    socat STDIO $MONITORFILE <<EOF
$*
EOF
  fi
}

# Install system
function kvmx_install {
  if kvmx_running; then
    echo "$BASENAME: guest $VM is running"
    exit 1
  fi

  local media="$1"

  if [ -z "$media" ]; then
    echo "usage: $BASENAME install $VM <installation-media>"

    # Possible places to find existing ISOs
    local candidates="/var/cache/media/distros /usr/local/share/isos"

    for candidate in $candidates; do
      if [ -d "$candidate" ]; then
        results="`find $candidate -not -iwholename '*.git*' -name '*.iso' | sed -e 's/^/\t/'`"

        if [ ! -z "$results" ]; then
          echo "available images at $candidate:"
          echo ""
          echo -n "$results"
        fi
      fi
    done

    exit 1
  elif [ ! -e "$media" ]; then
    echo "$BASENAME: file not found: $media"
    exit 1
  fi

  if [ -z "$memory" ]; then
    memory="2048"
  fi

  if [ -z "$format" ]; then
    format="qcow2"
  fi

  if [ ! -e "$image" ]; then
    echo "Creating $image with size $size..."
    qemu-img create -f $format $image $size
  fi

  # Basic install command
  #kvm -m $memory -net nic,model=virtio -net user -drive file=$image -cdrom $media

  # Install using kvmx_up
  install=1
  cdrom=$media
  boot="once=dc"
  kvmx_up
}

# Alias to install
function kvmx_boot {
  kvmx_install $*
}

# Serial console
function kvmx_console {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  # Script hack, useful for servers when using su before attaching to a console
  # See https://serverfault.com/questions/116775/sudo-as-different-user-and-running-screen
  #script /dev/null && screen -x kvmx-$VM
  #script -q -c "screen -x kvmx-$VM" /dev/null

  screen -x kvmx-$VM
}

# Alias to console
function kvmx_serial {
  kvmx_console $*
}

# Compress guest image
function kvmx_compress {
  if kvmx_running; then
    echo "$BASENAME: guest $VM is running"
    exit 1
  fi

  if [ ! -e "$image" ]; then
    echo "$BASENAME: image not found: $image"
    exit 1
  fi

  # Avoid trying to convert guest using other images such as LVM volumes
  if [ "$format" != "qcow2" ]; then
    echo "$BASENAME: please convert guest $VM image to qcow2 and update your config"
    exit 1
  fi

  if [ "$qcow2_compression" == "1" ]; then
    compression="-c"
  fi

  # Size before compression
  local size_before_bytes="`du    $image | awk '{ print $1 }'`"
  local size_before_human="`du -h $image | awk '{ print $1 }'`"

  echo "$BASENAME: compressing $image..."
  qemu-img convert -O qcow2 -p $compression $image $image.new && mv $image.new $image || exit 1

  # Size after compression
  local size_after_bytes="`du    $image | awk '{ print $1 }'`"
  local size_after_human="`du -h $image | awk '{ print $1 }'`"

  # Ratio
  #local ratio="$(($size_after_bytes / $size_before_bytes))"
  local ratio="$(echo "scale=4 ; $size_after_bytes / $size_before_bytes" | bc -l)"

  echo "size before: $size_before_human"
  echo "size_after:  $size_after_human"
  echo "compression ratio: $ratio"
}

# Version
function kvmx_version {
  echo $VERSION
}

# Shell
function kvmx_shell {
  local last_exit_code="0"
  local restricted="$1"
  local restricted_actions=":status:start:stop:poweroff:suspend:resume:console:monitor"
        restricted_actions="$restricted_actions:wipe:shred:app_base:version:list_image:kill:ssh_finger:"

  # While a "quit" command isn't entered, read STDIN
  while read -rep "$last_exit_code kvmx:/${USER}@${VM}> " STDIN; do
    history -s "$STDIN"

    if [ "$STDIN" == "quit" ] || [ "$STDIN" == "exit" ] || [ "$STDIN" == "bye" ]; then
      break
    elif [ "$STDIN" == "shell" ]; then
      echo "Why you need nesting?"
    elif [[ -n "$STDIN" && "$STDIN" != "#"* ]]; then
      # If line is not empty or commented, process command
      STDIN=($STDIN)

      # But check first if we're in a restricted shell
      if [ "$restricted" == "restricted" ]; then
        if ! echo $restricted_actions | grep -q ":${STDIN[0]}:"; then
          echo "Running in restricted shell mode."
          echo "Allowed commands are only `echo $restricted_actions | tr ':' ' '`"
        else
          $APP_BASE/kvmx ${STDIN[0]} $VM ${STDIN[@]:1}
          last_exit_code="$?"
        fi
      else
        $APP_BASE/kvmx ${STDIN[0]} $VM ${STDIN[@]:1}
        last_exit_code="$?"
      fi
    fi
  done
}

# Xrandr integration
function kvmx_xrandr {
  local size="$1"

  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  if [ "$ssh_support" != "y" ]; then
    echo "$BASENAME: xrandr needs ssh_support config"
    exit 1
  fi

  if echo which xrandr | kvmx_ssh &> /dev/null; then
    # Check for resolution configuration or explicit param
    if [ ! -z "$size" ]; then
      mode="`echo $size | tr 'x' ' '`"
    elif [ ! -z "$resolution" ]; then
      mode="`echo $resolution | tr 'x' ' '`"
    else
      # Set resolution according to the current active screen
      #local mode="`xrandr | grep '*+' | awk '{ print $1 }' | tr 'x' ' '`"

      # Set screen resolution depending on which screen the spice session is currently located
      local id="`xdotool search --name $VM`"

      # XrandR approach, matches the full screen size, not ideal when using
      # multiple monitors/outputs or if the window size is smaller than the
      # current screen size
      #local screen="`xdotool getwindowgeometry $id | grep screen: | cut -d '(' -f 2 | cut -d : -f 2 | cut -d ')' -f 1 | sed -e 's/ //'`"
      #local mode="`xrandr | grep "Screen ${screen}:" | cut -d , -f 2 | sed -e 's/ current //' -e 's/ //g' | tr 'x' ' '`"

      # Pure xdotool approach, matches the current window size
      # Better support for multiple monitors/outputs and for windows of arbitrary sizes
      local mode="`xdotool getwindowgeometry $id | grep -i geometry: | cut -d : -f 2 | tr 'x' ' '`"

      if [ ! -z "$resolution_y_offset" ]; then
        local x="`echo $mode | awk '{ print $1 }'`"
        local y="`echo $mode | awk '{ print $2 }'`"

        mode="$x $(($y $resolution_y_offset))"
      fi
    fi

    if [ -z "$xrandr_device" ]; then
      xrandr_device="Virtual-0"
    fi

    local line="`cvt $mode  | tail -1   | sed -e 's/^Modeline//'`"
    local name="`echo $line | awk '{ print $1 }'`"

    echo "Setting Modeline $line..."

    echo DISPLAY=:0 xrandr --newmode                       $line | kvmx_ssh
    echo DISPLAY=:0 xrandr --addmode $xrandr_device        $name | kvmx_ssh
    echo DISPLAY=:0 xrandr --output  $xrandr_device --mode $name | kvmx_ssh
  fi
}

# Wrapper to kvmx-create
function kvmx_create {
  if kvmx_running || kvmx_suspended; then
    echo "$BASENAME: guest $VM is running or suspended, cannot (re-)create"
    exit 1
  fi

  kvmx-create $KVMXFILE

  if [ "$?" != "0" ]; then
    exit $?
  fi
}

# Disposable guest
function kvmx_disposable {
  # Determine guest name by appending the current date
  # This approache leads to the following UNIX socket error
  # if the VM name is too long:
  #
  #   UNIX socket path '/var/cache/qemu/.../monitor' is too long
  #   Path must be less than 108 bytes
  #
  #local date="`date +%Y%m%d%I%M%S`"
  #local disposable="$VM-disposable-$date"

  local disposable="$VM-disposable-"
  local count=0

  # Determine guest name using a sequential suffix
  #
  # This results in smaller guest names which are les prone to
  # the UNIX socket path issue and also is easier to type
  while true; do
    disposable="${disposable}${count}"
    if ! kvmx list_image ${disposable} &> /dev/null; then
      break
    fi

    echo $count
    let count++
  done

  # Clone and ensure we use a backing file
  kvmx clone $VM $disposable --skell || exit 1
  echo "basebox=$VM"      >> $GLOBAL_USER_CONFIG_FOLDER/$disposable
  echo 'backing_file="1"' >> $GLOBAL_USER_CONFIG_FOLDER/$disposable

  kvmx up $disposable

  echo "Waiting for the VM $disposable to stop before erasing it..."

  local image="`kvmx list_image $disposable`"
  local folder="`dirname $image`"

  # Remove VM after it stopped
  while true; do
    if ! kvmx running $disposable; then
      kvmx purge $disposable
      rm -rf $folder
      exit
    fi

    sleep 10
  done
}

# Helper function to chose a device
function __kvmx_usb_devices_check {
  local total="`lsusb | wc -l`"

  # Check if USB is available in the system
  if [ "$total" == "0" ]; then
    echo "Sorry, but you don't have any available USB devices on your system."
    exit 1
  fi

  # Check for usb config option
  if [ -z "$usb" ] || [ "$usb" == "0" ]; then
    echo "You need to enable USB support into the kvmxfile."
    exit 1
  fi
}

# Helper function to list available USB devices
function __kvmx_usb_devices_list {
  # List available devices
  echo "Available devices:"
  echo ""
  lsusb | cat -n
  echo ""
}

# Helper function to choose an USB device
function __kvmx_usb_devices_choose {
  local total="`lsusb | wc -l`"

  # Read option
  read -rep "Choose a device (1-$total): " choice

  # Validate choice
  if (($choice < 1)) || (($choice > $total)); then
    echo "Invalid choice."
    exit 1
  fi

  echo $choice
}

# USB attach
function kvmx_usb_attach {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  # Get a device
  __kvmx_usb_devices_check
  __kvmx_usb_devices_list
  local choice="`__kvmx_usb_devices_choose`"

  # Get bus, device, vendor and product IDs
  local option="`lsusb | cat -n | grep \"^     $choice\"`"
  local bus="`echo $option     | awk '{ print $3 }'`"
  local device="`echo $option  | awk '{ print $5 }' | cut -d ':' -f 1`"
  local vendor="`echo $option  | awk '{ print $7 }' | cut -d ':' -f 1`"
  local product="`echo $option | awk '{ print $7 }' | cut -d ':' -f 2`"

  # Give permission to the device
  echo "Changing ownership of /dev/bus/usb/${bus}/${device} to you..."
  sudo chown `whoami` /dev/bus/usb/$bus/$device

  # Attach into the guest
  echo "Attaching device ${vendor}:${product} into the guest..."
  kvmx_monitor device_add usb-host,vendorid=0x${vendor},productid=0x${product},id=usb-0x${vendor}-0x${product}
}

# USB detach
function kvmx_usb_detach {
  if ! kvmx_running; then
    echo "$BASENAME: guest $VM is not running"
    exit 1
  fi

  # Get a device
  __kvmx_usb_devices_check
  __kvmx_usb_devices_list
  local choice="`__kvmx_usb_devices_choose`"

  # Get bus, device, vendor and product IDs
  local option="`lsusb | cat -n | grep \"^     $choice\"`"
  local bus="`echo $option     | awk '{ print $3 }'`"
  local device="`echo $option  | awk '{ print $5 }' | cut -d ':' -f 1`"
  local vendor="`echo $option  | awk '{ print $7 }' | cut -d ':' -f 1`"
  local product="`echo $option | awk '{ print $7 }' | cut -d ':' -f 2`"

  # Detach into the guest
  echo "Detaching device ${vendor}:${product} into the guest..."
  kvmx_monitor device_del usb-0x${vendor}-0x${product}

  # Restore permission to the device
  echo "Restoring ownership of /dev/bus/usb/${bus}/${device}..."
  sudo chown root /dev/bus/usb/$bus/$device
}

# Grow a partition
# This will restart the guest a few times
# Inspired by  https://ahelpme.com/linux/online-resize-of-a-root-ext4-file-system-increase-the-space/
function kvmx_growpart {
  local device="$1"
  local partition="$2"
  local size="$3"

  # Syntax check
  if [ -z "$size" ]; then
    echo "usage: $BASENAME growpart $VM <device> <partition> <additional_size>"
    echo "example: $BASENAME growpart test /dev/vda 2 5G"
    exit 1
  fi

  # Ensure the guest is running
  if ! kvmx_running; then
    echo "Powering up guest..."
    kvmx_up
  fi

  # Check for $partition
  echo "Checking for partition ${device}${partition}..."
  echo /bin/test -e ${device}${partition} | kvmx_ssh
  if [ "$?" != "0" ]; then
    echo "Could not determine ${device}${partition} existence, aborting."
    exit 1
  fi

  # Ensure cloud-guest-utils is installed
  echo "Checking for cloud-guest-utils availability..."
  echo which growpart | kvmx_ssh &> /dev/null
  if [ "$?" != "0" ]; then

    echo which apt-get | kvmx_ssh &> /dev/null
    if [ "$?" == "0" ]; then
      kvmx_ssh sudo apt-get install -y cloud-guest-utils
    else
      echo "Please install cloud-guest-utils in the guest first"
      exit 1
    fi
  fi

  # Now make sure the guest is off
  echo "Powering off guest"
  kvmx_poweroff

  # Resize the image
  echo "Resizing the image to an additional ${size}"
  qemu-img resize $image +${size}

  # Power up
  echo "Powering up the guest again"
  kvmx_up

  # Resize virtual machine root partition - while the filesystem is mounted!
  # this parted command currently need to be done manually
  #
  # Check https://unix.stackexchange.com/questions/373063/auto-expand-last-partition-to-use-all-unallocated-space-using-parted-in-batch-m
  #       https://unix.stackexchange.com/questions/190317/gnu-parted-resizepart-in-script#202872
  #       https://bugs.launchpad.net/ubuntu/+source/parted/+bug/1270203
  #       https://techtitbits.com/2018/12/using-parteds-resizepart-non-interactively-on-a-busy-partition/
  #       https://serverfault.com/questions/870594/resize-partition-to-maximum-using-parted-in-non-interactive-mode
  #
  #echo resizepart 2 -1 | kvmx ssh $guest sudo parted /dev/vda
  #kvmx_ssh sudo parted /dev/vda resizepart 2 -1 Yes
  echo "Growing ${device}${partition}..."
  kvmx_ssh sudo growpart ${device} ${partition}

  # Resize the file system and schedule a fsck for the next reboot
  echo "Resizing the ${device}${partition} filesystem..."
  kvmx_ssh sudo resize2fs ${device}${partition}
  kvmx_ssh sudo touch /forcefsck

  # Restart
  echo "Restarting the guest..."
  kvmx_restart
}

# Inotify dispatcher
function kvmx_inotify {
  local watched="$1"

  shift
  local command="$*"

  # Syntax check
  if [ -z "$command" ]; then
    echo "usage $BASENAME inotify $GUEST <watched> <command>"
    echo "example: kvmx inotify $guest hostfolder make -C guestfolder compile"
    exit 1
  fi

  # Check if watched exists
  if [ ! -e "$watched" ]; then
    echo "Not found: $watched"
    exit 1
  fi

  # Dispatch
  while inotifywait -r $watched; do
    echo "$command" | kvmx_ssh
  done
}

# Dispatch
if type kvmx_$ACTION 2> /dev/null | grep -q "kvmx_$ACTION ()"; then
  __kvmx_initialize $*

  if [ "$ACTION" != "app_base" ]; then
    shift $SHIFTARGS
  fi

  kvmx_$ACTION $*
else
  kvmx_usage
fi