#! /bin/sh
# shellcheck disable=SC3040,SC3043
set -o pipefail

PREFIX=/usr/local
LOCALBASE=/usr/local
VERSION=0.13.0
GUEST_ROOT=/usr/local/share/wifibox
SCRIPT=${PREFIX}/sbin/wifibox
LOGDIR=/var/log
RUNDIR=/var/run/wifibox
CONFDIR=${PREFIX}/etc/wifibox
WIFIBOX_LOG=${LOGDIR}/wifibox.log
UDS_CONFIG=${CONFDIR}/appliance/uds_passthru.conf

: "${SYSCTL:=/sbin/sysctl}"
: "${SED:=/usr/bin/sed}"

KERNEL_PATH=$(${SYSCTL} -n kern.module_path | ${SED} -E 's/^([^;]*);.*/\1/')

: "${BHYVE:=/usr/sbin/bhyve}"
: "${BHYVECTL:=/usr/sbin/bhyvectl}"
: "${VMM_KO:=${KERNEL_PATH}/vmm.ko}"
: "${DEVCTL:=/usr/sbin/devctl}"
: "${DAEMON:=/usr/sbin/daemon}"
: "${IFCONFIG:=/sbin/ifconfig}"
: "${KLDLOAD:=/sbin/kldload}"
: "${KLDUNLOAD:=/sbin/kldunload}"
: "${KLDSTAT:=/sbin/kldstat}"
: "${SHA256:=/sbin/sha256}"
: "${MKNOD:=/sbin/mknod}"
: "${NICE:=/usr/bin/nice}"

: "${CAT:=/bin/cat}"
: "${ECHO:=/bin/echo}"
: "${EXPR:=/bin/expr}"
: "${PS:=/bin/ps}"
: "${PRINTF:=/usr/bin/printf}"
: "${GREP:=/usr/bin/grep}"
: "${TAIL:=/usr/bin/tail}"
: "${HEAD:=/usr/bin/head}"
: "${SLEEP:=/bin/sleep}"
: "${PGREP:=/bin/pgrep}"
: "${KILL:=/bin/kill}"
: "${RM:=/bin/rm}"
: "${CU:=/usr/bin/cu}"
: "${NETSTAT:=/usr/bin/netstat}"
: "${READLINK:=/usr/bin/readlink}"

: "${GRUB_BHYVE:=${LOCALBASE}/sbin/grub-bhyve}"
: "${SOCAT:=${LOCALBASE}/bin/socat}"

DISK_IMAGE="${GUEST_ROOT}/disk.img"

NMDM_DEVICE=/dev/nmdm-wifibox
NMDM_A="${NMDM_DEVICE}.1A"
NMDM_B="${NMDM_DEVICE}.1B"

WIFIBOX_IF="wifibox0"
WIFIBOX_VM="wifibox"

UDS_PASSTHRU_DAEMON_ID="wifibox-uds-passthru"
VM_MANAGER_DAEMON_ID="wifibox-vm-manager"

log() {
    local _type="$1"
    local _level
    local _message="$2"
    local _timestamp
    local _config="${CONFDIR}/core.conf"
    local _loglevel

    _timestamp="$(date +'%FT%H:%M:%S%z')"

    if [ ! -f "${_config}" ]; then
	${ECHO} "ERROR: ${_config} is missing, please create it from the sample."
	exit 3
    fi

    loglevel=warn

    # shellcheck source=./etc/core.conf.sample
    . "${_config}"

    case ${loglevel} in
	[Ee][Rr][Rr][Oo][Rr]) _loglevel=1;;
	[Ww][Aa][Rr][Nn]) _loglevel=2;;
	[Ii][Nn][Ff][Oo]) _loglevel=3;;
	[Dd][Ee][Bb][Uu][Gg]) _loglevel=4;;
	*) _loglevel=0;;
    esac

    case ${_type} in
	error) [ ${_loglevel} -lt 1 ] && return 0;;
	warn) [ ${_loglevel} -lt 2 ] && return 0;;
	info) [ ${_loglevel} -lt 3 ] && return 0;;
	debug) [ ${_loglevel} -lt 4 ] && return 0;;
	*) return 0;;
    esac

    _level="$(${PRINTF} "%-5s" "${_type}" | tr "[:lower:]" "[:upper:]")"

    if [ -w ${WIFIBOX_LOG} ]; then
	${ECHO} "${_timestamp} ${_level} ${_message}" >> ${WIFIBOX_LOG}
    elif [ -z "${WARNED_ABOUT_LOG}" ]; then
	${ECHO} "WARNING: ${WIFIBOX_LOG} is not writeable, messages could not be logged."
	WARNED_ABOUT_LOG=yes
    fi

    case ${_type} in
	error) ${ECHO} "ERROR: ${_message}.";;
	warn) ${ECHO} "WARNING: ${_message}.";;
    esac
}

