#!/usr/bin/env bash
# Copyright (C) 2011-2012 Red Hat, Inc. All rights reserved.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions
# of the GNU General Public License v.2.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

set -e
MAX_TRIES=4
IFS_NL='
'

die() {
	rm -f debug.log*
	echo -e "$@" >&2
	return 1
}

rand_bytes() {
	n=$1

	chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

	dev_rand="/dev/urandom"
	if test -r "$dev_rand"; then
		# Note: 256-length($chars) == 194; 3 copies of $chars is 186 + 8 = 194.
		head -c"$n" "$dev_rand" | tr -c "$chars" "01234567$chars$chars$chars"
		return
	fi

	cmds='date; date +%N; free; who -a; w; ps auxww; ps ef; netstat -n'
	data=$( (eval "$cmds") 2>&1 | gzip )

	n_plus_50=$(( n + 50 ))

	# Ensure that $data has length at least 50+$n
	while :; do
		len=${#data} # number of chars in $data
		test "$n_plus_50" -le "$len" && break;
		data=$( (echo "$data"; eval "$cmds") 2>&1 | gzip )
	done

	echo "$data" | dd bs=1 skip=50 count="$n" 2>/dev/null \
		| tr -c "$chars" "01234567$chars$chars$chars"
}

mkdtemp() {
	case $# in
		2) ;;
		*) die "Usage: mkdtemp DIR TEMPLATE";;
	esac

	destdir=$1
	template=$2

        test -d "$destdir" || die "DIR ('$destdir') does not exist."

	case "$template" in
		*XXXX) ;;
		*) die "Invalid template: $template (must have a suffix of at least 4 X's)";;
	esac

	fail=0

	# First, try to use mktemp.
	d=$(env -u TMPDIR mktemp -d -t -p "$destdir" "$template" 2>/dev/null) || fail=1

	# The resulting name must be in the specified directory.
	case "$d" in "${destdir}"*);; *) fail=1;; esac

	# It must have created the directory.
	test -d "$d" || fail=1

	# It must have 0700 permissions.
	perms=$(ls -dgo "$d" 2>/dev/null) || fail=1
	case "$perms" in drwx------*) ;; *) fail=1;; esac

	test $fail = 0 && { echo "$d"; return; }

	# If we reach this point, we'll have to create a directory manually.

	# Get a copy of the template without its suffix of X's.
	base_template=$(echo "$template" | sed 's/XX*$//')

	# Calculate how many X's we've just removed.
	nx=$(( ${#template} - ${#base_template} ))

	err=
	i=1
	while :; do
		X=$(rand_bytes "$nx")
		candidate_dir="$destdir/$base_template$X"
		err=$(mkdir -m 0700 "$candidate_dir" 2>&1) && \
			{ echo "$candidate_dir"; return; }
		test $MAX_TRIES -le $i && break;
		i=$(( i + 1 ))
	done
	die "$err"
}

# Like grep, just always print 1st. line
grep1_() {
	awk -v pattern="${1}" 'NR==1 || $0~pattern' "${@:2}"
}

stacktrace() {
	trap - ERR
	# i=1 - ignoring innermost frame - it is always stacktrace function
	local i=1 n=${#BASH_LINENO[*]}
	# n-=1 - ignoring last frame as well - it is not interesting
	n=$(( n - 1 ))

	echo "## - $0:${BASH_LINENO[$((n-1))]}"
	while [[ $i -lt $n ]]; do
		echo "## $i ${FUNCNAME[$i]}() called from ${BASH_SOURCE[$((i+1))]}:${BASH_LINENO[$i]}"
		i=$(( i + 1 ))
	done
}

STACKTRACE() {
	trap - ERR
	local i

	stacktrace

	test "${LVM_TEST_PARALLEL:-0}" -eq 0 && test -z "$RUNNING_DMEVENTD" && \
		test ! -f LOCAL_DMEVENTD && pgrep dmeventd >DPID 2>/dev/null  && {
			echo "## ERROR: The test started dmeventd ($(< DPID)) unexpectedly."
			kill "$(< DPID)"
		}

	# Get backtraces from coredumps
	if which gdb &>/dev/null; then
		# Check for all cores newer then TESTNAME file
		# Assume users keep prefix 'core'
		# TODO: possibly better integrate with coredumpctl & systemd
		while IFS= read -r i; do
			bin=$(gdb -batch -c "$i" 2>&1 | grep "generated by" | \
			sed -e "s,.*generated by \`\([^ ']*\).*,\1,") || continue
			{
				echo bt full
				echo l
				echo quit
			} > gdb_commands.txt || rm -f gdb_commands.txt

			if test ! -s gdb_commands.txt ; then
				echo "Out of disk space, can't check coredump $i generated by $bin."
				continue
			fi

			echo "## Checking coredump: $i generated by $bin."
			gdb -batch -c "$i" -x gdb_commands.txt "$(which "$bin")" 2>/dev/null | \
			sed -e "s,^,## GDB:	," || continue
		done < <(find . "$(dirname "$(sysctl -n kernel.core_pattern)")" \
			"/var/lib/systemd/coredump/" -name 'core*' -newer TESTNAME 2>/dev/null || true)
	fi

	test -f SKIP_THIS_TEST && exit 200

	test -z "$LVM_TEST_NODEBUG" && test -f TESTNAME && {
		local name
		local idx=0
		for i in debug.log* ; do
			test -f "$i" || break  # nothing is found (expands to debug.log*)
			name=${i##debug.log_}
			name=${name%%_*}
			test "$name" = "DEBUG" && { name="$name$idx" ; idx=$(( idx + 1 )) ; }
			echo "<======== Debug log $i ========>"
			sed -e "s,^,## $name: ," "$i"
			mv -f "$i" "debug_${i#debug.}"
		done
		if test -e strace.log ; then
			echo "<======== Strace debug log ========>"
			sed -e "s,^,## STRACE: ," strace.log
		fi
		if dmsetup info -c | grep -q "$PREFIX" ; then
			echo "<======== Info ========>"
			dmsetup info -c | grep1_ "$PREFIX"| sed -e "s,^,## DMINFO:   ,"
			echo "<======== Active table ========>"
			dmsetup table | grep "$PREFIX" | sed -e "s,^,## DMTABLE:  ,"
			echo "<======== Inactive table ========>"
			dmsetup table --inactive  | grep "$PREFIX" | sed -e "s,^,## DMITABLE: ,"
			echo "<======== Status ========>"
			dmsetup status --noflush | grep "$PREFIX" | sed -e "s,^,## DMSTATUS: ,"
			echo "<======== Tree ========>"
			dmsetup ls --tree | sed -e "s,^,## DMTREE:   ,"
			echo "<======== Recursive list of $DM_DEV_DIR ========>"
			ls -lR -I bsg -I bus -I char -Idma_heap -I dri \
			   -I hugepages -I input -I mqueue \
			   -I net -I pts -I shm -I snd \
			   -I tty?* -I usb -I vfio -I vcs?* \
			   -I virtio-ports \
			   "$DM_DEV_DIR" | sed -e "s,^,## LS_LR:	,"
			echo "<======== Udev DB content ========>"
			for i in /sys/block/dm-* /sys/block/loop* ; do
				udevadm info --query=all --path "$i" 2>/dev/null || true
			done | sed -e "s,^,## UDEV:	,"
		fi
		echo "<======== Free space ========>"
		df -h | sed -e "s,^,## DF_H:	,"
		echo "<======== Script file \"$(< TESTNAME)\" ========>"
		local script=$0
		test -f "$script" || script="$TESTOLDPWD/$0"
		awk '{print "## Line:", NR, "\t", $0}' "$script"
	}
}

init_udev_transaction() {
	if test "$DM_UDEV_SYNCHRONIZATION" = 1; then
		local cookie
		cookie=$(dmsetup udevcreatecookie)
		# Cookie is not generated if udev is not running!
		test -z "$cookie" || export DM_UDEV_COOKIE=$cookie
	fi
}

finish_udev_transaction() {
	if test "$DM_UDEV_SYNCHRONIZATION" = 1 && test -n "${DM_UDEV_COOKIE-}" ; then
		dmsetup udevreleasecookie || true
		unset DM_UDEV_COOKIE
	fi
}

teardown_udev_cookies() {
	if test "$DM_UDEV_SYNCHRONIZATION" = 1; then
		# Delete any cookies created more than 10 minutes ago
		# and not used in the last 10 minutes.
		# Log only non-zero semaphores count
		(dmsetup udevcomplete_all -y 10 | grep -v "^0 ") || true
	fi
}

dm_info() {
	should dmsetup info --noheadings -c -o "$@"
}

dm_status() {
	should dmsetup status --noheadings "$@"
}

dm_table() {
	should dmsetup table "$@"
}

skip() {
	set +vx # debug off
	if test "$#" -eq 0; then
		stacktrace
	else
		echo -e "TEST SKIPPED:" "$@"
	fi
	touch SKIP_THIS_TEST
	exit 200
}

get_real_devs() {
	REAL_DEVICES=( $(<REAL_DEVICES) )
	export REAL_DEVICES
}

get_devs() {
	local IFS=$IFS_NL
	DEVICES=( $(<DEVICES) )
	export DEVICES
#	local DEVS=( $(<DEVICES) )
#	eval "$1"'=("${DEVS[@]}")'
}

prepare_test_vars() {
	vg="${PREFIX}vg"
	lv="LV"

	for i in {1..16}; do
		eval "lv$i=\"LV$i\""
		eval "vg$i=\"${PREFIX}vg$i\""
	done

	if test -n "$LVM_TEST_DEVICE_LIST"; then
		local count=0
		while read path; do
			count=$((  count + 1 ))
			eval "dev$count=\"$path\""
		done < "$LVM_TEST_DEVICE_LIST"
	else
		for i in {1..16}; do
			eval "dev$i=\"$DM_DEV_DIR/mapper/${PREFIX}pv$i\""
		done
	fi
}

if test -z "${abs_top_builddir+varset}" && test -z "${installed_testsuite+varset}"; then
    . lib/paths || die "something went wrong -- lib/paths is missing?"
fi

if test -z "${installed_testsuite+varset}"; then
    case "$PATH" in
    *"$abs_top_builddir/test/lib"*) ;;
    *)
	PATH="$abs_top_builddir/test/lib:$abs_top_builddir/test/api:$PATH"
	LVM_BINARY=$(which lvm)
	LD_LIBRARY_PATH="$abs_top_builddir/daemons/dmeventd:$abs_top_builddir/tools:$abs_top_builddir/libdm:$LD_LIBRARY_PATH"
	export PATH LD_LIBRARY_PATH LVM_BINARY ;;
    esac
fi

test -z "$PREFIX" || prepare_test_vars
