#!/bin/bash
# Copyright (c) International Business Machines  Corp., 2008
# Author: Matt Helsley <matthltc@us.ibm.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#

# A library of cgroup test functions for testing the cgroup freezer and,
# if present, a cgroup signal subsystem.
#
# Most of these assume the current directory is the cgroup of interest.
# mount_{freezer|signal} and make_sample_cgroup are the exceptions to this rule.
#
# On failure, a message indicating what failed is printed and the
#    exit status of the failing command is returned. If "result" is unset
#    then we assign the exit status of the failed command to it.
#
# The variable "result" holds the exit status of the first command that failed,
#    $UNFINISHED if no command has failed yet, or $FINISHED if no command
#    has failed and we've finished all significant parts of the test. Note:
#    significant parts usually do not include the cleanup of the test.
#


# xargs 4.1.20 only accepts -i instead of -I
# However -I is added and -i deprecated somewhere between (4.1.20, 4.2.32]
XRGSV=$(xargs --version | sed -e 's/^[^[:digit:]]*//')
export XARGS_REPL_STR="%"
case ${XRGSV} in
[456789].[23456789][0-9]*.*|[1-9][0-9][0-9]*.*.*) # version > 4.1.*
	export XARGS_REPL_OPT="-I${XARGS_REPL_STR}"
	;;
4.1.*|*)
	export XARGS_REPL_OPT="-i${XARGS_REPL_STR}"
	;;
esac
unset XRGSV

export UNFINISHED=""
export FINISHED=0
export TMP=${TMP:-/tmp}

#
# Tests are either running or cleaning up. Cleanup phases do not emit TFAIL.
#
export LIB_TEST_STATE="running"

export max_state_samples=5 # number of times to sample
export sample_state_period=1  # number of seconds between samplings
export sample_sleep=5     # how long the sample program should sleep
export result="$UNFINISHED"

# These are echo'd to freezer.state.
export FREEZE='FROZEN'
export THAW='THAWED'

# We use /bin/echo to write to cgroup files because it's exit status will not
# hide write errors (bash's echo does not indicate if the write succeeded).
export CG_FILE_WRITE=/bin/echo

declare -r UNFINISHED FINISHED FREEZE THAW max_state_samples sample_state_period

# When we're running we want to issue failure results when things go wrong.
function running_cgroup_test()
{
	export LIB_TEST_STATE="TFAIL"
}

# But when we're cleaning up we want to issue warnings (if not TINFO).
function cleanup_cgroup_test()
{
	export LIB_TEST_STATE="TWARN"
}

# Mounts the cgroup filesystem somewhere and pushes pwd onto the dir stack
function mount_cgroup_subsys()
{
	local rc=0

	mkdir -p $TMP/${cgroup_subsys}_test > /dev/null 2>&1
	rc=$?

	# Don't test status because we don't care if the directory already
	# exists.

	if [ ! -d $TMP/${cgroup_subsys}_test ]; then
		result=${result:-$rc}
		tst_brkm TBROK "Failed to make mount point for cgroup filesystem"
		return $result
	fi

	mount -t cgroup -o${cgroup_subsys} test_cgroup_${cgroup_subsys} $TMP/${cgroup_subsys}_test
	rc=$?
	if [ $rc -ne 0 ]; then
		result=${result:-$rc}
		tst_resm TINFO "Failed to mount cgroup filesystem with ${cgroup_subsys} subsystem."
		rmdir $TMP/${cgroup_subsys}_test 2> /dev/null
		return $rc
	fi

	export mount_cgroup_freezer_saved_dir="$(pwd)"
	cd $TMP/${cgroup_subsys}_test > /dev/null 2>&1
	rc=$?
	if [ $rc -ne 0 ]; then
		result=${result:-$rc}
		tst_brkm TBROK "Failed to change working directory into cgroup filesystem (pwd: \"$(pwd)\")"
		umount $TMP/${cgroup_subsys}_test || umount -l $TMP/${cgroup_subsys}_test || tst_brkm TBROK "Failed to unmount cgroup filesystem with ${cgroup_subsys} subsystem"
		rmdir $TMP/${cgroup_subsys}_test 2> /dev/null
		return $rc
	fi

	return 0
}