output() {
    local _message="$1"

    log info "Output: ${_message}"
    ${ECHO} "${_message}."
}

capture_output() {
    local _type="$1"
    local _id="$2"

    while read -r message; do
	log "${_type}" "[${_id}] ${message}"
    done
}

check_virtfs() {
    local _backends

    _backends=$(${BHYVE} -s help)
    log debug "Backends reported by bhyve:"
    echo "${_backends}" | capture_output debug bhyve

    if ! (${ECHO} "${_backends}" | ${GREP} -Fq virtio-9p); then
        log error "The Virtio 9p (VirtFS) bhyve interface is not available"
	exit 127
    fi
}

sysctl_value() {
    ${SYSCTL} -nq "$1"
}

assert_kmod_loaded() {
    local _kmod="$1"
    local _kmod_file

    if [ -n "$2" ]; then
	_kmod_file="$2"
    else
	_kmod_file="${_kmod}"
    fi

    log debug "assert loaded: kmod=${_kmod}, kmod_file=${_kmod_file}"

    if ! ${KLDSTAT} -q -m "${_kmod}"; then
	log debug "Kernel module ${_kmod} is not loaded"

	if ! (${KLDLOAD} "${_kmod_file}" 2>&1 | capture_output debug kldload); then
	    log error "${_kmod_file} kernel module could not be loaded"
	    exit 127
	fi

	log debug "Kernel module ${_kmod} was loaded successfully"
    fi
}

get_kmod_path() {
    local _kmod="$1"

    ${KLDSTAT} -v -n "${_kmod}" \
	| ${TAIL} +2 \
	| ${HEAD} -1 \
	| ${SED} -e 's![^(]*(\([^)]*\))!\1!'
}

assert_vmm_loaded() {
    local _kmod_path

    if ${KLDSTAT} -q -m vmm; then
	_kmod_path="$(get_kmod_path vmm)"
	log info "vmm.ko is expected at path: ${VMM_KO}"
	log info "vmm.ko is found at path: ${_kmod_path}"

	if [ "${_kmod_path}" != "${VMM_KO}" ] && ! ${KLDUNLOAD} vmm; then
	    log error "vmm.ko is loaded from a different location, but cannot be replaced"
	    exit 127
	fi
    fi

    assert_kmod_loaded "vmm" "${VMM_KO}"
}

# shellcheck disable=SC2046
check_iommu() {
    local _iommu
    local _amdvi

    _iommu=$(sysctl_value "hw.vmm.iommu.enable" || ${ECHO} "0")
    _amdvi=$(sysctl_value "hw.vmm.amdvi.enable" || ${ECHO} "0")
    log debug "assert hardware support present: iommu=${_iommu}, amdvi=${_amdvi}"

    if [ "${_iommu}" -eq "0" ] && [ "${_amdvi}" -eq "0" ]; then
	log error "PCI pass-through is not available for bhyve"
	exit 127
    fi
}

assert_vm_can_run() {
    assert_vmm_loaded
    check_iommu
    check_virtfs
}

get_image_checksum() {
    ${SHA256} -q "${DISK_IMAGE}"
}

check_configuration() {
    local _file="$1"

    if [ ! -f "${_file}" ]; then
	log error "${_file} is missing, please create it from the sample"
	exit 3
    fi
}

has_bridge_interface() {
    ${IFCONFIG} | ${GREP} -Fq "${WIFIBOX_IF}: "
}

get_tap_interface() {
    if has_bridge_interface; then
	${IFCONFIG} "${WIFIBOX_IF}" \
	    | ${GREP} -F member \
	    | ${SED} -E 's/^.*member:.*(tap[^ ]*).*$/\1/'
    else
	${ECHO} ""
    fi
}

