#!/bin/sh
# Copyright 2023 Helmut Grohne <helmut@subdivi.de>
# SPDX-License-Identifier: MIT

: <<'POD2MAN'
=head1 NAME

debvm-waitssh - Wait for a ssh server to be reachable

=head1 SYNOPSIS

B<debvm-waitssh> [B<-q>] [B<-t> I<timeout>] [I<hostname>:]I<port>

=head1 DESCRIPTION

B<debvm-waitssh> can be used to wait for a virtual machine with exposed ssh port to be reachable on that port.
If no hostname is given, B<127.0.0.1> is assumed. No authentication is attempted by B<debvm-waitssh>, so neither
a username nor a key have to be supplied.

=head1 OPTIONS

=over 8

=item B<-t> I<timeout>, B<--timeout>=I<timeout>

Set the maximum duration for waiting in seconds.
Defaults to one minute.

=item B<-q>, B<--quiet>

Be quiet.
Do not output a message when the timeout has been reached without success.

=back

=head1 EXIT VALUES

=over 8

=item B<0>

The server is reachable.

=item B<1>

A timeout was reached before the server answered.

=item B<2>

Usage error.

=back

=head1 SEE ALSO

    debvm-run(1)

=cut
POD2MAN

set -u

TOTALTIMEOUT=60
SCANTIMEOUT=10
SCANDELAY=1
VERBOSITY=1

nth_arg() {
	shift "$1"
	printf "%s" "$1"
}

die() {
	echo "$*" >&2
	exit 2
}
usage() {
	die "usage: $0 [-q] [-t <timeout>] [<host>:]<port>"
}
usage_error() {
	echo "error: $*" >&2
	usage
}

opt_help() {
	# shellcheck disable=SC2317 # not dead, called as "opt_$OPTARG"
	usage
}
opt_quiet() {
	VERBOSITY=0
}
opt_timeout() {
	TOTALTIMEOUT=$1
}

while getopts :qt:-: OPTCHAR; do
	case "$OPTCHAR" in
		q)	opt_quiet		;;
		t)	opt_timeout "$OPTARG"	;;
		-)
			case "$OPTARG" in
				help|quiet)
					"opt_$OPTARG"
				;;
				timeout)
					test "$OPTIND" -gt "$#" && usage_error "missing argument for --$OPTARG"
					"opt_$OPTARG" "$(nth_arg "$OPTIND" "$@")"
					OPTIND=$((OPTIND+1))
				;;
				timeout=*)
					"opt_${OPTARG%%=*}" "${OPTARG#*=}"
				;;
				*)
					usage_error "unrecognized option --$OPTARG"
				;;
			esac
		;;
		:)
			usage_error "missing argument for -$OPTARG"
		;;
		'?')
			usage_error "unrecognized option -$OPTARG"
		;;
		*)
			die "internal error while parsing command options, please report a bug"
		;;
	esac
done
shift "$((OPTIND - 1))"

test "$#" = 1 || usage

case "$1" in
	"")
		usage
	;;
	*:*)
		HOST=${1%:*}
		PORT=${1##*:}
	;;
	*)
		HOST=127.0.0.1
		PORT=$1
	;;
esac

case "$HOST" in *@*)
	die "$0: hostname '$HOST' must not contain the '@' character. No username is required."
;; esac

# Guard against strings containing anything but digits, strings starting with
# zero and empty strings as the port number.
#
# In some locales, [0-9] can match other kinds of digits, see
# https://unix.stackexchange.com/a/414230/46985.
case "$PORT" in *[!0123456789]*|0?*|""|??????*)
	die "$0: port '$PORT' is not an integer between 1 and 65535"
;; esac
if test "$PORT" -lt 1 -o "$PORT" -gt 65535; then
	die "$0: port '$PORT' is not an integer between 1 and 65535"
fi

now=$(date +%s)
deadline=$((now + TOTALTIMEOUT))
while test "$now" -lt "$deadline"; do
	start=$now
	ssh-keyscan -t rsa -T "$SCANTIMEOUT" -p "$PORT" "$HOST" >/dev/null 2>&1 && exit 0
	now=$(date +%s)
	if test "$((now - start))" -lt "$SCANTIMEOUT"; then
		sleep "$SCANDELAY"
		now=$(date +%s)
	fi
done
if [ "$VERBOSITY" -ge 1 ]; then
	echo "$0: timeout reached trying to contact $HOST:$PORT after waiting $TOTALTIMEOUT seconds." >&2
fi
exit 1