function mount_freezer()
{
	export cgroup_subsys=freezer
	mount_cgroup_subsys
}

function mount_signal()
{
	export cgroup_subsys=signal
	mount_cgroup_subsys
}

# umounts the cgroup filesystem and pops the dir stack
function umount_cgroup_subsys()
{
	cd "${mount_cgroup_freezer_saved_dir}"
	local cwd_result=$?
	umount $TMP/${cgroup_subsys}_test > /dev/null 2>&1 \
	  || umount -l $TMP/${cgroup_subsys}_test || \
	    tst_brkm TBROK "Failed to unmount cgroup filesystem (umount exit status: $?)"
	local umnt_result=$?
	local rc=0

	if [ $cwd_result -ne 0 ]; then
		result=${result:-$cwd_result}
		rc=$cwd_result
	elif [ $umnt_result -ne 0 ]; then
		result=${result:-$umnt_result}
		rc=$umnt_result
	elif [ $umnt_result -eq 0 -a $cwd_result -eq 0 ]; then
		rmdir $TMP/${cgroup_subsys}_test
		return 0
	fi

	return $rc
}

function umount_freezer()
{
	[ "${cgroup_subsys}" != "freezer" ] && {
		result=${result:-5}
		exit -1
	}
	umount_cgroup_subsys
	unset cgroup_subsys
}

function cleanup_freezer()
{
	local save_result="${result}"
	local save_pwd="$(pwd)"

	mount_freezer && {
		# Run these commands in bash because we need $(cmd subst) and
		# we need to redirect to different freezer.state files for each
		# group
		# Kill any leftover tasks
		disown -a
		find $TMP/${cgroup_subsys}_test -mindepth 1 -depth -type d -print0 | \
		xargs -0r -n 1 ${XARGS_REPL_OPT} /bin/bash -c 'kill $(cat "'"${XARGS_REPL_STR}"'/tasks") 2> /dev/null'

		# For each group in the freezer hierarch, that its tasks
		find $TMP/${cgroup_subsys}_test -mindepth 1 -depth -type d -print0 | \
		xargs -0r -n 1 ${XARGS_REPL_OPT} /bin/bash -c "\"${CG_FILE_WRITE}\" \"${THAW}\" > '${XARGS_REPL_STR}/freezer.state'"

		# Kill any leftover tasks
		find $TMP/${cgroup_subsys}_test -mindepth 1 -depth -type d -print0 | \
		xargs -0r -n 1 ${XARGS_REPL_OPT} /bin/bash -c 'kill $(cat "'"${XARGS_REPL_STR}"'/tasks") 2> /dev/null'

		sleep 2

		# Really kill any leftover tasks
		find $TMP/${cgroup_subsys}_test -mindepth 1 -depth -type d -print0 | \
		xargs -0r -n 1 ${XARGS_REPL_OPT} /bin/bash -c 'kill -s SIGKILL $(cat "'"${XARGS_REPL_STR}"'/tasks") 2> /dev/null'

		# Don't need to run these xargs commands in bash since we want
		# to see what's left on stdout
		LINES=$(find $TMP/${cgroup_subsys}_test -mindepth 1 -depth -type d -print0 | \
			xargs -0r -n 1 ${XARGS_REPL_OPT} cat "${XARGS_REPL_STR}/tasks" | wc -l)
		if (( LINES == 0 )); then
			# Remove the empty groups
			find $TMP/${cgroup_subsys}_test -mindepth 1 -depth -type d -print0 | xargs -r0 rmdir
		else
			tst_resm TWARN "Could not cleanup:"
			find $TMP/${cgroup_subsys}_test -mindepth 1 -depth -type d -print0 | xargs -0r -n 1 ${XARGS_REPL_OPT} ls -ld "${XARGS_REPL_STR}/tasks"
		fi

		umount_freezer
	}

	if [ "$save_pwd" != `pwd` ]; then
		tst_resm TWARN "libcgroup_subsys: cleanup_freezer() is broken"
		cd "$save_pwd"
	fi

	result="${save_result}"
}

