#!/usr/bin/env bash
#
# Copyright (C) 2020 David Runge <dvzrv@archlinux.org>
# Copyright (C) 2021 Laurent Jourden <laurent85@enarel.fr>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# A simple script to run a bootable iso image or a bootable
# usb drive using qemu. Both BIOS and UEFI booting is
# supported.
#
# Requirements:
# - qemu
# - edk2-ovmf (when UEFI booting)


set -e -u -o pipefail

isoimage=''
ramsize=''
usbdrive=''
declare -i accessibility=0
readonly app_name="${0##*/}"
boot_type='uefi'
console=''
display='sdl'
image=''
mediatype='cdrom'
declare -i secure_boot=0
declare -i use_vnc=0
vga='virtio'
working_dir="$(mktemp -dt auirun.XXXXXXXX)"
trap cleanup_working_dir EXIT


print_help() {
    local usagetext
    IFS='' read -r -d '' usagetext <<EOF || true
Usage:
    "${app_name}" [options]

Options:
    -a                         set accessibility support using brltty (console profile only)
    -b, --bios                 set boot type to 'BIOS'
    -c, --console              enable serial console output on stdio
    -d, --device device_name   set usb block device to boot from
    -h, --help                 print help
    -i, --iso image_name       image to boot into
    --hd                       set image type to hard disk instead of optical disc
    -r, --ram integer[g/G]     set vm ram size in GiB (Default: 3)
    -s, --sb                   use Secure Boot (only relevant when using UEFI)
    -u, --uefi                 set boot type to 'UEFI' (default)
    -v, --vnc                  use VNC display (instead of default SDL)

Examples:
    Run an image using UEFI:
    $ aui-run --uefi --iso aui-xfce-linux_5_10_5-0108-x64.iso
    Run a bootable usb drive using UEFI:
    $ sudo aui-run --uefi --device /dev/sdc
EOF
    printf '%s' "${usagetext}"
}

cleanup_working_dir() {
    if [[ -d "${working_dir}" ]]; then
        rm -rf -- "${working_dir}"
    fi
}

copy_ovmf_vars() {
    if [[ ! -f '/usr/share/edk2/x64/OVMF_VARS.4m.fd' ]]; then
        printf '[%s] ERROR: %s\n' "${app_name}" "OVMF_VARS.4m.fd not found. Install edk2-ovmf."
        exit 1
    fi
    cp -a -- '/usr/share/edk2/x64/OVMF_VARS.4m.fd' "${working_dir}/"
}

_checks() {
    if [[ -z "${isoimage}" && -z "${usbdrive}" ]]; then
        printf '[%s] ERROR: %s\n' "${app_name}" "No iso image or usb device specified!"
        exit 1
    fi
    if [[ -n "${isoimage}" && -n "${usbdrive}" ]]; then
        printf '[%s] ERROR: %s\n' "${app_name}" "iso image and usb device are mutually exclusive!"
        exit 1
    fi
    if [[ -n "${isoimage}" && ! -f "${image}" ]]; then
        printf '[%s] ERROR: %s\n' "${app_name}" "Image file (${image}) does not exist."
        exit 1
    fi
    if [[ -n "${usbdrive}" && ! $(stat -c %t "${device}" 2> /dev/null) -eq 8 ]]; then
        printf '[%s] ERROR: %s\n' "${app_name}" "${device} is not a block device!"
        exit 1
    fi
    if [[ -n "${usbdrive}" && ! ( $(lsblk -dnro rm      "${device}" 2> /dev/null) -eq 1 || \
                                  $(lsblk -dnro hotplug "${device}" 2> /dev/null) -eq 1    ) ]]; then
        printf '[%s] ERROR: %s\n' "${app_name}" "${device} is not a removable block device!"
        exit 1
    fi
    if [[ -n "${usbdrive}" && ! "$(lsblk -dnro tran "${device}" 2> /dev/null)" == 'usb' ]]; then
        printf '[%s] ERROR: %s\n' "${app_name}" "${device} is not a usb device!"
        exit 1
    fi
    if ! pacman -Q qemu &> /dev/null; then
        printf '[%s] ERROR: %s\n' "${app_name}" 'qemu not installed, aborting!'
        exit 1
    fi
    if [[ -n "${ramsize}" ]]; then
        if ! [[ "${ramsize}" =~ ^[1-9][0-9]*$ ]]; then
            printf '[%s] ERROR: %s\n' "${app_name}" "RAM size invalid argument (GiB): ${ramsize}"
            exit 1
        fi
        ramsize=$(( ramsize * 1024 ))
    else
        ramsize=3072
    fi
}

