#
# SPDX-License-Identifier: BSD-2-Clause
#
# Copyright (c) 2023 Klara, Inc.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#

# sysexits(3)
: ${EX_USAGE:=64}
: ${EX_UNAVAILABLE:=69}
: ${EX_CANTCREAT:=73}
: ${EX_TEMPFAIL:=75}

waitlock()
{
	local cur lockfile tmo

	lockfile="$1"

	cur=0
	tmo=20

	while [ "$cur" -lt "$tmo" -a ! -f "$lockfile" ]; do
		sleep 0.1
		cur=$((cur + 1))
	done

	atf_check_not_equal "$cur" "$tmo"
}


atf_test_case badargs
badargs_body()
{
	atf_check -s exit:${EX_USAGE} -e not-empty lockf
	atf_check -s exit:${EX_USAGE} -e not-empty lockf "testlock"
}

atf_test_case basic
basic_body()
{
	# Something innocent so that it does eventually go away without our
	# intervention.
	lockf "testlock" sleep 10 &
	lpid=$!

	# Make sure that the lock exists...
	while ! test -e "testlock"; do
		sleep 0.1
	done

	# Attempt both verbose and silent re-lock
	atf_check -s exit:${EX_TEMPFAIL} -e not-empty \
	    lockf -t 0 "testlock" sleep 0
	atf_check -s exit:${EX_TEMPFAIL} -e empty \
	    lockf -t 0 -s "testlock" sleep 0

	# Make sure it cleans up after the initial sleep 10 is over.
	wait "$lpid"
	atf_check test ! -e "testlock"
}

atf_test_case bubble_error
bubble_error_body()
{
	# Ensure that lockf bubbles up the error as expected.
	atf_check -s exit:9 lockf testlock sh -c 'exit 9'
}

atf_test_case fdlock
fdlock_body()
{
	# First, make sure we don't get a false positive -- existing uses with
	# numeric filenames shouldn't switch to being fdlocks automatically.
	atf_check lockf -k "9" sleep 0
	atf_check test -e "9"
	rm "9"

	subexit_lockfail=1
	subexit_created=2
	subexit_lockok=3
	subexit_concurrent=4
	(
		lockf -s -t 0 9
		if [ $? -ne 0 ]; then
			exit "$subexit_lockfail"
		fi

		if [ -e "9" ]; then
			exit "$subexit_created"
		fi
	) 9> "testlock1"
	rc=$?

	atf_check test "$rc" -eq 0

	sub_delay=5

	# But is it actually locking?  Child 1 will acquire the lock and then
	# signal that it's ok for the second child to try.  The second child
	# will try to acquire the lock and fail immediately, signal that it
	# tried, then try again with an indefinite timeout.  On that one, we'll
	# just check how long we ended up waiting -- it should be at least
	# $sub_delay.
	(
		lockf -s -t 0 /dev/fd/9
		if [ $? -ne 0 ]; then
			exit "$subexit_lockfail"
		fi

		# Signal
		touch ".lock_acquired"

		while [ ! -e ".lock_attempted" ]; do
			sleep 0.5
		done

		sleep "$sub_delay"

		if [ -e ".lock_acquired_again" ]; then
			exit "$subexit_concurrent"
		fi
	) 9> "testlock2" &
	lpid1=$!

	(
		while [ ! -e ".lock_acquired" ]; do
			sleep 0.5
		done

		# Got the signal, try
		lockf -s -t 0 9
		if [ $? -ne "${EX_TEMPFAIL}" ]; then
			exit "$subexit_lockok"
		fi

		touch ".lock_attempted"
		start=$(date +"%s")
		lockf -s 9
		touch ".lock_acquired_again"
		now=$(date +"%s")
		elapsed=$((now - start))

		if [ "$elapsed" -lt "$sub_delay" ]; then
			exit "$subexit_concurrent"
		fi
	) 9> "testlock2" &
	lpid2=$!

	wait "$lpid1"
	status1=$?

	wait "$lpid2"
	status2=$?

	atf_check test "$status1" -eq 0
	atf_check test "$status2" -eq 0
}

atf_test_case keep
keep_body()
{
	lockf -k "testlock" sleep 10 &
	lpid=$!

	# Make sure that the lock exists now...
	while ! test -e "testlock"; do
		sleep 0.5
	done

	kill "$lpid"
	wait "$lpid"

	# And it still exits after the lock has been relinquished.
	atf_check test -e "testlock"
}

atf_test_case needfile
needfile_body()
{
	# Hopefully the clock doesn't jump.
	start=$(date +"%s")

	# Should fail if the lockfile does not yet exist.
	atf_check -s exit:"${EX_UNAVAILABLE}" lockf -sn "testlock" sleep 30

	# It's hard to guess how quickly we should have finished that; one would
	# hope that it exits fast, but to be safe we specified a sleep 30 under
	# lock so that we have a good margin below that duration that we can
	# safely test to make sure we didn't actually execute the program, more
	# or less.
	now=$(date +"%s")
	tpass=$((now - start))
	atf_check test "$tpass" -lt 10
}

atf_test_case termchild
termchild_body()
{
	lockf -kp testlock sleep 30 &
	lpid=$!

	waitlock testlock

	atf_check -o file:testlock pgrep -F testlock

	start=$(date +"%s")
	atf_check kill -TERM "$lpid"
	wait "$lpid"
	end=$(date +"%s")
	elapsed=$((end - start))

	if [ "$elapsed" -gt 5 ]; then
		atf_fail "lockf seems to have dodged the SIGTERM ($elapsed passed)"
	fi

	# We didn't start lockf with -T this time, so the process should not
	# have been terminated.
	atf_check -o file:testlock pgrep -F testlock

	lockf -kpT testlock sleep 30 &
	lpid=$!

	waitlock testlock

	atf_check -o file:testlock pgrep -F testlock

	start=$(date +"%s")
	atf_check kill -TERM "$lpid"
	wait "$lpid"
	end=$(date +"%s")
	elapsed=$((end - start))

	if [ "$elapsed" -gt 5 ]; then
		atf_fail "lockf -T seems to have dodged the SIGTERM ($elapsed passed)"
	fi

	# This time, it should have terminated (notably much earlier than our
	# 30 second timeout).
	atf_check -o empty -e not-empty -s not-exit:0 pgrep -F testlock
}

atf_test_case timeout
timeout_body()
{
	lockf "testlock" sleep 30 &
	lpid=$!

	while ! test -e "testlock"; do
		sleep 0.5
	done

	start=$(date +"%s")
	timeout=2
	atf_check -s exit:${EX_TEMPFAIL} lockf -st "$timeout" "testlock" sleep 0

	# We should have taken no less than our timeout, at least.
	now=$(date +"%s")
	tpass=$((now - start))
	atf_check test "$tpass" -ge "$timeout"

	kill "$lpid"
	wait "$lpid" || true
}

atf_test_case writepid
writepid_body()
{
	lockf -p "testlock" sleep 10 &
	lpid=$!

	waitlock "testlock"

	atf_check test -s testlock
	atf_check -o file:testlock pgrep -F testlock
	atf_check -o file:testlock pgrep -F testlock -fx "sleep 10"
	atf_check pkill -TERM -F testlock

	wait

	atf_check test ! -f testlock
}

atf_test_case writepid_keep
writepid_keep_body()
{
	# Check that we'll clobber any existing contents (a pid, usually)
	# once we acquire the lock.
	jot -b A -s "" 64 > testlock
	atf_check lockf -kp testlock sleep 0
	atf_check -o not-match:"A" cat testlock
}

atf_test_case wrlock
wrlock_head()
{
	atf_set "require.user" "unprivileged"
}
wrlock_body()
{
	touch "testlock"
	chmod -w "testlock"

	# Demonstrate that we can lock the file normally, but -w fails if we
	# can't write.
	atf_check lockf -kt 0 "testlock" sleep 0
	atf_check -s exit:${EX_CANTCREAT} -e not-empty \
	    lockf -wt 0 "testlock" sleep 0
}

atf_init_test_cases()
{
	atf_add_test_case badargs
	atf_add_test_case basic
	atf_add_test_case bubble_error
	atf_add_test_case fdlock
	atf_add_test_case keep
	atf_add_test_case needfile
	atf_add_test_case termchild
	atf_add_test_case timeout
	atf_add_test_case writepid
	atf_add_test_case writepid_keep
	atf_add_test_case wrlock
}