function umount_signal()
{
	[ "${cgroup_subsys}" != "signal" ] && {
		result=${result:-6}
		exit -1
	}
	umount_cgroup_subsys
	unset cgroup_subsys
}

function cleanup_signal()
{
	local save_result="${result}"
	local save_pwd="$(pwd)"

	mount_signal && {
		find $TMP/${cgroup_subsys}_test -mindepth 1 -depth -type d -print0 | xargs -r0 rmdir
		umount_signal
	}

	if [ "$save_pwd" != `pwd` ]; then
		tst_resm TWARN "libcgroup_subsys: cleanup_signal() is broken"
		cd "$save_pwd"
	fi
	result="${save_result}"
}

function assert_cgroup_rwfile()
{
	local file="$1"
	local descr="$2"
	local rc=0

	if [ ! -e "${file}" ]; then
		tst_resm ${LIB_TEST_STATE} "$descr missing"
		rc=1
	fi

	if [ ! -f "${file}" ]; then
		tst_resm ${LIB_TEST_STATE} "$descr is not a regular file"
		rc=2
	fi

	if [ ! -r "${file}" ]; then
		tst_resm ${LIB_TEST_STATE} "$descr is not readable"
		rc=3
	fi

	if [ ! -w "${file}" ]; then
		tst_resm ${LIB_TEST_STATE} "$descr is not writeable"
		rc=4
	fi

	[ $rc -ne 0 ] && {
		result=${result:-$rc}
		local s="$(stat "${file}")"
		tst_resm ${LIB_TEST_STATE} "${s}"
	}

	return $rc
}

function assert_cgroup_tasks_rwfile()
{
	assert_cgroup_rwfile "tasks" "task list"
	return $?
}

function dump_named_cgroup_tasks()
{
	local cgroup_name="$1"
	local tasks

	tasks=( $(cat "${cgroup_name}/tasks") ) # don't assign directly (bash bug)
	if [ -z "${tasks[*]}" ]; then
		return 0
	fi
	ps -p "${tasks[*]}" -o 'pid,ppid,pgid,tid,tpgid,blocked,ignored,pending,stat,tty,args'
}

function dump_cgroup_tasks()
{
	dump_named_cgroup_tasks "$(pwd)"
}

function assert_cgroup_tasks_empty()
{
	local nlines=$(( `cat tasks | wc -l` + 0))
	local rc=$?

	[ $rc -eq 0 -a $nlines -eq 0 ] && return 0
	rc=$?
	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "cgroup task list is not empty: "
	dump_cgroup_tasks 1>&2
	return $rc
}

function assert_task_in_named_cgroup()
{
	local task_pid=$1
	local cgroup_name="$2"

	cat "${cgroup_name}/tasks" | grep -E "^${task_pid}\$" > /dev/null 2>&1 && return 0
	local rc=$?
	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "Expected pid ${task_pid} is not in \"${cgroup_name}\" task list"
	dump_named_cgroup_tasks "${cgroup_name}" 1>&2
	return $rc
}

function assert_task_not_in_named_cgroup()
{
	local task_pid=$1
	local cgroup_name="$2"

	cat "${cgroup_name}/tasks" | grep -E "^${task_pid}\$" > /dev/null 2>&1 || return 0
	local rc=1 # $? == 0 is an error in this case
	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "Expected pid ${task_pid} is in \"${cgroup_name}\" task list"
	dump_named_cgroup_tasks "${cgroup_name}" 1>&2
	return $rc
}

function assert_task_in_cgroup()
{
	assert_task_in_named_cgroup $1 "$(pwd)"
	return $?
}

function assert_task_not_in_cgroup()
{
	assert_task_not_in_named_cgroup $1 "$(pwd)"
	return $?
}

function assert_sample_proc_in_cgroup()
{
	assert_task_in_cgroup $sample_proc
	return $?
}

function assert_sample_proc_not_in_cgroup()
{
	assert_task_not_in_cgroup $sample_proc
	return $?
}