_run() {
    local -a qemu_options
    local serial_option=()

    # Set default qemu options
    qemu_options=(
        -boot 'menu=on,reboot-timeout=5000'
        -m "${ramsize}"
        -cpu max
        -smp 4
        -name 'archiso,process=archiso_0'
        -display "${display}"
        -device 'virtio-net-pci,romfile=,netdev=net0'
        -netdev 'user,id=net0,hostfwd=tcp::60022-:22'
        -device qemu-xhci
        -device usb-kbd
        -device usb-mouse
        -device usb-tablet
        -vga "${vga}"
        -global ICH9-LPC.disable_s3=1
        -no-reboot
    )

    if (( secure_boot )); then
        printf '[%s] %s\n' "${app_name}" 'Using Secure Boot'
        local ovmf_code='/usr/share/edk2/x64/OVMF_CODE.secboot.4m.fd'
        qemu_options+=('-global' 'driver=cfi.pflash01,property=secure,value=on')
    else
        local ovmf_code='/usr/share/edk2/x64/OVMF_CODE.4m.fd'
        qemu_options+=('-global' 'driver=cfi.pflash01,property=secure,value=off')
    fi

    if [[ "$boot_type" == 'uefi' ]]; then
        if ! pacman -Q edk2-ovmf &> /dev/null; then
            printf '[%s] ERROR: %s\n' "${app_name}" 'edk2-ovmf not installed, aborting!'
            exit 1
        fi
        copy_ovmf_vars
        qemu_options+=('-drive' "if=pflash,format=raw,unit=0,file=${ovmf_code},read-only=on"
                       '-drive' "if=pflash,format=raw,unit=1,file=${working_dir}/OVMF_VARS.4m.fd")
    fi

    if (( accessibility )); then
        qemu_options+=('-chardev' 'braille,id=brltty'
                       '-device' 'usb-braille,id=usbbrl,chardev=brltty')
    fi

    if (( use_vnc )); then
        qemu_options+=('-device' 'virtio-gpu'
                       -vnc 'vnc=0.0.0.0:0,vnc=[::]:0')
    fi

    if [[ "${isoimage}" == 'yes' ]]; then
        qemu_options+=('-device' 'virtio-scsi-pci,id=scsi0'
                       '-device' "scsi-${mediatype%rom},bus=scsi0.0,drive=${mediatype}0"
                       '-drive' "id=${mediatype}0,if=none,format=raw,media=${mediatype/hd/disk},read-only=on,file=${image}")
    fi

    if [[ "${usbdrive}" == 'yes' ]]; then
        device="/sys/block/${device##*/}"
        device="$(readlink "${device}")"
        device="${device#*usb}"
        device="${device#*\/}"
        device="${device%%/*}"
        if ! device="$(grep -- DEVNAME /sys/bus/usb/devices/"${device}"/uevent 2>/dev/null)"; then
            printf '[%s] ERROR: %s\n' "${app_name}" "DEVNAME not found in /sys/bus/usb/devices/${device}/uevent, aborting!"
            exit 1
        fi
        device="${device/DEVNAME=//dev/}"
        if [[ -z "${device}" ]]; then
            printf '[%s] ERROR: %s\n' "${app_name}" 'could not resolve USB device path, aborting!'
            exit 1
        fi
        qemu_options+=('-device' 'qemu-xhci,id=xhci'
                       '-device' "usb-host,bus=xhci.0,hostdevice=${device}")
    fi

    if (( EUID != 0 )); then
        # Enable sound
        qemu_options+=('-audiodev' 'pa,id=snd0'
                       '-device' 'ich9-intel-hda'
                       '-device' 'hda-output,audiodev=snd0'
                       '-machine' 'type=q35,smm=on,accel=kvm,usb=on,pcspk-audiodev=snd0')
    else
        qemu_options+=('-machine' 'type=q35,smm=on,accel=kvm,usb=on')
    fi

    [[ "${console}" == 'on' ]] && serial_option=('-serial' 'stdio')
    qemu_options+=("${serial_option[@]}")

    qemu-system-x86_64 "${qemu_options[@]}"
}

if ! OPTS=$(getopt -o     'abcd:hi:r:suv' \
                   --long 'bios,console,device:,hd,help,iso:,ram:,sb,uefi,vnc' \
                   -n 'aui-run' -- "$@"); then
    print_help
    exit 1
fi
eval set -- "${OPTS}"

unset OPTS
[[ $# -eq 1 ]] && { print_help; exit 1; }

while true; do
    case "${1}" in
        '-a')
            accessibility=1
            shift ;;
        '-b'|'--bios')
            boot_type='bios'
            shift ;;
        '-c'|'--console')
            console='on'
            shift ;;
        '-d'|'--device')
            usbdrive='yes'
            device="${2}"
            shift 2 ;;
        '-h'|'--help')
            print_help
            exit 0
            ;;
        '-i'|'--iso')
            isoimage='yes'
            image="${2}"
            shift 2 ;;
        '--hd')
            mediatype='hd'
            shift ;;
        '-r'|'--ram')
            ramsize="${2/[gG]}"
            shift 2 ;;
        '-s'|'--sb')
            secure_boot=1
            shift ;;
        '-u'|'--uefi')
            boot_type='uefi'
            shift ;;
        '-v'|'--vnc')
            display='none'
            vga='none'
            use_vnc=1
            shift ;;
        '--')
            shift
            break ;;
    esac
done

_checks
_run

# vim:ts=4:sw=4:et:
