#!/bin/sh
# Utility to enable background operations on an eMMC, slowly scan the blocks
# of the eMMC and periodically allow non-urgent BKOPS to run.

progname="${0##*/}"

# Default device if not specified
DEF_DEV="/dev/mmcblk0"

# Default scan interval if not specified (in days)
DEF_INTERVAL="28"	# Results in a loop every 85 seconds on Eredan

# Persistent location for saved data file. This allows us to restart the
# scan at the point that we left off before a controlled restart.
persistDir="/mnt/config/etc_persist"
[ -d "${persistDir}" ] || persistDir="/mnt/sysfs/system"

# Wait time if BKOPS was required
bkopsWait="5"

usageString="Usage: $progname [-i <intervalDays>] [-1] [devFile]

Scan the eMMC user partition over the specified number of days.

Options:
-i n	Interval for a full scan in days. The eMMC is slowly scanned
	over this interval.
	Default: ${DEF_INTERVAL} days

-1	Scan once from the beginning of the user partition as fast
	as possible.

-v	Be a bit more verbose (for debugging). Can be used multiple
	times to get more output.

devFile	Device to scan.
	Default: ${DEF_DEV}
"
usage()
{
	echo "$usageString" >&2
	exit 1
}
err()
{
	echo "$progname: $* " >&2
	exit 1
}

# Display the difference between the passed uptime value and current
# uptime value.
elapsedTime()
{
	local startTime="${1:-0}"
	local elapse=$(( $(cat /proc/uptime | cut -d. -f1) - ${startTime} ))
	local sec=$(( ${elapse} % 60 ))
	local min=$(( (${elapse} / 60) % 60 ))
	local hr=$(( (${elapse} / (60 * 60)) % 24 ))
	local day=$(( (${elapse} / (24 * 60 * 60)) ))

	printf "%03u %02u:%02u:%02u" ${day} ${hr} ${min} ${sec}
}

# Track how long the script has run before being terminated
startTime="$(cat /proc/uptime | cut -d. -f1)"
exitTime()
{
	# Save current scan LBA to persistent store
	[ -e "/tmp/${progname}.dat" ] && cp "/tmp/${progname}.dat" "${persistDir}/${progname}.dat"

	echo "$progname teminated after $(elapsedTime ${startTime})"
	exit 1
}

# Read a chunk of data from the specified eMMC device.
# Parameters:
# $1	device
# $2	block size
# $3	Number of blocks to read
# $4	Starting sector number.
readBlk()
{
	local dev="${1:-${devFile}}"
	local bkSz="${2:-4096}"
	local count="${3:+count=$3}"
	local skip="${4:+skip=$(( (${4} * 512) / ${bkSz} ))}"
	[ "$((verbose))" -gt "1" ] && echo "dd if=$dev of=/dev/null bs=${bkSz} ${count} ${skip}"
	dd if="$dev" of=/dev/null bs="${bkSz}" ${count} ${skip} 2> /dev/null
}

# Rearrange options and arguments
options=`getopt -o hi:v1 --long help -- "$@"` || err "Bad options"
eval set -- "$options"

verbose="0"
interval=""
once=""
while true ; do
	case "$1" in
	-h|--help) usage ;;
	-i)
		shift
		interval="${1}"
		[ -n "${interval}" ] || err "Need to specify an interval in days"
		echo "${interval}" | grep -Eq "[^0-9]" && err "Interval must be a decimal number: ${interval}"
		;;
	-1) once="y" ;;
	-v) verbose="$((verbose + 1))" ;;
	--) shift; break ;;
	esac
	shift
done

devFile="${1}"
shift

[ -z "${*}" ] || err "Unexpected options: ${*}"

: ${devFile:=${DEF_DEV}}
dev="${devFile##*/}"
if [ ! -b "${devFile}" ] ||
   [ ! -e "/sys/block/${dev}/size" ] ||
   [ ! -e "/sys/block/${dev}/device/erase_size" ] ; then
	err "Invalid eMMC device: ${devFile}"
fi

: ${interval:=${DEF_INTERVAL}}

# Size of the user and boot partitions in 512B sectors
userSec=$(cat /sys/block/${dev}/size)
bootSec=$(cat /sys/block/${dev}/${dev}boot0/size)

# Erase block size in bytes
eraseSz=$(cat /sys/block/${dev}/device/erase_size)

# Set pgSz and block count to transfer an eraseSz block.
# If we are only doing this once, we assume we are trying to refresh as
# fast as possible so we transfer eraseSz blocks otherwise we transfer
# multiple pages.
[ -n "${once}" ] && pgSz="${eraseSz}" || pgSz="4096"
pgCnt=$((${eraseSz} / ${pgSz}))

# Number of sectors in an eraseSz block
lbaInc=$((${eraseSz} / 512))