function assert_sample_proc_in_named_cgroup()
{
	assert_task_in_named_cgroup $sample_proc "$1"
	return $?
}

function assert_sample_proc_not_in_named_cgroup()
{
	assert_task_not_in_named_cgroup $sample_proc "$1"
	return $?
}

function get_task_state()
{
	ps -p $1 -o 'state=' 2>/dev/null
}

# TODO Check: Do these need to ignore case differences?
function assert_task_state()
{
	local task_pid=$1
	local expected_state="$2"
	local ps_state="$(get_task_state ${task_pid})"
	local rc=$?

	[ $rc -eq 0 -a "$ps_state" == "${expected_state}" ] && return 0
	rc=$?
	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "Expected task ${task_pid} to be in state \"${expected_state}\""
	return $rc
}

#
# Check that the specified task is not in the specified state
#
function assert_task_not_in_state()
{
	local task_pid=$1
	local expected_state="$2"
	local ps_state="$(get_task_state ${task_pid})"
 	local rc=$?

	[ $rc -eq 0 -a "$ps_state" != "${expected_state}" ] && return 0
	rc=$?
	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "Expected task ${task_pid} to not be in state \"${expected_state}\""
	return $rc
}

#
# Frozen tasks are in the "D" state according to ps
# tasks in "T" state may also be in a "frozen" state
#
function assert_task_not_frozen()
{
	local task_pid=$1
	local ps_state="$(ps -p $task_pid -o 'state=')"
	local rc=$?

	[ $rc -eq 0 -a "$ps_state" != "D" ] && return 0
	rc=$?
	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "Expected task ${task_pid} is not frozen (unexpected task state: \"$ps_state\")"
	return $rc
}

function assert_task_is_frozen()
{
	local task_pid=$1
	local ps_state="$(ps -p $task_pid -o 'state=')"
	local rc=$?

	[ $rc -eq 0 -a "$ps_state" == "D" -o "$ps_state" == "T" ] && return 0
	rc=$?
	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "Expected task ${task_pid} to be frozen (unexpected task state: \"$ps_state\")"
	return $rc
}

function assert_sample_proc_not_frozen()
{
	assert_task_not_frozen $sample_proc
	return $?
}

function assert_sample_proc_is_frozen()
{
	assert_task_is_frozen $sample_proc
	return $?
}

function assert_sample_proc_stopped()
{
	assert_task_state $sample_proc 'T'
	return $?
}

function assert_sample_proc_not_stopped()
{
	assert_task_not_in_state $sample_proc 'T'
	return $?
}

function assert_sample_proc_sleeping()
{
	assert_task_state $sample_proc 'S'
	return $?
}

function assert_sample_proc_not_sleeping()
{
	assert_task_not_in_state $sample_proc 'S'
	return $?
}

function assert_cgroup_subsys_state_rwfile()
{
	if [ "${cgroup_subsys}" == "freezer" ]; then
		assert_cgroup_rwfile "freezer.state" "freezer state"
		return $?
	elif [ "${cgroup_subsys}" == "freezer" ]; then
		assert_cgroup_rwfile "signal.kill" "signal file"
		return $?
	else
		return -1
	fi
}

function get_freezer_state()
{
	local state="$(cat freezer.state)"
	local rc=$?

	if [ $rc -ne 0 ]; then
		result=${result:-$rc}
		tst_resm ${LIB_TEST_STATE} "Failed to read freezer state."
		return $rc
	fi
	echo "${state}"
	return 0
}

function assert_cgroup_freezer_state()
{
	local goal_state="$1"
	local state="$(get_freezer_state)"
	local rc=$?

	[ $rc -eq 0 -a "${state}" == "${goal_state}" ] && return 0
	rc=$?
	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "Expected freezer state \"$2\" but found freezer state: \"$state\""
	return $rc
}