create_bridge() {
    local _tap

    if ! has_bridge_interface; then
	log info "Creating bridge interface: ${WIFIBOX_IF}"
	${IFCONFIG} bridge create name ${WIFIBOX_IF} up 2>&1 | capture_output debug ifconfig
    else
	log warn "Bridge interface already exists: ${WIFIBOX_IF}, skipping creation"
    fi

    _tap="$(get_tap_interface)"

    if [ -z "${_tap}" ]; then
	_tap="$(${IFCONFIG} tap create up)"
	log info "Linking tap interface to ${WIFIBOX_IF}: ${_tap}"
	${IFCONFIG} ${WIFIBOX_IF} addm "${_tap}" 2>&1 | capture_output debug ifconfig
    else
	log warn "Linked tap interface already exists: ${_tap}, skipping creation"
    fi
}

destroy_bridge() {
    local _tap

    _tap="$(get_tap_interface)"

    log info "Destroying bridge interface: ${WIFIBOX_IF}"
    ${IFCONFIG} ${WIFIBOX_IF} destroy 2>&1 | capture_output debug ifconfig

    if [ -n "${_tap}" ]; then
	log info "Destroying linked tap interface: ${_tap}"
	${IFCONFIG} "${_tap}" destroy 2>&1 | capture_output debug ifconfig
    else
	log warn "No linked tap inteface found for ${WIFIBOX_IF}"
    fi
}

create_nmdm() {
    check_configuration "${CONFDIR}/bhyve.conf"

    # shellcheck source=./etc/bhyve.conf.sample
    . "${CONFDIR}/bhyve.conf"

    [ "${console}" != "yes" ] && return 0

    log info "Bringing up null-modem devices for console connection"
    assert_kmod_loaded "nmdm"

    [ ! -c ${NMDM_A} ] && ${MKNOD} ${NMDM_A} 2>&1 | capture_output debug mknod
    [ ! -c ${NMDM_B} ] && ${MKNOD} ${NMDM_B} 2>&1 | capture_output debug mknod

    [ ! -c ${NMDM_A} ] && log warn "${NMDM_A} is not available"
    [ ! -c ${NMDM_B} ] && log warn "${NMDM_B} is not available"

    [ -c ${NMDM_A} ] && [ -c ${NMDM_B} ] \
	&& log info "Null-modem devices are available"
}

destroy_nmdm() {
    log info "Removing null-modem devices"

    [ -c ${NMDM_A} ] && ${RM} ${NMDM_A} 2>&1 | capture_output debug rm
    [ -c ${NMDM_B} ] && ${RM} ${NMDM_B} 2>&1 | capture_output debug rm

    [ -c ${NMDM_A} ] && log warn "${NMDM_A} could not be removed"
    [ -c ${NMDM_B} ] && log warn "${NMDM_B} could not be removed"

    [ ! -c ${NMDM_A} ] && [ ! -c ${NMDM_B} ] \
	&& log info "Null-modem devices are removed"
}

find_guest_ip() {
    ${NETSTAT} -r \
	| ${GREP} "^default.*${WIFIBOX_IF}\$" \
	| ${HEAD} -1 \
	| ${SED} -E "s!^default[ ]+([0-9\.]+)[ ]+.*${WIFIBOX_IF}\$!\1!"
}

get_uds_passthru_connections() {
    ${PGREP} -fx "daemon: ${UDS_PASSTHRU_DAEMON_ID}\[[0-9]*\]"
}