# Number of iterations to scan all of the user partition
maxScanLoops=$((${userSec}/${lbaInc}))

# Determine where to start scanning the user partition. We keep track of where
# we left off in a file in /tmp so that the script can be restarted if necessary.
# If the /tmp file is not available (say at boot up) we can look for a file in
# a persistent location ${persistDir}.  If neither file is available we will start
# scanning from a random location.
if [ ! -e "/tmp/${progname}.dat" ] ; then
	if [ -e "${persistDir}/${progname}.dat" ] ; then
		cp "${persistDir}/${progname}.dat" "/tmp/${progname}.dat"
	fi
fi
lba=""
if [ -e "/tmp/${progname}.dat" ] ; then
	lba=$(tail -n 1 "/tmp/${progname}.dat")

	# Punt data with non-decimal characters
	echo "${lba}" | grep -Eq "[^0-9]" && lba=""

	# Punt data without any decimal characters. Handles
	# blanks lines or empty file.
	echo "${lba}" | grep -Eq "[0-9]" || lba=""
fi
if [ -z "${lba}" ] ; then
	# Generate a 56-bit random number
	rn=$((0x$(dd bs=1 count=7 if=/dev/urandom 2>/dev/null | hexdump -e '"%02x"') ))
	# Generate a random LBA start position in the user partition in terms of
	# eraseSz chunks.
	lba=$(( (${rn} % ${maxScanLoops}) * ${lbaInc} ))
fi
[ -z "${once}" ] || lba="0"

# Compute the wait time between reading chunks of data.
scanWait=$(( (${interval} * 24 * 60 * 60) / ${maxScanLoops} ))
[ -z "${once}" ] || scanWait="0"

if [ "$((verbose))" -gt "0" ] ; then
	echo "Scaning device ${devFile}"
	echo "User partition: ${userSec} sectors, Erase size: ${eraseSz} bytes"
	echo "Starting at lba ${lba} for ${maxScanLoops} loops waiting ${scanWait} s between loops"
fi

# Enable bkops if necessary
status=$(emmcutil -nb ${devFile})
status="${status#*Enable background operations handshake*(}"
status="${status%%)*}"
[ "${#status}" -eq 4 ] || status="0"
[ "$(( 1 & ${status} ))" -eq "1" ] || err "BKOPS not enabled"

# Clean up/save progress in persistent dir on controlled termination
trap "exitTime" EXIT

# Do an initial scanWait to give SAOS a little time to finish up it's
# initialization and hopefully it's need to touch the disk.
[ -z "${once}" ] && sleep "${scanWait}"

while true; do
	scanStart=$(cat /proc/uptime | cut -d. -f1)

	i=0
	while test "${i}" -lt "${maxScanLoops}" ; do
		# Read an erase block worth of data
		readBlk "${devFile}" "${pgSz}" "${pgCnt}" "${lba}"

		# Start any outstanding BKOPS and see if there are
		# urgent or critical outstanding BKOPS left. Normally the
		# kernel will take care of those synchronously so we
		# really shouldn't see them here.
		status=$(emmcutil -b ${devFile})
		status="${status#*Background operations status*(}"
		status="${status%%)*}"
		[ "${#status}" -eq 4 ] || status="Unknown"
		case "${status}" in
		0x00)	;;
		*)	[ "$((verbose))" -gt "0" ] && \
				echo "BKOPS_STATUS '${status}' at lba ${lba}"
			;;
		esac
		case "${status}" in
		0x00|0x01)
			lba=$(( ${lba} + ${lbaInc} ))
			[ "${lba}" -lt "${userSec}" ] || lba="0"
			echo "${lba}" > "/tmp/$progname.dat"
			[ "${status}" = "0x00" ] || emmcutil -s ${devFile} > /dev/null
			sleep "${scanWait}"
			;;
		0x02|0x03)
			emmcutil -s ${devFile} > /dev/null
			sleep "$bkopsWait"
			;;
		*)
			echo "$progname: Cannot detemine BKOP_STATUS"
			sleep "$bkopsWait"
			;;
		esac
		i="$(($i + 1))"
	done

	# Read the boot partitions
	for b in boot0 boot1 ; do
		# Make sure the block device exists
		[ -b "${devFile}${b}" ] || continue

		# Quick scan of the boot partition. It is small.
		i=0
		while test "$((i))" -lt "$((bootSec))" ; do
			readBlk "${devFile}${b}" "${pgSz}" "${pgCnt}" "${i}"
			[ -z "${once}" ] || sleep 1
			i="$(($i + ${lbaInc} ))"
		done
	done

	echo "Partition scan complete after $(elapsedTime ${scanStart})"

	[ -z "${once}" ] || break;
done

exit