function make_sample_cgroup_named()
{
	local name="$1"
	local saved_dir="$(pwd)"
	mkdir "${name}"
	local rc=$?

	# So long as we made the directory we don't care
	if [ ! -d "${name}" -a $rc -ne 0 ]; then
		# But if it doesn't exist report the exit status of mkdir
		result=${result:-$rc}
		return $rc
	fi

	cd "${name}" > /dev/null 2>&1

	rc=$?
	if [ $rc -ne 0 ]; then
		result=${result:-$rc}
		return $rc
	fi

	assert_cgroup_tasks_rwfile || {
		cd "${saved_dir}"
		return $?
	}
	assert_cgroup_tasks_empty || {
		cd "${saved_dir}"
		return $?
	}
	assert_cgroup_subsys_state_rwfile || {
		cd "${saved_dir}"
		return $?
	}
	cd "${saved_dir}"
	return 0
}

function make_sample_cgroup()
{
	make_sample_cgroup_named "child"
	local rc=$?

	# So long as we made the directory we don't care
	if [ $rc -ne 0 ]; then
		return $rc
	fi

	cd "child" # we know this will succeed since make_sample_cgroup_named
		   # tested this
	return 0
}

function rm_sample_cgroup_named()
{
	local cgroup_name="$1"
	local saved_dir="$(pwd)"
	local rc=0

	cd "${cgroup_name}" && {
		assert_cgroup_tasks_rwfile || {
			cd "${saved_dir}"
			return $?
		}
		assert_cgroup_tasks_empty || {
			cd "${saved_dir}"
			return $?
		}
		assert_cgroup_subsys_state_rwfile || {
			cd "${saved_dir}"
			return $?
		}
		cd "${saved_dir}"
	} || {
		rc=$?
		result=${result:-$rc}
		return $rc
	}

	[ -d "${cgroup_name}" ] && rmdir "${cgroup_name}" && return 0
	rc=$?
	tst_resm TWARN "Failed to remove cgroup \"${cgroup_name}\""
	result=${result:-$rc}
	return $rc
}

function rm_sample_cgroup()
{
	local cgroup_name="$(basename $(pwd))"
	local rc=0

	cd .. || {
		rc=$?
		result=${result:-$rc}
		return $rc
	}
	rm_sample_cgroup_named "${cgroup_name}"
	return $?
}

function ls_pids()
{
	ps -e -o 'pid=' | sed -e 's/[[:space:]]\+//g'
}

function assert_task_exists()
{
	local task_pid=$1

	ls_pids | grep -E "^${task_pid}\$" > /dev/null 2>&1 && return 0
	local rc=$?

	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "Expected pid ${task_pid} does not exist"
	return $rc
}

function assert_task_does_not_exist()
{
	local task_pid=$1

	ls_pids | grep -E "^${task_pid}\$" > /dev/null 2>&1 || return 0
	local rc=1 # $? == 0 is an error in this case

	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "Did not expect pid ${task_pid} to exist"
	return $rc
}

function assert_sample_proc_exists()
{
	assert_task_exists $sample_proc
	return $?
}

function assert_sample_proc_does_not_exist()
{
	assert_task_does_not_exist $sample_proc
	return $rc
}