uds_passthru_start() {
    local network
    local sockets
    local _connections
    local _ip
    local _path
    local _port
    local _user
    local _group
    local _mode

    [ ! -r "${UDS_CONFIG}" ] && return 0

    _connections="$(get_uds_passthru_connections)"

    if [ -n "${_connections}" ]; then
	log warn "Unix Domain Sockets are already forwarded, skipping"
	return 1
    fi

    if [ ! -x "${SOCAT}" ]; then
	log warn "Socat binary could not be found as ${SOCAT}, dropping UDS pass-through"
	return 1
    fi

    log info "Bringing up Unix Domain Socket pass-through"

    # shellcheck disable=SC1090
    . "${UDS_CONFIG}"

    if [ -z "${network}" ]; then
	_ip=$(find_guest_ip)
    else
	_ip=${network%%:*}
    fi

    log info "Found guest IP address: ${_ip}"

    if [ -z "${_ip}" ]; then
	log warn "No guest IP address could be discovered, dropping UDS pass-through"
	return 1
    fi

    log info "Configured sockets: [${sockets}]"

    for s in ${sockets}; do
	_path="${s##*path=}"; _path="${_path%%,*}"
	_port="${s##*port=}"; _port="${_port%%,*}"
	_user="${s##*user=}"; _user="${_user%%,*}"
	_group="${s##*group=}"; _group="${_group%%,*}"
	_mode="${s##*mode=}"; _mode="${_mode%%,*}"

	if [ -z "${_port}" ]; then
           log warn "No port defined for ${_path}, dropping UDS pass-through"
	   continue
	fi

	log info "Hooking up ${_ip}:${_port} as ${_path} (${_user}:${_group}@${_mode})"
	${DAEMON} -r -t "${UDS_PASSTHRU_DAEMON_ID}" \
	    "${SOCAT}" \
	    UNIX-RECVFROM:"${_path}",reuseaddr,fork,unlink-early,user="${_user}",group="${_group}",mode="${_mode}" \
	    TCP4:"${_ip}":"${_port}" 2>&1 \
	    | capture_output debug socat &
    done
}

uds_passthru_stop() {
    local _connections

    _connections="$(get_uds_passthru_connections)"
    [ -z "${_connections}" ] && return 0

    log info "Tearing down Unix Domain Socket pass-through"
    log info "Daemonized socat processes found: [${_connections}]"

    # shellcheck disable=SC2086
    ${KILL} -TERM ${_connections}
}

get_ppt_devices() {
    check_configuration "${CONFDIR}/bhyve.conf"
    passthru=

    # shellcheck source=./etc/bhyve.conf.sample
    . "${CONFDIR}/bhyve.conf"

    if [ -z "${passthru}" ]; then
	${ECHO} ""
    else
	${ECHO} "${passthru}" \
	    | ${SED} -E 's!([0-9]*)/([0-9]*)/([0-9]*)!pci\1:\2:\3!g'
    fi
}

get_vm_manager_pid() {
    ${PGREP} -fx "daemon: ${VM_MANAGER_DAEMON_ID}\[[0-9]*\]"
}

destroy_vm() {
    log info "Destroying guest ${WIFIBOX_VM}"

    ${BHYVECTL} --destroy --vm=${WIFIBOX_VM} 2>&1 | capture_output info bhyvectl
    ${SLEEP} 1 2>&1 | capture_output debug sleep

    _ppts="$(get_ppt_devices)"

    if [ -n "${_ppts}" ]; then
	for ppt in ${_ppts}; do
	    log info "Destroying bhyve PPT device: ${ppt}"
	    if ! ${DEVCTL} clear driver -f "${ppt}" 2>&1 | capture_output debug devctl; then
		log warn "PPT device ${ppt} could not be destroyed"
	    fi
	done
    else
	log warn "No bhyve PPT device could be found"
    fi
}

vm_start() {
    local _pid

    _pid="$(get_vm_manager_pid)"

    if [ -n "${_pid}" ]; then
	log warn "Guest is already run by PID ${_pid}, left intact"
	return 1
    fi

    ${DAEMON} -r -t "${VM_MANAGER_DAEMON_ID}" \
	      "${0}" _manage_vm

    log info "Waiting for bhyve to start up"

    ${SLEEP} 1 2>&1 | capture_output debug sleep
    _pid="$(get_vm_manager_pid)"

    if [ -z "${_pid}" ]; then
	log error "Guest could not be started"
	exit 4
    else
	log info "Guest ${WIFIBOX_VM} has started up"
    fi

    log info "Guest is managed by PID ${_pid}"
}

assert_daemonized() {
    local _parent

    _parent=$(${PS} -o comm $PPID | ${TAIL} +2)
    log debug "assert daemonized: parent=${_parent}"

    if [ "${_parent}" != "daemon" ]; then
	log error "This portion of the program could only be run daemonized"
	exit 127
    fi
}

quit_daemonization() {
    log info "VM manager: quit daemonization"
    ${KILL} -SIGTERM $PPID
}

