#!/bin/sh
# SPDX-License-Identifier: MIT

error() {
	printf "Error: %s\n" "$@" >&2
	logger -t swupdate-url "auto-update error: $*"
	exit 1
}

warning() {
	printf "Warning: %s\n" "$@" >&2
	logger -t swupdate-url "auto-update warning: $*"
}

info() {
	[ -n "$VERBOSE" ] && printf "%s\n" "$@"
	logger -t swupdate-url "$*"
}

print_help() {
	echo "Usage: $0 [options]"
	echo ""
	echo "Options:"
	echo "  -u, --urlfile <urlfile>    file with urls to test for update."
	echo "                             Defaults to /etc/swupdate.watch"
	echo "  -s, --schedule <schedule>  schedule_ts argument to specify next run start interval,"
	echo "                             e.g. '2am tomorrow'."
	echo "  -r, --rdelay <max delay>   random number of seconds up to which we can sleep. (default 0)"
	echo "  -O, --once                 run once first before applying schedule (or exit if none)"
	echo "  --state <file>             remember when last run happened to respect schedule accross reboots"
	echo "                             (default unset)"
	echo "  --update-statefile         Update state with schedule"
	echo "  -v, --verbose              verbose"
	echo "  -h, --help                 this help"
	echo ""
	echo "Setting either -s (schedule) or -O (run on startup) is required"

	exit "$1"
}

wait_for_net() {
	# we only check if there is a route defined (e.g. we got an IP somehow)
	# as that is usually enough: tcp will do a few retries if required.
	timeout 180s sh -c "while ! ip route get 1.1.1.1 > /dev/null 2>&1; do
		sleep 5;
	done"
}

update() {
	local url _ TMPDIR

	# skip non-existing conf file
	[ -r "$URLFILE" ] || return 0

	while read -r url _; do
		# skip urls that don't start with http, e.g. comments
		[ "${url#http}" = "$url" ] && continue

		if ! wait_for_net; then
			warning "Network was not avaiable for update, skipping this attempt"
			return
		fi

		logger -t swupdate "Trying to update $url"
		TMPDIR=$(mktemp -d /var/tmp/swupdate-url.XXXXXX) \
			|| error "Could not create temp dir"

		TMPDIR="$TMPDIR" swupdate -d "-u $url"

		find "$TMPDIR" -xdev -delete 2>/dev/null

		# update was successful, stop here
		[ -e /run/swupdate_rebooting ] && exit
	done < "$URLFILE"

}

parse_args() {
	while [ "$#" -ge 1 ]; do
		case "$1" in
		-u|--urlfile)
			[ "$#" -ge 2 ] || print_help 1
			URLFILE="$2"
			shift 2
			;;
		-s|--schedule)
			[ "$#" -ge 2 ] || print_help 1
			SCHEDULE="$2"
			shift 2
			;;
		-r|--rdelay)
			[ "$#" -ge 2 ] || print_help 1
			RDELAY="$2"
			shift 2
			;;
		--state)
			[ "$#" -ge 2 ] || print_help 1
			STATE="$2"
			shift 2
			;;
		-O|--once)
			ONCE=1
			shift
			;;
		--update-statefile)
			UPDATESTATE=1
			shift
			;;
		-v|--verbose)
			VERBOSE=1
			shift
			;;
		-h|--help)
			print_help 0
			;;
		*)
			print_help 1
			;;
		esac
	done
}

next_schedule() {
	NOW=$(date +%s)
	TARGET=$(schedule_ts "$SCHEDULE")
	[ -n "$TARGET" ] || error "schedule $SCHEDULE is invalid: $(schedule_ts "$SCHEDULE" 2>&1)"
	if [ "$TARGET" -le "$NOW" ]; then
		# try to add +1day if target is in recent past; this happens when
		# schedule contains only an hour target without 'tomorrow'
		local new_target=$((TARGET + 3600 * 24))
		if [ "$new_target" -le "$NOW" ]; then
			error "schedule $SCHEDULE is in the past? Would be $(date -d @"$TARGET") (now: $(date -d @"$NOW"))"
		fi
		TARGET="$new_target"
	fi
	[ "$RDELAY" -gt 0 ] && TARGET=$((TARGET + (RANDOM % RDELAY)))
	[ -z "$STATE" ] || echo "$TARGET" > "$STATE" \
		|| warning "Could not write next update time to $STATE"
}

create_statedir() {
	[ -z "$STATE" ] && return
	state_parent="$(dirname "$STATE")"
	[ -d "$state_parent" ] || mkdir -p "$state_parent" \
		|| warning "Could not create state parent directory $state_parent"
}

read_state() {
	local new_target="" state_parent
	[ -z "$STATE" ] && return
	if [ -e "$STATE" ]; then
		new_target=$(cat "$STATE" 2>/dev/null)
	fi
	if [ -z "$new_target" ] && [ -z "$ONCE" ]; then
		# first time running, check immediately once for good measure
		new_target=0
	fi
	if [ "$new_target" -gt $((TARGET + 2 * RDELAY)) ]; then
		info "Skipping state too far in the future $(date -d @"$new_target")" "Possible clock skew or config change"
		echo "$TARGET" > "$STATE" \
			|| warning "Could not write next update time to $STATE"
	else
		TARGET="$new_target"
	fi
}

main() {
	local URLFILE=/etc/swupdate.watch SCHEDULE="" RDELAY="0" ONCE=""
	local STATE=""
	local NOW TARGET=""

	parse_args "$@"

	create_statedir

	if [ -n "$UPDATESTATE" ]; then
		next_schedule
		echo "Updated state to $(date -d @"$TARGET")"
		exit
	fi

	if [ -n "$ONCE" ]; then
		update
	fi

	if [ -z "$SCHEDULE" ]; then
		[ -n "$ONCE" ] || print_help 1
		exit
	fi

	# Get schedule once without state to detect bogus values
	STATE="" next_schedule
	read_state

	while true; do
		if [ "$TARGET" -gt "$NOW" ]; then
			info "Next run on $(date -d @"$TARGET"), sleeping $((TARGET - NOW))s"
			sleep $((TARGET - NOW))
		else
			info "Target date in the past $(date -d @"$TARGET"): running now"
		fi
		update
		next_schedule
	done
}

main "$@"
