#!/bin/sh -e
# qemu-runlinux [options] <kernel> <program> ...
# run kernel/program in QEMU with root fs taken from host
#
# Copyright (C) 2014-2021  Nexedi SA and Contributors.
#                          Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.

# qemu-runlinux spawns linux kernel under qemu, setups rootfs to be taken from
# / of host, and runs specified program inside.
#
# It might be useful, for example, to test/debug just compiled kernel via
# running programs edited/compiled on host.

# ---- init under spawned kernel ----

# pid=1: we are running inside booted kernel as init.
# mount /sys /proc etc and tail to the program.
if [ $$ == 1 ]; then
	qinfo() {
		test "$qrun_loglevel" -le 6 && return	# <= KERN_INFO
		echo "$*"
	}

	qinfo "qinit ..."

	qshutdown() {
		echo 1 >/proc/sys/kernel/sysrq
		echo o >/proc/sysrq-trigger	# shutdown

	}

	qdie() {
		echo "E: $*" 1>&2
		qshutdown
		sleep 1d # give time for shutdown to happen
		exit 1	 # just in case
	}

	# mount proc early & set loglevel for run phase
	mount -t proc       none /proc
	echo "$qrun_loglevel" >/proc/sys/kernel/printk

	mount -t sysfs      none /sys
	mount -t debugfs    none /sys/kernel/debug
	mount -t bpf        none /sys/fs/bpf
	mount -t fusectl    none /sys/fs/fuse/connections

	mount -t devtmpfs   none /dev
	mkdir /dev/{pts,mqueue,hugepages,shm}
	mount -t devpts     none /dev/pts
	mount -t mqueue     none /dev/mqueue
	mount -t hugetlbfs  none /dev/hugepages
	mount -t tmpfs      none /dev/shm
	# XXX securityfs

	mount -t tmpfs	none /run
	mkdir /run/lock
	mount -t tmpfs	none /run/lock
	mount -t tmpfs	none /tmp

	# run program in cwd in new terminal session attached to console
	# (if we don't establish a session accessing /dev/tty will give "No such device or address")
	test -n "$CWD" || qdie "CWD=?"
	cd "$CWD"

	test $# != 0 || qdie "no program to run"
	#cat /proc/cmdline
	#env
	#set -x
	set +e
	setsid "$0" .qinit2 "$@" <>/dev/ttyS0 >&0 2>&1	# run qinit2 with argv[1:] passed to init
	qinfo "exit code: $?"

	qshutdown
	sleep 1d # give time to shutdown
	qdie "unreachable"
fi

# init part spawned from under setsid.
# $0 .qinit2 <command> ...
if [ "$1" == .qinit2 ]; then
	# initialize terminal. In particular this has the effect to restore
	# line wrapping for xterm, as kernel, initially assuming it has "linux"
	# type terminal, somehow messes xterm settings.
	command -v tput >/dev/null && tput init
	# resize terminal to current host's xterm
	command -v resize >/dev/null && eval `resize`

	# tail to argv[1:] passed to init
	shift
	exec "$@"
fi


# ---- qemu setup ----

die() {
	echo "$*" 1>&2
	exit 1
}

usage() {
	cat <<EOF
Usage: qemu-runlinux [options] <kernel> <program> ...
Run linux/program under QEMU with rootfs taken from host.

<kernel>	is path vmlinuz-like kernel image
<program> ...	is program to run and arguments to it.

Options:

	-v	increase verbosity

			0: ERROR+ on boot/run
			1: INFO+  on      run
			2: INFO+  on boot/run
			3: DEBUG+ on boot/run

	-g	run with graphics
EOF
}

# by default output goes to stdout with both /dev/console and /dev/ttyS0 (where
# program is run) attached there.
verbose=0
nographic=y

while test $# != 0
do
	case "$1" in
	-v)
		verbose=$(($verbose + 1));;
	-vv)
		verbose=$(($verbose + 2));;
	-vvv)
		verbose=$(($verbose + 3));;

	-g)	# run with graphics UI, /dev/console goes to VGA; program to /dev/ttyS0
		nographic=;;
	-h)
		usage
		exit 0
		;;
	*)
		break;;
	esac
	shift
done

kernel=$1
test -n "$kernel" || die "kernel not specified"

shift
prog="$@"
test -n "$prog" || die "program not specified"

dir=`pwd`

# loglevel: default ERROR+ on boot/run
loglevel=4
qrun_loglevel=4
test $verbose -ge 1 && qrun_loglevel=7	# INFO+ on run
test $verbose -ge 2 && loglevel=7	# INFO+  on boot/run
test $verbose -ge 3 && loglevel=8	# DEBUG+ on boot/run
test $loglevel -gt 4 && qrun_loglevel=$loglevel

# may be also useful:
#   -serial  stdio
#   -serial  file:ttyS0
#   -monitor stdio
#   -serial  stdio
#   -S -gdb tcp::1234

# NOTES
#   - for kernel to mount root, 9P support must be compiled in:
#       CONFIG_NET_9P=y
#       CONFIG_NET_9P_VIRTIO=y
#       CONFIG_9P_FS=y
#
#   - mount_tag *must* be /dev/root - as of 3.17-rc1 the kernel hardcodes it
#
# References
#   http://unix.stackexchange.com/questions/90423/can-virtfs-9p-be-used-as-root-file-system
#   http://stackoverflow.com/questions/11408041/kernel-debugging-with-gdb-and-qemu
arch=`uname -m`
qemu-system-$arch   \
    -enable-kvm \
    ${nographic:+-nographic}	\
 \
    -m 512M `# default 128M is too limiting` \
 \
    -fsdev  local,id=R,path=/,security_model=none,readonly,multidevs=remap  \
    -device virtio-9p-pci,fsdev=R,mount_tag=/dev/root   \
 \
    -kernel $kernel \
    -append "ro rootfstype=9p rootflags=trans=virtio \
${nographic:+console=ttyS0} loglevel=$loglevel qrun_loglevel=$qrun_loglevel \
init="$(realpath $0)" \
CWD="$dir" HOME="$HOME" LANG="$LANG" ${nographic:+TERM="$TERM"} PATH="$PATH" \
-- $prog \
"