assert_value_wellformed() {
    local _location="$1"
    local _name="$2"
    local _syntax="$3"
    local _expected="$4"
    local _value="$5"

    if ! ${ECHO} "${_value}" | ${GREP} -Eq "${_syntax}"; then
	log error "${_location}: malformed ${_name} value: \"${_value}\", expected: ${_expected}"
	quit_daemonization
	exit 3
    fi
}

assert_value_ranged_integer() {
    local _location="$1"
    local _name="$2"
    local _lower="$3"
    local _upper="$4"
    local _value="$5"
    local _syntax='^[[:space:]]*[0-9]+[[:space:]]*$'

    assert_value_wellformed \
	"${_location}" "${_name}" \
	"${_syntax}" "non-negative integer" \
	"${_value}"

    if [ "${_value}" -lt "${_lower}" ] || [ "${_value}" -gt "${_upper}" ]; then
	log error "${_location}: ${_name} should be in between ${_lower} and ${_upper}: ${_value}"
	quit_daemonization
	exit 3
    fi
}

assert_value_yesno() {
    local _location="$1"
    local _name="$2"
    local _value="$3"
    local _syntax='^[[:space:]]*(yes|no)[[:space:]]*$'

    assert_value_wellformed \
	"${_location}" "${_name}" \
	"${_syntax}" "yes or no" \
	"${_value}"
}