function start_sample_proc()
{
	local sample_cmd="/bin/sleep"
	local args

	args=( $sample_sleep ) # can't assign directly because of bash v2/v3 inconsistency
	if [ $# -gt 0 ]; then
		sample_cmd="$1"
		shift 1
		args=( "$@" )
	fi

	[ -n "$sample_proc" ] && assert_sample_proc_does_not_exist

	"$sample_cmd" "${args[@]}" &
	local rc=$?
	export sample_proc=$!
	assert_sample_proc_exists

	return $rc
}

function add_sample_proc_to_named_cgroup()
{
	local cgroup_name="$1"

	assert_sample_proc_exists
	"${CG_FILE_WRITE}" $sample_proc > "${cgroup_name}/tasks"
	local rc=$?
	if [ $rc -ne 0 ]; then
		result=${result:-$rc}
		tst_resm ${LIB_TEST_STATE} "Failed to add sample process $sample_proc to cgroup \"${cgroup_name}\""
		return $rc
	fi
	assert_task_in_named_cgroup $sample_proc "${cgroup_name}"
	return $?
}

function add_sample_proc_to_cgroup()
{
	add_sample_proc_to_named_cgroup "$(pwd)"
	return $?
}

function kill_sample_proc()
{
	if [ -z "$sample_proc" ]; then
		# It's no longer running or never started.
		# If it was supposed to have started but did not then that
		# should be determined by checking start_sample_proc results.
		return 0
	fi

	# Hey, bash, don't print out any of your messy job status notices
	disown -a

	if [ "$(get_task_state $sample_proc)" == "D" ]; then
		tst_resm TWARN "sample process is frozen stiff"
		kill $sample_proc
		local rc=$?
		result=${result:-$rc}
		return $rc
	fi

	# kill child processes of the sample process
	while pgrep -P $sample_proc ; do
		pkill -SIGTERM -P $sample_proc
		sleep 1
		pkill -SIGKILL -P $sample_proc

		ps -p $(pgrep -P $sample_proc) -o 'state=' | grep -v D && continue
		# Give up if all the child processes are frozen in D state or
		# if there aren't any more child processes
		break
	done
	# DEBUG dump pstree under $sample_proc:
	# pstree -A -p $sample_proc
	kill $sample_proc > /dev/null 2>&1 || kill -s SIGKILL $sample_proc > /dev/null 2>&1 || {
		local rc=$?

		ps -p $sample_proc -o 'state=' > /dev/null 2>&1
		if [ $? -eq 1 ]; then
			# It's dead. We're OK.
			return 0
		fi
		# It's still alive somehow! Give up.
		result=${result:-$rc}
		tst_resm TWARN "Failed to kill sample process $sample_proc (kill exit status: $rc)"
	}
	assert_sample_proc_not_in_cgroup
	assert_sample_proc_does_not_exist
	return $?
}

function issue_freeze_cmd()
{
	local goal_state="FROZEN"
	local sample_state_count=1
	local state="$(get_freezer_state)"
	local rc=$?

	if [ $rc -ne 0 ]; then
		return $rc
	fi

	while [ "${state}" != "${goal_state}" ]; do
		"${CG_FILE_WRITE}" "${FREEZE}" > freezer.state
		sleep $sample_state_period
		state="$(get_freezer_state)"
		rc=$?
		if [ $rc -ne 0 ]; then
			break
		fi

		((sample_state_count++))
		if [ "$sample_state_count" -ge "$max_state_samples" ]; then
			break
		fi
	done

	if [ "${state}" == "${goal_state}" ]; then
		return 0
	fi

	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "Failed to issue freeze command (freezer state: \"`get_freezer_state`\")."
	return $rc
}

# If we're trying to "freeze" tasks with SIGTOP
function issue_stop_as_freeze_cmd()
{
	local goal_state="T"
	local sample_state_count=1
	local ps_state="$(get_task_state ${task_pid})"
	local rc=$?

	if [ $rc -ne 0 ]; then
		return $rc
	fi

	while [ "${ps_state}" != "${goal_state}" ]; do
		kill -s SIGSTOP $sample_proc
		sleep $sample_state_period
		ps_state="$(get_task_state ${task_pid})"
		rc=$?
		if [ $rc -ne 0 ]; then
			break
		fi

		((sample_state_count++))
		if [ "$sample_state_count" -ge "$max_state_samples" ]; then
			break
		fi
	done

	if [ "${ps_state}" == "${goal_state}" ]; then
		return 0
	fi

	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "Failed to issue stop (freeze) command (task state: \"${ps_state}\")."
	return $rc
}

function send_signal()
{
	"${CG_FILE_WRITE}" $1 > 'signal.kill' && return 0
	local rc=$?
	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "Failed to send signal: $1 to tasks in cgroup (rc: $rc)"
 	return $rc
}

function wait_until_goal_state_or_timeout()
{
	local goal_state="$1"
	local sample_state_count=1
	local state="$(get_freezer_state)"
	local rc=$?

	if [ $rc -ne 0 ]; then
		return $rc
	fi

	while [ "${state}" != "${goal_state}" ]; do
		sleep $sample_state_period
		state="$(get_freezer_state)"
		rc=$?
		if [ $rc -ne 0 ]; then
			break
		fi

		((sample_state_count++))
		if [ "$sample_state_count" -ge "$max_state_samples" ]; then
			break
		fi
	done
	return $rc
}

# TODO convert signal scripts -- insert task between until and goal
function wait_until_sample_proc_goal_state_or_timeout()
{
	local goal_state="$1"
	local sample_state_count=1
	local ps_state="$(get_task_state ${sample_proc})"
	local rc=$?

	while [ $rc -eq 0 -a "${ps_state}" != "${goal_state}" -a \
		"$sample_state_count" -lt "$max_state_samples" ]; do
		sleep $sample_state_period
		ps_state="$(get_task_state ${sample_proc})"
		rc=$?
		if [ $rc -ne 0 ]; then
			result=${result:-$rc}
			tst_resm ${LIB_TEST_STATE} "Failed to read process state."
			break
		fi

		((sample_state_count++))
	done
	return $rc
}

# TODO convert signal scripts -- insert task between until and not
function wait_until_sample_proc_not_goal_state_or_timeout()
{
	local goal_state="$1"
	local sample_state_count=1
	local ps_state="$(get_task_state ${sample_proc})"
	local rc=$?

	while [ $rc -eq 0 -a "${ps_state}" == "${goal_state}" -a \
		"$sample_state_count" -lt "$max_state_samples" ]; do
		sleep $sample_state_period
		ps_state="$(get_task_state ${sample_proc})"
		rc=$?
		if [ $rc -ne 0 ]; then
			result=${result:-$rc}
			tst_resm ${LIB_TEST_STATE} "Failed to read process state."
			break
		fi

		((sample_state_count++))
	done
	return $rc
}

function wait_until_frozen()
{
	wait_until_goal_state_or_timeout "FROZEN" || return $?
	assert_cgroup_freezer_state "FROZEN" "ERROR: failed to freeze cgroup"
	# TODO assert all tasks in cgroup are in 'D' or 'T' state
	# TODO assert that trying to add a task to cgroup results in EBUSY
	return $?
}

function issue_thaw_cmd()
{
	"${CG_FILE_WRITE}" "${THAW}" > freezer.state && return 0
	local rc=$?
	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "Failed to issue thaw command."
	return $rc
}

function issue_cont_as_thaw_cmd()
{
	local goal_state="T"
	local sample_state_count=1
	local ps_state="$(get_task_state ${task_pid})"
	local rc=$?

	if [ $rc -ne 0 ]; then
		return $rc
	fi

	while [ "${ps_state}" == "${goal_state}" ]; do
		kill -s SIGCONT $sample_proc
		sleep $sample_state_period
		ps_state="$(get_task_state ${task_pid})"
		rc=$?
		if [ $rc -ne 0 ]; then
			break
		fi

		((sample_state_count++))
		if [ "$sample_state_count" -ge "$max_state_samples" ]; then
			break
		fi
	done

	if [ "${ps_state}" != "${goal_state}" ]; then
		return 0
	fi

	result=${result:-$rc}
	tst_resm ${LIB_TEST_STATE} "Failed to issue continue (thaw) command (task state: \"${ps_state}\")."
	return $rc
}

function wait_until_thawed()
{
	wait_until_goal_state_or_timeout "THAWED" || return $?
	assert_cgroup_freezer_state "THAWED" "ERROR: Failed to thaw cgroup."
	return $?
}

function wait_until_freezing()
{
	wait_until_goal_state_or_timeout "FREEZING"
	# Time critical -- we race with the kernel as it freezes tasks in the
	# cgroup. So rather than assert "FREEZING" we just return
	return $?
}

function wait_until_sample_proc_stopped()
{
	wait_until_sample_proc_state_or_timeout 'T' || return $?
	assert_sample_proc_stopped
	return $?
}

function wait_until_sample_proc_not_stopped()
{
	wait_until_sample_proc_not_goal_state_or_timeout 'T' || return $?
	assert_sample_proc_not_stopped
	return $?
}
