#!/bin/sh -x
##
## @package   software-loader
## @file      software-loader.sh
## @brief     Script file for applying software upgrades
##			  to the system
##
## @author    Howard Dowthwaite
## @author    Subhasish Ghosh
## @copyright Copyright 2019-2020 Hanover Displays Limited.
## @license   This program is the confidential and proprietary product of
##            Hanover Displays Limited. Any unauthorised use, reproduction or
##            transfer of this program is strictly prohibited. (Subject to 
##            limited distribution and restricted disclosure only.) All 
##            rights reserved.                                              
##

# Clean up on exit
cleanup() {
	echo "Cleaning up temporary files..."
	retval=$?
	trap '' EXIT
	if [ -n "${DELTA_EXTRACT_DIR}" ] && [ "${DELTA_EXTRACT_DIR}" != "/" ] && [ -d "${DELTA_EXTRACT_DIR}" ]
	then
		echo "Cleaning delta extract folder: ${DELTA_EXTRACT_DIR}"
		for item in "${DELTA_EXTRACT_DIR}"/* "${DELTA_EXTRACT_DIR}"/.[!.]* "${DELTA_EXTRACT_DIR}"/..?*
		do
			[ -e "${item}" ] || continue
			rm -rf "${item}"
		done
	fi

	if [ -n "${SRC_FOLDER}" ] && [ "${SRC_FOLDER}" != "/" ] && [ "${SRC_FOLDER}" != "${DELTA_EXTRACT_DIR}" ]
	then
		rm -rf "${SRC_FOLDER}"/*
	else
		echo "Skipping cleanup of SRC_FOLDER (empty or unsafe)"
	fi
	sync
	[ $retval -eq 0 ] || echo "$0: Update failed with exitcode: $retval"
}

# Default delta extract directory (must be set before trap can run cleanup).
DELTA_EXTRACT_DIR=${DELTA_EXTRACT_DIR:-/mnt/hos-data/delta}

# trap exit to ensure cleanup
trap cleanup EXIT

# Initialise constants
SRC_ARG=${1:-}
SRC_FOLDER=${SRC_ARG}
HOS_DELTA=${HOS_DELTA:-/usr/bin/hos-delta}
SETBOOT=${SETBOOT:-}

# If a delta archive is found inside the passed folder, ALWAYS extract its
# contents into ${DELTA_EXTRACT_DIR} after clearing that folder.
archive_file=
if [ -d "${SRC_FOLDER}" ]
then
	archive_file=$(ls -1t "${SRC_FOLDER}"/*.tar.gz "${SRC_FOLDER}"/*.tgz "${SRC_FOLDER}"/*.zip 2>/dev/null | head -n 1)
fi

if [ -n "${archive_file}" ] && [ -f "${archive_file}" ]
then
	echo "Cleaning delta staging folder before extracting: ${DELTA_EXTRACT_DIR}"
	mkdir -p "${DELTA_EXTRACT_DIR}" || exit 71
	for item in "${DELTA_EXTRACT_DIR}"/* "${DELTA_EXTRACT_DIR}"/.[!.]* "${DELTA_EXTRACT_DIR}"/..?*
	do
		[ -e "${item}" ] || continue
		rm -rf "${item}"
	done

	echo "Extracting delta archive: ${archive_file}"
	case "${archive_file}" in
		*.zip)
			command -v unzip >/dev/null 2>&1 || { echo "Missing application: unzip"; exit 70;}
			unzip -o "${archive_file}" -d "${DELTA_EXTRACT_DIR}" || exit $?
			;;
		*)
			tar -xzf "${archive_file}" -C "${DELTA_EXTRACT_DIR}" || exit $?
			;;
	esac
	touch "${DELTA_EXTRACT_DIR}/package-ready" || exit $?
	SRC_FOLDER="${DELTA_EXTRACT_DIR}"
fi


# Determine support for querying boot slot. Prefer setboot, fall back to boot-slot-manager.
if [ -z "$SETBOOT" ]
then
	if [ -x /usr/bin/setboot ]
	then
		SETBOOT=/usr/bin/setboot
	elif [ -x /usr/sbin/boot-slot-manager ]
	then
		SETBOOT=/usr/sbin/boot-slot-manager
	else
		echo "Missing application: /usr/bin/setboot or /usr/sbin/boot-slot-manager"
		exit 70
	fi
fi

# Determine the currently selected boot slot by parsing the output of the setboot query command.
get_boot_selected() {
	if [ "$SETBOOT" = "/usr/bin/setboot" ]
	then
		echo "$($SETBOOT query 2>/dev/null | tr 'a-z' 'A-Z')"
	else
		echo "$($SETBOOT query 2>/dev/null | grep -i '^[[:space:]]*Current slot:' | head -n 1 | tr 'a-z' 'A-Z' | grep -Eo '[AB]' | head -n 1)"
	fi
	return 0
}

# Switch the boot slot using the setboot application.
switch_boot_to_slot() {
	slot=${1:-}
	if [ "$SETBOOT" = "/usr/bin/setboot" ]
	then
		$SETBOOT switch
	else
		$SETBOOT switch "$slot"
	fi
}

PACKAGE_READY_FLAG=${PACKAGE_READY_FLAG:-"${SRC_FOLDER}"/package-ready}
A_INACTIVE_MOUNT_POINT=${A_INACTIVE_MOUNT_POINT:-/mnt/hos-rootA/}
B_INACTIVE_MOUNT_POINT=${B_INACTIVE_MOUNT_POINT:-/mnt/hos-rootB/}

# Test constants (exit with sysexits)
[ -d "$SRC_FOLDER" ] || { echo "Usage: $0 <folder path>"; exit 64;}
[ -d "$A_INACTIVE_MOUNT_POINT" ] || { echo "Missing directory: $A_INACTIVE_MOUNT_POINT"; exit 70;}
[ -d "$B_INACTIVE_MOUNT_POINT" ] || { echo "Missing directory: $B_INACTIVE_MOUNT_POINT"; exit 70;}
[ -f "$HOS_DELTA" ] || { echo "Missing application: $HOS_DELTA"; exit 70;}
[ -x "$HOS_DELTA" ] || { echo "Invalid permissions: $HOS_DELTA"; exit 77;}
[ -f "$SETBOOT" ] || { echo "Missing application: $SETBOOT"; exit 70;}
[ -x "$SETBOOT" ] || { echo "Invalid permissions: $SETBOOT"; exit 77;}
[ -f "$PACKAGE_READY_FLAG" ] || { echo "Missing file: $PACKAGE_READY_FLAG"; exit 70;}

# Determine if either inactive root partition is mounted based on the device number of the mount
# point differing from that of the root directory.
#
# There should always be exactly ONE mounted.

if [ "$(stat -c '%d' "$A_INACTIVE_MOUNT_POINT")" != "$(stat -c '%d' "/")" ]
then
	a_inactive_mounted=true
	target_mount="$A_INACTIVE_MOUNT_POINT"
	active_mount="$B_INACTIVE_MOUNT_POINT"
else
	a_inactive_mounted=false
fi

if [ "$(stat -c '%d' "$B_INACTIVE_MOUNT_POINT")" != "$(stat -c '%d' "/")" ]
then
	b_inactive_mounted=true
	target_mount="$B_INACTIVE_MOUNT_POINT"
	active_mount="$A_INACTIVE_MOUNT_POINT"
else
	b_inactive_mounted=false
fi

if [ "$a_inactive_mounted" = "true" -a "$b_inactive_mounted" = "true" ] || [ "$a_inactive_mounted" = "false" -a "$b_inactive_mounted" = "false" ]
then
	echo "Unable to determine inactive root filesystem mount point!" 1>&2
	exit 71 # EX_OSERR
fi

# Determine which root partition is configured to be booted.
boot_selected=$(get_boot_selected)

if [ "$boot_selected" != "A" -a "$boot_selected" != "B" ]
then
	echo "Cannot identify selected boot partition"
	exit 71 # EX_OSERR
fi

# Determine the slot we just upgraded (the currently-mounted inactive rootfs).
if [ "$a_inactive_mounted" = "true" ]
then
	inactive_slot=a
else
	inactive_slot=b
fi

# Check we aren't about to upgrade the selected boot partition.
if [ "$boot_selected" = "A" -a "$a_inactive_mounted" = "true" ] || [ "$boot_selected" = "B" -a "$b_inactive_mounted" = "true" ]
then
	# TODO: Switch boot flag and replace already performed upgrade?
	echo "Mounted root device doesn't match selected boot device" 1>&2
	echo "An upgrade may have already been performed since booting, aborting" 1>&2
	exit 75 # EX_TEMPFAIL
fi
	# Remount inactive root fs read-write.
	mount -o remount,rw "$target_mount"
	[ $? -eq 0 ] || exit $?

	if [ "$(basename "${SRC_FOLDER}")" = "delta" ]; then
		$HOS_DELTA apply $SRC_FOLDER "$active_mount" "$target_mount"
		retval=$?
		if [ $retval -ne 0 ]
		then
			# Remount inactive root fs read-only
			mount -o remount,ro "$target_mount"
			exit $retval
		fi
	else
	    (
		set -e
		mount -t tmpfs tmpfs "$target_mount/var/volatile/"

		mkdir "$target_mount/var/volatile/log/"
		mkdir "$target_mount/var/volatile/tmp/"
		chmod 1777 "$target_mount/var/volatile/tmp/"

		# TODO: Do we need proc/sys/etc within the chroot for postinst scripts?

		# Installing updates from a local repo (i.e. USB drive), bind mount it into the
		# target filesystem so it can be accessed within the chroot.
		#
		# The path must be canonicalised (e.g. symlinks resolved) to ensure we bind mount the real
		# directory, otherwise we will (for example) try bind mounting a directory under "/media/"
		# which is actually a symlink to a tmpfs and the mount will end up outside of the chroot.

		SRC_FOLDER=$(realpath "$SRC_FOLDER")

		mkdir -p "$target_mount/$SRC_FOLDER"
		mount -o bind "$SRC_FOLDER" "$target_mount/$SRC_FOLDER"

		# Recreate opkg.conf in target with only the upgrade repo in it

		grep -Ev "^(src|src/gz) " "/etc/opkg/opkg.conf" > "$target_mount/etc/opkg/opkg.conf"

		for d in "${SRC_FOLDER}"/*/ ; do
			echo "src/gz $(basename "${d}")" file://"${d}" >> "$target_mount/etc/opkg/opkg.conf"
		done

		do_opkg() {
			env - PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin" chroot "$target_mount" /usr/bin/opkg "$@"
		}

		# Perform the upgrade
		do_opkg clean
		do_opkg update
		do_opkg upgrade --autoremove

		umount "$target_mount/$SRC_FOLDER"
		umount "$target_mount/var/volatile/"
	    )
	    retval=$?
		if [ $retval -ne 0 ]
		then
			# Upgrade process failed. Try to unmount any filesystems that might be leftover.
			umount "$target_mount/$SRC_FOLDER" > /dev/null 2>&1
			umount "$target_mount/var/volatile/" > /dev/null 2>&1
			# Remount inactive root fs read-only
			mount -o remount,ro "$target_mount"
			exit $retval
		fi
	fi

	# Finalize the inactive rootfs
	if [ $? -eq 0 ]; then
		# Flush filesystem
		sync -f "$target_mount"

		# Remount inactive root filesystem read-only
		mount -o remount,ro "$target_mount" || exit $?

		# Switch boot partition
		switch_boot_to_slot "$inactive_slot"
		exit $?
	fi

exit $?