# shellcheck disable=SC2086
vm_manager() {
    local _max_vmm_cpus
    local _nmdm_grub_bhyve
    local _nmdm_bhyve
    local _passthru_bhyve
    local _tap_bhyve
    local _ppt
    local _tap
    local _app_conf
    local _app_conf_ptr="${CONFDIR}/app_config"
    local _app_conf_mode
    local _app_conf_bhyve
    local _grub_bhyve_args
    local _grub_bhyve_exit_code
    local _grub_device_map="${GUEST_ROOT}/device.map"
    local _grub_cfg="${GUEST_ROOT}/grub.cfg"
    local _nice_priority
    local _bhyve_args
    local _bhyve_devs
    local _slot
    local _bhyve_exit_code
    local _restart

    assert_daemonized

    log info "VM manager launched"
    log info "Gathering necessary configuration files for launching the guest"

    check_configuration "${CONFDIR}/bhyve.conf"

    cpus=1
    memory=128M
    passthru=
    console=no
    priority=50

    log info "Pulling bhyve options from configuration file"
    # shellcheck source=./etc/bhyve.conf.sample
    . "${CONFDIR}/bhyve.conf"

    log debug "cpus=${cpus}"
    log debug "memory=${memory}"
    log debug "passthru=[${passthru}]"
    log debug "console=${console}"
    log debug "priority=${priority}"

    _max_vmm_cpus=$(${SYSCTL} -n hw.vmm.maxcpu)

    log debug "bhyve max cpus=${_max_vmm_cpus}"

    assert_value_ranged_integer \
	"bhyve.conf" "cpus" 1 ${_max_vmm_cpus} \
	"${cpus}"

    assert_value_wellformed \
	"bhyve.conf" "memory" \
	'^[[:space:]]*[0-9]+([Kk]|[Mm]|[Gg]|[Tt])?[[:space:]]*$' \
	"non-negative integer, suffixed with unit: K, M, G, T" \
	"${memory}"

    assert_value_wellformed \
	"bhyve.conf" "passthru" \
	'^([[:space:]]*[0-9]{1,3}/[0-9]{1,3}/[0-9]{1,3}[[:space:]]*)*$' \
	"list of slot/bus/function, triples of non-negative integers" \
	"${passthru}"

    assert_value_yesno \
	"bhyve.conf" "console" \
	"${console}"

    assert_value_ranged_integer \
	"bhyve.conf" "priority" 0 99 \
	"${priority}"

    _nice_priority=$(${EXPR} \( ${priority} \* 40 / 99 \) - 20)

    if [ "${console}" = "yes" ]; then
	_nmdm_grub_bhyve="-c ${NMDM_A}"
	_nmdm_bhyve="-l com1,${NMDM_A}"
	log info "Guest console is configured to use"
    else
	log info "Guest console is not configured to use"
    fi

    if [ -n "${passthru}" ]; then
	_passthru_bhyve=""
	_slot=0

	for sbf in ${passthru}; do
	    _passthru_bhyve="${_passthru_bhyve} -s 6:${_slot},passthru,${sbf}"
	    _slot=$(${EXPR} ${_slot} + 1)
        done

	log info "Passthru devices configured: [${passthru}]"
    else
	log warn "No passthru device is configured"
    fi

    _ppts="$(get_ppt_devices)"
    if [ -n "${_ppts}" ]; then
	for ppt in ${_ppts}; do
	    ${DEVCTL} set driver -f "${ppt}" ppt 2>&1 | capture_output debug devctl
	    log info "PPT driver is configured for ${ppt} device"
	done
    else
	log warn "No PPT driver is attached due to lack of device"
    fi

    _tap="$(get_tap_interface)"
    if [ -n "${_tap}" ]; then
	_tap_bhyve="-s 5:0,e1000,${_tap}"
	${IFCONFIG} "${_tap}" up 2>&1 | capture_output debug ifconfig
	log info "tap interface is configured: ${_tap}"
    else
	log error "No tap interface is available, cannot proceed"
	quit_daemonization
	exit 5
    fi

    log info "Launching guest ${WIFIBOX_VM} from ${GUEST_ROOT} with grub-bhyve"

    if [ ! -f "${_grub_cfg}" ]; then
	log error "${_grub_cfg} could not be found, guest cannot be started"
	quit_daemonization
	exit 4
    fi

    if [ ! -f "${DISK_IMAGE}" ]; then
	log error "${DISK_IMAGE} could not be found, guest cannot be started"
	quit_daemonization
	exit 4
    fi

    _bhyve_devs="virtio-blk,${DISK_IMAGE}"
    _bhyve_devs="${_bhyve_devs} virtio-9p,config=${CONFDIR}/appliance,ro"
    _bhyve_devs="${_bhyve_devs} virtio-9p,var=${RUNDIR}/appliance"
    _app_conf=$(${READLINK} -f "%Y" ${_app_conf_ptr})

    if [ -n "${_app_conf}" ]; then
	log info "Application config is found at ${_app_conf}"

	if [ -r "${UDS_CONFIG}" ]; then
	    _app_conf_mode=""
	    log info "Application config will be mounted writeable"
	else
	    _app_conf_mode=",ro"
	    log info "Application config will be mounted read-only"
	fi

	_bhyve_devs="${_bhyve_devs} virtio-9p,app_config=${_app_conf}${_app_conf_mode}"
    else
	log info "No application config found, nothing to mount"
    fi

    log debug "Devices: ${_bhyve_devs}"

    _grub_bhyve_args=""
    _grub_bhyve_args="${_grub_bhyve_args} -S -M ${memory}"
    _grub_bhyve_args="${_grub_bhyve_args} -r host ${_nmdm_grub_bhyve}"
    [ -f "${_grub_device_map}" ] \
	&& _grub_bhyve_args="${_grub_bhyve_args} -m ${_grub_device_map}"
    _grub_bhyve_args="${_grub_bhyve_args} -d ${GUEST_ROOT} ${WIFIBOX_VM}"

    log debug "Arguments: ${_grub_bhyve_args}"
    ${GRUB_BHYVE} ${_grub_bhyve_args} 2>&1 | capture_output debug grub-bhyve
    _grub_bhyve_exit_code="$?"

    if [ "${_grub_bhyve_exit_code}" -ne "0" ]; then
	destroy_vm

	log debug "exit_code=${_grub_bhyve_exit_code}"
	log info "grub-bhyve failed to start, signaling restart after 5 seconds"

	${SLEEP} 5 2>&1 | capture_output debug sleep
	exit 1
    fi

    log info "Launching guest ${WIFIBOX_VM} from ${GUEST_ROOT} with bhyve"

    _bhyve_args=""
    _bhyve_args="${_bhyve_args} -c ${cpus}"
    _bhyve_args="${_bhyve_args} -m ${memory} -AHP -u -S"
    _bhyve_args="${_bhyve_args} ${_nmdm_bhyve}"
    _bhyve_args="${_bhyve_args} -s 0,hostbridge"
    _bhyve_args="${_bhyve_args} -s 31,lpc"
    _slot=0

    for dev in ${_bhyve_devs}; do
	_bhyve_args="${_bhyve_args} -s 4:${_slot},${dev}"
	_slot=$(${EXPR} ${_slot} + 1)
    done

    _bhyve_args="${_bhyve_args} ${_tap_bhyve}"
    _bhyve_args="${_bhyve_args} ${_passthru_bhyve}"
    _bhyve_args="${_bhyve_args} ${WIFIBOX_VM}"

    log debug "Nice priority: ${_nice_priority}"
    log debug "Arguments: ${_bhyve_args}"
    ${NICE} -n ${_nice_priority} \
	    ${BHYVE} ${_bhyve_args} 2>&1 | capture_output debug bhyve
    _bhyve_exit_code="$?"
    destroy_vm

    case "${_bhyve_exit_code}" in
	0) log info "VM manager: guest was rebooted, signaling restart"
	   _restart=yes;;
	1) log info "VM manager: guest was powered off, signaling exit";;
	2) log info "VM manager: guest was halted, signaling exit";;
	*) log info "VM manager: guest crashed, signaling restart after 5 seconds"
	   log debug "exit_code=${_bhyve_exit_code}"
	   ${SLEEP} 5 2>&1 | capture_output debug sleep
	   _restart=yes;;
    esac

    [ -n "${_restart}" ] && exit 1

    destroy_nmdm
    destroy_bridge
    quit_daemonization
}

