#!/bin/dash
# drive icon display daemon
# Copyright (C) James Budiono 2013, 2015, 2017, 2018, 2020
# License: GNU GPL Version 3 or later
#
# Version 1 - kick-start drive icon display and then quit
# Version 2 - this one runs as a daemon and manages the drive icons
# Version 3 - rox icons now fully managed here, not by udev-handler anymore
# Version 4 - single instance for same user on same $DISPLAY
# Version 5 - add support for mountmon
# Version 6 - re-written to unify icon drawing - turn into multicall
#

TEXTDOMAIN=fatdog
. gettext.sh

### configurations
. $BOOTSTATE_PATH # AUFS_ROOT

# the following must be identical with those in udev-handler
UDEV_DRIVE_ICON_ROOT=/tmp/udev-fatdog-drive-icon
UDEV_DRIVE_ICON_EVENTS=$UDEV_DRIVE_ICON_ROOT/events
UDEV_DRIVE_ICON_DEVICES=$UDEV_DRIVE_ICON_ROOT/devices
UDEV_EVENT_COUNTER=$UDEV_DRIVE_ICON_ROOT/counter
COUNTER_MAX=100  # max number of queued events

UDEV_MAX_WAIT=30 # in seconds, wait time for udev-handler to get ready
PINBOARD_FILE=$HOME/.config/rox.sourceforge.net/ROX-Filer/PuppyPin
DRIVE_ICON_ROOT=/tmp/fatdog-drive-icons.$USER.$XSESSION_ID
FIFO_FILE=$DRIVE_ICON_ROOT/fifo

grep -q sandbox /etc/shinit && IN_SANDBOX=1
DEBUG_FILE=$DRIVE_ICON_ROOT/log
DEBUG=

# get the rox functions
. /usr/sbin/fatdog-drive-icon-roxlib.sh # create_rox_app, rox_add_icon, rox_remove_icon, get_free_coord
. $EVENTMANAGER_CONFIG	# override default settings in get_free_coord, EXCLUDED_DRIVES

# output $@ to debug file
log_debug() {
	[ "$DEBUG" ] && echo "$@" >> $DEBUG_FILE
}

################# helpers ##################

# $1-devname (with /dev prefix)
get_mount_point() {
	local dmdev
	dmdev=$(ls -l /dev/mapper/* 2> /dev/null | awk -v dmdev=${1##*-} '$6 == dmdev {print $10; exit; }')
	[ "$dmdev" ] || dmdev=$(ls -l /dev/mapper/* 2> /dev/null | awk -v dmdev="/${1##*/}$" '$NF ~ dmdev { print $(NF-2); exit; }')
	awk -v dev=$1 -v dmdev=$dmdev '$1 == dev || $1 == dmdev { print $2; exit; }' /proc/mounts
}

# $1-devname (without /dev prefix)
get_crypt_point() {
	local dmdev
	dmdev=$(find /sys/devices/virtual/block/dm-*/slaves -type l -name "$1" 2>/dev/null | grep -wo 'dm-[0-9]\+' | grep -vw "$1")
	[ "$dmdev" ] && cat /sys/devices/virtual/block/$dmdev/dm/name
}

# $1-devname (without /dev)
get_drive_state() {
	local mountpoint
	mountpoint=$(get_mount_point /dev/$1)
	case $mountpoint in
		"")          [ "$(get_crypt_point $1)" = "" ] && echo unmounted || echo mounted;;
		$AUFS_ROOT*) echo system ;;
		*)           echo mounted ;;
	esac
}


############### action handlers #############

# $1 dev (without /dev), EXCLUDED_DRIVES
action_add() {
	local state="" p pp ppp
	if [ -e $UDEV_DRIVE_ICON_DEVICES/$1 ]; then
		. $UDEV_DRIVE_ICON_DEVICES/$1 # DRIVENAME, DEVNAME, DRIVE_TYPE, FS_TYPE, SUMMARY, VOLABEL
		# if a drive is excluded either by device name or by label name, don't show it
		if [ "$EXCLUDED_DRIVES" ]; then
			p="|$EXCLUDED_DRIVES|" pp="|$1|" ppp="|$VOLABEL|"
			case "$p" in *"$pp"*|*"$ppp"*) return ;; esac
		fi
		mkdir -p "$DRIVE_ICON_ROOT/$DRIVENAME"
		state=$(get_drive_state $1)
		create_rox_app "$DRIVE_ICON_ROOT/$DRIVENAME" "$DRIVE_TYPE" "$DEVNAME" "$SUMMARY" "$state" "$FS_TYPE"
		get_free_coord $PINBOARD_FILE $ICON_ALIGNMENT # returnx COORD_X, COORD_Y
		rox_add_icon "$DRIVE_ICON_ROOT/$DRIVENAME" "$COORD_X" "$COORD_Y" "$(get_icon_label $DRIVENAME "$VOLABEL")" "click $DEVNAME"
	fi
}

# $1 dev (without /dev)
action_remove() {
	rox_remove_icon $DRIVE_ICON_ROOT/$1
	rm -rf $DRIVE_ICON_ROOT/$1
}

# $1 dev (without /dev)
action_redraw() {
	action_remove "$1"
	action_add "$1"
}