vm_stop() {
    local _ppt
    local _pid

    _pid="$(get_vm_manager_pid)"

    if [ -z "${_pid}" ]; then
	log warn "Guest is not running, hence not stopped"
	return 1
    fi

    log info "Stopping guest ${WIFIBOX_VM}, managed by PID ${_pid}"

    if ! (${KILL} -SIGTERM "${_pid}" 2>&1 | capture_output debug kill); then
	log warn "Guest could not be stopped gracefully"
    fi

    log info "Waiting 3 seconds for the guest to stop"
    ${SLEEP} 3 2>&1 | capture_output debug sleep

    log info "Forcing shutdown of guest ${WIFIBOX_VM}"
    ${BHYVECTL} --force-poweroff --vm=${WIFIBOX_VM} 2>&1 | capture_output debug bhyvectl
    destroy_vm
}

show_progress() {
    ${PRINTF} "."
}

has_flag() {
    local _flags="$1"
    local _flag="$2"

    ${ECHO} "${_flags}" | ${GREP} -Fq "${_flag}"
}

wifibox_start() {
    local _target="$1"
    local _start

    log info "Begin: wifibox start"

    if [ -n "$2" ]; then
	log error "Too many parameters"
	exit 1
    fi

    case ${_target} in
	guest) _start="G";;
	""|netif) _start="GN";;
	console) _start="GM";;
	vmm) _start="GV";;
	*) log error "Unknown target: ${_target} (supported: guest, netif, console, vmm)"
	   exit 1;;
    esac

    log debug "start=${_start}"
    ${PRINTF} "Starting wifibox..."

    if has_flag "${_start}" "V"; then
	load_vmm
	show_progress
    fi

    if has_flag "${_start}" "N"; then
	create_bridge
	show_progress
    fi

    if has_flag "${_start}" "M"; then
	create_nmdm
	show_progress
    fi

    if has_flag "${_start}" "G"; then
	assert_vm_can_run

	create_nmdm
	show_progress

	vm_start
	show_progress

	uds_passthru_start
	show_progress
    fi

    ${ECHO} "OK"
    log info "End: wifibox start"
}

wifibox_stop() {
    local _target="$1"
    local _stop

    log info "Begin: wifibox stop"

    if [ -n "$2" ]; then
	log error "Too many parameters"
	exit 1
    fi

    case ${_target} in
	guest) _stop="G";;
	""|netif) _stop="GN";;
	console) _stop="GM";;
	vmm) _stop="GV";;
	*) log error "Unknown target: ${_target} (supported: guest, netif, console, vmm)"
	   exit 1;;
    esac

    log debug "stop=${_stop}"
    ${PRINTF} "Stopping wifibox..."

    if has_flag "${_stop}" "G"; then
	uds_passthru_stop
	show_progress

	vm_stop
	show_progress

	destroy_nmdm
	show_progress
    fi

    if has_flag "${_stop}" "M"; then
	destroy_nmdm
	show_progress
    fi

    if has_flag "${_stop}" "N"; then
	destroy_bridge
	show_progress
    fi

    if has_flag "${_stop}" "V"; then
	unload_vmm
	show_progress
    fi

    ${ECHO} "OK"
    log info "End: wifibox stop"
}

# This is a workaround to recover from the unfortunate state of the
# wireless device after resume.
load_vmm() {
    log info "Reloading vmm.ko"
    ${KLDLOAD} "${VMM_KO}" 2>&1 | capture_output debug kldload
}

unload_vmm() {
    log info "Unloading vmm.ko"

    if ! (${KLDUNLOAD} vmm 2>&1 | capture_output debug kldunload); then
	log error "Some other bhyve guests might be running, vmm.ko could not be unloaded"
	exit 127
    fi
}

reload_vmm() {
    unload_vmm
    load_vmm
}

wifibox_restart() {
    local _target="$1"
    local _restart
    local _pid

    log info "Begin: wifibox restart"

    if [ -n "$2" ]; then
	log error "Too many parameters"
	exit 1
    fi

    _pid="$(get_vm_manager_pid)"

    if [ -z "${_pid}" ]; then
	log warn "No running instance found that could be restarted"
	return 1
    fi

    case ${_target} in
	guest) _restart="G";;
	""|netif) _restart="GN";;
	console) _restart="GM";;
	vmm) _restart="GV";;
	*) log error "Unknown target: ${_target} (supported: guest, netif, console, vmm)"
	   exit 1;;
    esac

    log debug "restart=${_restart}"
    ${PRINTF} "Restarting wifibox..."

    if has_flag "${_restart}" "G"; then
	uds_passthru_stop
	show_progress
	vm_stop
	show_progress
    fi

    if has_flag "${_restart}" "M"; then
	destroy_nmdm
	show_progress
    fi

    if has_flag "${_restart}" "N"; then
	destroy_bridge
	show_progress
    fi

    if has_flag "${_restart}" "V"; then
	reload_vmm
	show_progress
    fi

    if has_flag "${_restart}" "N"; then
	create_bridge
	show_progress
    fi

    if has_flag "${_restart}" "M"; then
	create_nmdm
	show_progress
    fi

    if has_flag "${_restart}" "G"; then
	vm_start
	show_progress
	uds_passthru_start
	show_progress
    fi

    ${ECHO} "OK"
    log info "End: wifibox restart"
}

wifibox_status() {
    local _pid

    log info "Begin: wifibox status"

    _pid="$(get_vm_manager_pid)"

    if [ -n "${_pid}" ]; then
	output "wifibox is run by PID ${_pid}"
    else
	output "wifibox is not run"
	return 1
    fi

    log info "End: wifibox status"
}

wifibox_console() {
    local _pid

    log info "Begin: wifibox console"
    _pid="$(get_vm_manager_pid)"
    log debug "Guest is run by ${_pid}"

    if [ -z "${_pid}" ]; then
	log error "There is no guest to attach to"
	exit 127
    fi

    if [ ! -c ${NMDM_B} ]; then
	log error "No null-modem device is configured"
	exit 127
    fi

    ${ECHO} 'Connecting, type "~." to leave the session...'
    log info "Attaching to the guest"
    ${CU} -s 115200 -l ${NMDM_B}
    log info "Detached from the guest"
    ${ECHO} "Finished."
    log info "End: wifibox console"
}

wifibox_version() {
    local _checksum

    log info "Begin: wifibox version"
    _checksum="$(get_image_checksum)"

    log debug "version=${VERSION}, checksum=${_checksum}"
    ${ECHO} "wifibox version ${VERSION}"
    ${ECHO} "Disk image checksum: ${_checksum}"
    log info "End: wifibox version"
}

wifibox_usage() {
    ${CAT} <<EOF
USAGE: ${SCRIPT} ...
    start [guest|netif|console|vmm]
    stop [guest|netif|console|vmm]
    restart [guest|netif|console|vmm]
    status
    console
    version
EOF
    exit 1
}

# Do not execute the main program when sourced.
[ "$0" != "${SCRIPT}" ] && return 0

log debug "Program started as $0, with arguments: $*"
command="$1"
shift

case ${command} in
    start) wifibox_start "$@";;
    stop) wifibox_stop "$@";;
    restart) wifibox_restart "$@";;
    status) wifibox_status;;
    console) wifibox_console;;
    version) wifibox_version;;
    _manage_vm) vm_manager;;
    *) wifibox_usage;;
esac