# no parameters
action_redraw_all() {
	# remove icons
	sed "\_${DRIVE_ICON_ROOT}_ !d; s_.*${DRIVE_ICON_ROOT}_${DRIVE_ICON_ROOT}_; s_</icon>__" $PINBOARD_FILE |
	while read -r p; do
		rox_remove_icon $p
		rm -rf $p
	done

	# add icons
	ls -v $UDEV_DRIVE_ICON_DEVICES | while read -r p; do action_add $p; done
}




############ Rox drive icon daemon ############

rox_icon_daemon() {
	local p next current action dev

	read next < $UDEV_EVENT_COUNTER
	while true; do
		! [ -e $FIFO_FILE ] && break
		read p  # pause until we've got events to process
		
		# handle events - these could be inotifyd events, or our commands
		. $EVENTMANAGER_CONFIG	# reload, in case things have changed.		
		log_debug cmd $p
		case $p in
			redraw#*)  
				action_redraw ${p#redraw#}
				continue ;;
				
			redraw_all) 
				action_redraw_all
				continue ;;
				
		esac

		read current < $UDEV_EVENT_COUNTER
		[ -z "$current" ] && current=$next
		#echo ">$next-$current<"
		while [ "$next" -ne "$current" ]; do
			: $((next = next + 1))
			[ $next -gt $COUNTER_MAX ] && next=0
			
			read action dev < $UDEV_DRIVE_ICON_EVENTS/$next
			log_debug udev $action $dev
			case $action in
				add|change)	action_remove $dev; action_add $dev ;;
				remove)	action_remove $dev ;;
			esac
		done
	done < $FIFO_FILE
}

### kill processes run by the same username and DISPLAY
stop_previous_instance() {
	local p pid

	for p in ${DRIVE_ICON_ROOT%.*}.*; do
		! [ -e $p ] && continue
		if [ $(stat -c %u $p) -eq $(id -u) ]; then
			read pid < $p/pid				
			if kill -0 $pid 2>/dev/null; then
				# process is still up
				disp="$(tr '\0' '\n' < /proc/$pid/environ | sed '/DISPLAY=/!d;s/DISPLAY=//')"
				if [ $disp = $DISPLAY ]; then # same display, kill it
					kill $pid # thanks to dumb-init, this will kill everything
					rm -rf $p 
				fi
			else
				# process already dead, remove its ghosts
				[ $IN_SANDBOX ] || rm -rf $p 
			fi
		fi
	done
}



############ mountmon daemon ############

mountmon_daemon() {
	mountmon |
	while read -r EVENT; do

		# get the event, filter devices we don't care
		set -- $EVENT
		ACTION=$1
		DEV=$2
		#MNT="${3}"
		case "$DEV" in
			/dev/loop*) continue ;; # ignore loop mounts
			/dev/*) ;;
			*) continue ;; # ignore none-device mounts
		esac

		# get device, map dm devices as needed
		DEV="${DEV##/dev/}"
		case "$DEV" in
			'mapper'*) DEV="dm-$(dmsetup ls | sed -n 's/^'${DEV#*mapper/}'\t.*:\(.*\)).*/\1/p')" ;;
		esac

		# refresh icon, wait if needed
		case $ACTION in
			DEL:)	wait_while_busy $DEV ;;
		esac
		echo "redraw#$DEV" > $FIFO_FILE # redraw icon
	done
}

# $1 - device (without /dev)
wait_while_busy() {
	local XPID="" dev=$1
	sleep 0.5	# needed due to small delay between umount and 12th field getting non-zero
	while [ $(awk -v dev=$dev '$3==dev {print $12}' /proc/diskstats) -ne 0 ]; do
		if [ -z $XPID ]; then
			Xdialog --title "$(gettext 'WAIT')" --no-buttons \
					--infobox "$(eval_gettext 'Please wait, flushing $dev\nDO NOT REMOVE YOUR DISK ...')" 0 0 1000000 &
			XPID=$!
		fi		
		sleep 1
	done
	[ $XPID ] && kill $XPID	
}


################# frontend ###################

frontend() {
	if [ -z "$UNDER_DUMB_INIT" ]; then
		# fork with dumb-init as supervisor
		UNDER_DUMB_INIT=yes dumb-init ${0} &
		exit
	fi

	# 1. kill previous instance, if any
	stop_previous_instance

	# 2. wait for udev to get ready
	while ! [ -e $UDEV_EVENT_COUNTER ] && [ $UDEV_MAX_WAIT -gt 0 ]; do
		sleep 1
		UDEV_MAX_WAIT=$(( $UDEV_MAX_WAIT - 1 ))
	done
	# if still not ready after timeout expires, just leave
	! [ -e $UDEV_EVENT_COUNTER ] && echo "udev not ready, bailing out" && exit 

	# 3. prepare daemon
	mkdir -p $DRIVE_ICON_ROOT; mkfifo $FIFO_FILE
	echo $$ > $DRIVE_ICON_ROOT/pid
	rox_icon_daemon &

	# 5. start daemon
	inotifyd - $UDEV_EVENT_COUNTER:c > $FIFO_FILE &
	echo redraw_all > $FIFO_FILE # kick start

	# 6. mountmon
	mountmon_daemon &

	# don't die and stay as process supervisor
	while :; do /bin/sleep 1000000; done
}



################# main ###################

### multicall
case ${0##*/} in
	*frontend*) frontend ;;
	*refresh*)  echo "redraw#${1#/dev/}" > $FIFO_FILE ;;
	*redraw*)   echo redraw_all > $FIFO_FILE ;;
esac
