#!/bin/bash
# initscripts functions
#

# sanitize PATH (will be overridden later when /etc/profile is sourced but is useful for udev)
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

localevars=(LANG LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY
            LC_MESSAGES LC_PAPER LC_NAME LC_ADDRESS LC_TELEPHONE
            LC_MEASUREMENT LC_IDENTIFICATION LC_ALL)

vconsolevars=(KEYMAP KEYMAP_TOGGLE FONT FONT_MAP FONT_UNIMAP)

if [[ $1 == "start" ]]; then
	if [[ $STARTING ]]; then
		echo "A daemon is starting another daemon; this is unlikely to work as intended."
	else
		export STARTING=1
	fi
fi

# width:
calc_columns () {
	STAT_COL=80
	if [[ ! -t 1 ]]; then
		USECOLOR=""
	elif [[ -t 0 ]]; then
		# stty will fail when stdin isn't a terminal
		STAT_COL=$(stty size)
		# stty gives "rows cols"; strip the rows number, we just want columns
		STAT_COL=${STAT_COL##* }
	elif tput cols &>/dev/null; then
		# is /usr/share/terminfo already mounted, and TERM recognized?
		STAT_COL=$(tput cols)
	fi
	if (( STAT_COL == 0 )); then
		# if output was 0 (serial console), set default width to 80
		STAT_COL=80
		USECOLOR=""
	fi

	# we use 13 characters for our own stuff
	STAT_COL=$(( STAT_COL - 13 ))

	if [[ -t 1 ]]; then
		SAVE_POSITION="\e[s"
		RESTORE_POSITION="\e[u"
		DEL_TEXT="\e[$(( STAT_COL + 4 ))G"
	else
		SAVE_POSITION=""
		RESTORE_POSITION=""
		DEL_TEXT=""
	fi
}

calc_columns

# disable colors on broken terminals
TERM_COLORS=$(tput colors 2>/dev/null)
if (( $? != 3 )); then
	case $TERM_COLORS in
		*[!0-9]*) USECOLOR="";;
		[0-7])    USECOLOR="";;
		'')       USECOLOR="";;
	esac
fi
unset TERM_COLORS

# clear the TZ envvar, so daemons always respect /etc/localtime
unset TZ

# sanitize the locale settings
unset "${localevars[@]}"

parse_envfile() {
	local file=$1 validkeys=("${@:2}") ret=0 lineno=0 key= val=
	local -r quotes=$'[\'"]' comments=$'[;#]*'

	if [[ -z $file ]]; then
		printf "error: no environment file specified\n"
		return 1
	fi

	if [[ ! -f $file ]]; then
		printf "error: cannot parse \`%s': No such file or directory\n" "$file"
		return 1
	fi

	if [[ ! -r $file ]]; then
		printf "error: cannot read \`%s': Permission denied\n" "$file"
		return 1
	fi

	while IFS='=' read -r key val; do
		(( ++lineno ))

		# trim whitespace, avoiding usage of a tempfile
		key=$(echo "$key" | { read -r key; echo "$key"; })

		# key must exist and line must not be a comment
		[[ -z $key || ${key:0:1} = $comments ]] && continue

		# trim whitespace, strip matching quotes
		val=$(echo "$val" | { read -r val; echo "$val"; })
		[[ ${val:0:1} = $quotes && ${val:(-1)} = "${val:0:1}" ]] && val=${val:1:(-1)}

		if [[ -z $val ]]; then
			printf "error: found key \`%s' without value on line %s of %s\n" \
					"$key" "$lineno" "$file"
			(( ++ret ))
			continue
		fi

		# ignore invalid keys if we have a list of valid ones
		if (( ${#validkeys[*]} )) && ! in_array "$key" "${validkeys[@]}"; then
			continue
		fi

		export "$key=$val" || (( ++ret ))
	done <"$file"

	return $ret
}

# functions:

deltext() {
	printf "${DEL_TEXT}"
}

printhl() {
	printf "${C_OTHER}${PREFIX_HL} ${C_H1}${1}${C_CLEAR} \n"
}

printsep() {
	printf "\n${C_SEPARATOR}   ------------------------------\n"
}

stat_bkgd() {
	printf "${C_OTHER}${PREFIX_REG} ${C_MAIN}${1}${C_CLEAR} "
	deltext
	printf "   ${C_OTHER}[${C_BKGD}BKGD${C_OTHER}]${C_CLEAR} \n"
}

stat_busy() {
	printf "${C_OTHER}${PREFIX_REG} ${C_MAIN}${1}${C_CLEAR} "
	printf "${SAVE_POSITION}"
	deltext
	printf "   ${C_OTHER}[${C_BUSY}BUSY${C_OTHER}]${C_CLEAR} "
}

stat_append() {
	printf "${RESTORE_POSITION}"
	printf -- "${C_MAIN}${1}${C_CLEAR}"
	printf "${SAVE_POSITION}"
}

stat_done() {
	deltext
	printf "   ${C_OTHER}[${C_DONE}DONE${C_OTHER}]${C_CLEAR} \n"
}

stat_fail() {
	deltext
	printf "   ${C_OTHER}[${C_FAIL}FAIL${C_OTHER}]${C_CLEAR} \n"
}

stat_die() {
	stat_fail
	exit ${1:-1}
}

status() {
	[[ $1 = '-q' ]] && { local quiet=1; shift; }
	stat_busy "$1"
	shift
	if (( quiet )); then
		"$@" &>/dev/null
	else
		"$@"
	fi
	local ret=$?
	(( ret == 0 )) && stat_done || stat_fail
	return $ret
}

#  usage : in_array( $needle, $haystack )
# return : 0 - found
#          1 - not found
in_array() {
	local needle=$1; shift
	local item
	for item; do
		[[ $item = "${needle}" ]] && return 0
	done
	return 1 # Not Found
}

# daemons:

add_daemon() {
	[[ -d /run/daemons ]] || mkdir -p /run/daemons
	>| /run/daemons/"$1"
}

rm_daemon() {
	rm -f /run/daemons/"$1"
}

ck_daemon() {
	[[ ! -f /run/daemons/$1 ]]
}

# Check if $1 is a valid daemon name
have_daemon() {
	[[ -f /etc/rc.d/$1 && -x /etc/rc.d/$1 ]]
}

# Check if $1 is started at boot
ck_autostart() {
	local daemon
	for daemon in "${DAEMONS[@]}"; do
		[[ $1 = "${daemon#@}" ]] && return 1
	done
	return 0
}

start_daemon() {
	have_daemon "$1" && /etc/rc.d/"$1" start
}

start_daemon_bkgd() {
	stat_bkgd "Starting $1"
	(start_daemon "$1") >/dev/null &
}

stop_daemon() {
	have_daemon "$1" && /etc/rc.d/"$1" stop
}

# Status functions
status_started() {
	deltext
	echo -ne "$C_OTHER[${C_STRT}STARTED$C_OTHER]$C_CLEAR "
}

status_stopped() {
	deltext
	echo -ne "$C_OTHER[${C_STRT}STOPPED$C_OTHER]$C_CLEAR "
}

ck_status() {
	! ck_daemon "$1" && status_started || status_stopped
}

# Return PID of $1
get_pid() {
	pidof -o %PPID $1 || return 1
}

# Check if PID-file $1 is still the active PID-file for command $2
ck_pidfile() {
	if [[ -f $1 ]]; then
		local fpid ppid
		read -r fpid <"$1"
		ppid=$(get_pid "$2")
		[[ $fpid = "${ppid}" ]] && return 0
	fi
	return 1
}

# PIDs to be omitted by killall5
declare -a omit_pids

add_omit_pids() {
	omit_pids+=( $@ )
}

# Stop all daemons
# This function should *never* ever perform any other actions beside calling stop_daemon()!
# It might be used by a splash system etc. to get a list of daemons to be stopped.
stop_all_daemons() {
	# Find daemons NOT in the DAEMONS array. Shut these down first
	local daemon
	for daemon in /run/daemons/*; do
		[[ -f $daemon ]] || continue
		daemon=${daemon##*/}
		ck_autostart "$daemon" && stop_daemon "$daemon"
	done

	# Shutdown daemons in reverse order
	local i daemon
	for (( i=${#DAEMONS[@]}-1; i>=0; i-- )); do
		[[ ${DAEMONS[i]} = '!'* ]] && continue
		daemon=${DAEMONS[i]#@}
		ck_daemon "$daemon" || stop_daemon "$daemon"
	done
}

# $1 - signal
# $2 - iterations
kill_all_wait() {
	# Send SIGTERM/SIGKILL all processes and wait until killall5
	# reports all done or timeout.
	# Unfortunately killall5 does not support the 0 signal, so just
	# use SIGCONT for checking (which should be ignored).

	local i

	killall5 -${1} ${omit_pids[@]/#/-o } &>/dev/null

	for (( i=0; i<${2}; i++ )); do

		sleep .25 # 1/4 second

		# sending SIGCONT to processes to check if they are there
		killall5 -18 ${omit_pids[@]/#/-o } &>/dev/null

		if (( $? == 2 )); then
			return 0
		fi
	done

	return 1
}

kill_all() {
	stat_busy "Sending SIGTERM to processes"
		kill_all_wait 15 40
		if (( $? == 0 )); then
			stat_done
		else
			stat_fail
			status "Sending SIGKILL to processes" kill_all_wait 9 60
		fi
}

print_welcome() {
	# see os-release(5)
	. /etc/os-release

	echo " "
	printhl "${PRETTY_NAME}\n"
	printhl "${C_H2}${HOME_URL}"
	printsep
}

load_modules() {
	local rc=0

	/usr/lib/systemd/systemd-modules-load &>/dev/null
	rc=$?

	if (( ${#MODULES[*]} )); then
		modprobe -ab "${MODULES[@]}"
		(( rc += $? ))
	fi

	return $rc
}

# Start/trigger udev, load MODULES, and settle udev
udevd_modprobe() {
	# $1 = where we are being called from.
	# This is used to determine which hooks to run.
	status "Starting udev daemon" /usr/lib/systemd/systemd-udevd --daemon

	run_hook "$1_udevlaunched"

	stat_busy "Triggering udev uevents"
		udevadm trigger --action=add --type=subsystems
		udevadm trigger --action=add --type=devices
	stat_done

	# Load modules from the MODULES array and modules-load.d
	status "Loading user-specified modules" load_modules

	status "Waiting for udev uevents to be processed" \
		udevadm settle

	run_hook "$1_udevsettled"

	# in case loading a module changed the display mode
	calc_columns
}

activate_vgs() {
	[[ $USELVM = [yY][eE][sS] && -x $(type -P lvm) && -d /sys/block ]] || return 0
	stat_busy "Activating LVM2 groups"
		vgchange --sysinit -a y >/dev/null
	(( $? == 0 )) && stat_done || stat_fail
}

do_unlock_legacy() {
	# $1 = requested name
	# $2 = source device
	# $3 = password
	# $4 = options
	printf "${C_FAIL}Using legacy crypttab format. This will stop working in the future. See crypttab(5).${C_CLEAR}\n"
	local open=create a=$1 b=$2 failed=0
	# Ordering of options is different if you are using LUKS vs. not.
	# Use ugly swizzling to deal with it.
	# isLuks only gives an exit code but no output to stdout or stderr.
	if cryptsetup isLuks "$2" 2>/dev/null; then
		open=luksOpen
		a=$2
		b=$1
	fi
	case $3 in
		SWAP)
			local _overwriteokay=0
			if [[ -b $2 && -r $2 ]]; then
				# This is DANGEROUS! If there is any known file system,
				# partition table, RAID, or LVM volume on the device,
				# we don't overwrite it.
				#
				# 'blkid' returns 2 if no valid signature has been found.
				# Only in this case should we allow overwriting the device.
				#
				# This sanity check _should_ be sufficient, but it might not.
				# This may cause data loss if it is not used carefully.
				blkid -p "$2" &>/dev/null
				(( $? == 2 )) && _overwriteokay=1
			fi
			if (( _overwriteokay == 0 )); then
				false
			elif cryptsetup -d /dev/urandom $4 $open "$a" "$b" >/dev/null; then
				printf "creating swapspace..\n"
				mkswap -f -L $1 /dev/mapper/$1 >/dev/null
			fi;;
		ASK)
			printf "\nOpening '$1' volume:\n"
			cryptsetup $4 $open "$a" "$b" < /dev/console;;
		/dev*)
			local ckdev=${3%%:*}
			local cka=${3#*:}
			local ckb=${cka#*:}
			local cka=${cka%:*}
			local ckfile=/dev/ckfile
			local ckdir=/dev/ckdir
			case ${cka} in
				*[!0-9]*)
					# Use a file on the device
					# cka is not numeric: cka=filesystem, ckb=path
					mkdir ${ckdir}
					mount -r -t ${cka} ${ckdev} ${ckdir}
					dd if=${ckdir}/${ckb} of=${ckfile} >/dev/null 2>&1
					umount ${ckdir}
					rmdir ${ckdir};;
				*)
					# Read raw data from the block device
					# cka is numeric: cka=offset, ckb=length
					dd if=${ckdev} of=${ckfile} bs=1 skip=${cka} count=${ckb} >/dev/null 2>&1;;
			esac
			cryptsetup -d ${ckfile} $4 $open "$a" "$b" >/dev/null
			dd if=/dev/urandom of=${ckfile} bs=1 count=$(stat -c %s ${ckfile}) conv=notrunc >/dev/null 2>&1
			rm ${ckfile};;
		/*)
			cryptsetup -d "$3" $4 $open "$a" "$b" >/dev/null;;
		*)
			echo "$3" | cryptsetup $4 $open "$a" "$b" >/dev/null;;
	esac
	return $?
}

do_unlock_systemd() {
	local failed=0

	if ! /usr/lib/systemd/systemd-cryptsetup attach "$1" "$2" "$3" $4; then
		failed=1
	else
		if IFS=, in_array swap ${options[@]}; then
			# create swap on the device only if no fs signature exists
			blkid -p "$2" &>/dev/null
			if (( $? != 2 )) || ! mkswap /dev/mapper/$name >/dev/null; then
				failed=1
			fi
		elif IFS=, in_array tmp ${options[@]}; then
			# create fs on the device only if no fs signature exists
			blkid -p "$2" &>/dev/null
			if (( $? != 2 )) || ! mke2fs /dev/mapper/$name >/dev/null; then
				failed=1
			fi
		fi
	fi
	return $failed
}

do_unlock() {
	local name=$1 device=$2 password=$3 options=$4

	printf "${C_MAIN}Unlocking $1${C_CLEAR}\n"

	if [[ ${options:0:2} =~ -. ]]; then
		do_unlock_legacy "$name" "$device" "$password" "$options"
		return $?
	fi

	case $password in
		ASK|SWAP)
			do_unlock_legacy "$name" "$device" "$password" "$options"
			;;
		/dev/*)
			if [[ ${password##*:} == $password ]]; then
				do_unlock_systemd "$name" "$device" "$password" "$options"
			else
				do_unlock_legacy "$name" "$device" "$password" "$options"
			fi
			;;
		/*|none|-)
			do_unlock_systemd "$name" "$device" "$password" "$options"
			;;
		*)
			do_unlock_legacy "$name" "$device" "$password" "$options"
			;;
	esac
	failed=$?
	if (( $failed )); then
		printf "${C_FAIL}Unlocking of $1 failed.${C_CLEAR}\n"
	fi
	return $?
}

do_lock() {
	status "Detaching encrypted device ${1}" /usr/lib/systemd/systemd-cryptsetup detach "$1" >/dev/null
}

read_crypttab() {
	# $1 = function to call with the split out line from the crypttab
	local line nspo failed=0
	while read line <&3; do
		[[ $line && $line != '#'* ]] || continue
		eval nspo=("${line%#*}")
		if $1 "${nspo[0]}" "${nspo[1]}" "${nspo[2]}" "${nspo[*]:3}"; then
			crypto_unlocked=1
		else
			failed=1
		fi
	done 3< /etc/crypttab
	return $failed
}

set_timezone() {
	local tz=$1 zonefile=/usr/share/zoneinfo/$1

	[[ $tz ]] || return 1

	if [[ ! -e $zonefile ]]; then
		printf "error: \`%s' is not a valid time zone\n" "$tz"
		return 1
	fi

	if [[ -L /etc/localtime && /etc/localtime -ef $zonefile ]]; then
		return 0
	else
		ln -sf "/usr/share/zoneinfo/$tz" /etc/localtime
	fi
}

# Filesystem functions
# These can be overridden/reused for customizations like shutdown/loop-fsck.
NETFS="nfs,nfs4,smbfs,cifs,codafs,ncpfs,shfs,fuse,fuseblk,glusterfs,davfs,fuse.glusterfs"

# Check local filesystems
fsck_all() {
	if [[ -f /forcefsck ]] || in_array forcefsck $(< /proc/cmdline); then
		FORCEFSCK="-f"
	elif [[ -f /fastboot ]] || in_array fastboot $(< /proc/cmdline); then
		return 0
	elif [[ -e /run/initramfs/root-fsck ]]; then
		IGNORE_MOUNTED="-M"
	fi

	fsck -A -T -C${FSCK_FD} -a -t no${NETFS//,/,no},noopts=_netdev ${IGNORE_MOUNTED} -- ${FORCEFSCK}
}

# Single-user login and/or automatic reboot after fsck (if needed)
fsck_reboot() {
	# $1 = exit code returned by fsck
	# Ignore conditions 'FS errors corrected' and 'Cancelled by the user'
	(( ($1 | 33) == 33 )) && return 0
	if (( $1 & 2 )); then
		echo
		echo "********************** REBOOT REQUIRED *********************"
		echo "*                                                          *"
		echo "* The system will be rebooted automatically in 15 seconds. *"
		echo "*                                                          *"
		echo "************************************************************"
		echo
		sleep 15
	else
		echo
		echo "*****************  FILESYSTEM CHECK FAILED  ****************"
		echo "*                                                          *"
		echo "*  Please repair manually and reboot. Note that the root   *"
		echo "*  file system is currently mounted read-only. To remount  *"
		echo "*  it read-write, type: mount -o remount,rw /              *"
		echo "*  When you exit the maintenance shell, the system will    *"
		echo "*  reboot automatically.                                   *"
		echo "*                                                          *"
		echo "************************************************************"
		echo
		sulogin -p
	fi
	echo "Automatic reboot in progress..."
	umount -a
	mount -o remount,ro /
	reboot -f
	exit 0
}

mount_all() {
	mount -a -t "nosysfs,no${NETFS//,/,no}" -O no_netdev
}

umount_all() {
	# $1: restrict to fstype

	findmnt -mrunRo TARGET,FSTYPE,OPTIONS / | {
		while read -r target fstype options; do
			# match only targeted fstypes
			if [[ $1 && $1 != "$fstype" ]]; then
				continue
			fi

			# do not unmount API filesystems
			if [[ $target = /@(proc|sys|run|dev|dev/pts) ]]; then
				continue
			fi

			# avoid networked devices
			IFS=, read -ra opts <<< "$options"
			if in_array _netdev "${opts[@]}"; then
				continue
			fi

			mounts=("$target" "${mounts[@]}")
		done

		if (( ${#mounts[*]} )); then
			umount -r "${mounts[@]}"
		fi
	}

}

remove_leftover() {
	status 'Removing leftover files' systemd-tmpfiles --create --remove --clean
}

bootlogd_stop() {
	[[ -f /run/bootlogd.pid ]] || return 0
	touch /var/log/boot
	kill $(< /run/bootlogd.pid)
	rm -f /run/bootlogd.pid
}

###############################
# Custom hooks in initscripts #
###############################
# Hooks can be used to include custom code in various places in the rc.* scripts
#
# Define a hook function in a functions.d file using:
#  function_name() {
#    ...
#  }
#  add_hook hook_name function_name
# It is allowed to register several hook functions for the same hook
# Is is also allowed to register the same hook function for several hooks
#
# Currently, the following hooks exist:
# sysinit_start: at the beginning of rc.sysinit
# multi_start: at the beginning of rc.multi
# single_start: at the beginning of rc.single
# shutdown_start: at the beginning of rc.shutdown
# sysinit_end: at the end of rc.sysinit
# multi_end: at the end of rc.multi
# single_end: at the end of rc.single
# sysinit_udevlaunched: after udev has been launched in rc.sysinit
# single_udevlaunched: after udev has been launched in rc.single
# sysinit_udevsettled: after uevents have settled in rc.sysinit
# single_udevsettled: after uevents have settled in rc.single
# sysinit_premount: before local filesystems are mounted, but after root is mounted read-write in rc.sysinit
# sysinit_postmount: after local filesystems are mounted
# shutdown_prekillall: before all processes are being killed in rc.shutdown
# single_prekillall: before all processes are being killed in rc.single
# shutdown_postkillall: after all processes have been killed in rc.shutdown
# single_postkillall: after all processes have been killed in rc.single
# shutdown_preumount: after last filesystem write, but before filesystems are unmounted
# shutdown_postumount: after filesystems are unmounted
# shutdown_poweroff: directly before powering off in rc.shutdown
#
# Declare add_hook and run_hook as read-only to prevent overwriting them.
# Too bad we cannot do the same thing with hook_funcs

if (( RC_FUNCTIONS_HOOK_FUNCS_DEFINED != 1 )); then
	declare -A hook_funcs

	add_hook() {
		[[ $1 && $2 ]] || return 1
		hook_funcs[$1]+=" $2"
	}

	run_hook() {
		[[ $1 ]] || return 1
		local func
		for func in ${hook_funcs["$1"]}; do
			"${func}"
		done
	}

	declare -fr add_hook run_hook
	declare -r RC_FUNCTIONS_HOOK_FUNCS_DEFINED=1
fi

if [[ $DAEMON_LOCALE != [nN][oO] ]]; then
	export LANG=${LOCALE:-C}
	if [[ -r /etc/locale.conf ]]; then
		parse_envfile /etc/locale.conf "${localevars[@]}"
	fi
else
	export LANG=C
fi

# set colors
if [[ $USECOLOR != [nN][oO] ]]; then
	if tput setaf 0 &>/dev/null; then
		C_CLEAR=$(tput sgr0)                      # clear text
		C_MAIN=${C_CLEAR}$(tput bold)        # main text
		C_OTHER=${C_MAIN}$(tput setaf 4)     # prefix & brackets
		C_SEPARATOR=${C_MAIN}$(tput setaf 0) # separator
		C_BUSY=${C_CLEAR}$(tput setaf 6)     # busy
		C_FAIL=${C_MAIN}$(tput setaf 1)      # failed
		C_DONE=${C_MAIN}                          # completed
		C_BKGD=${C_MAIN}$(tput setaf 5)      # backgrounded
		C_H1=${C_MAIN}                            # highlight text 1
		C_H2=${C_MAIN}$(tput setaf 6)        # highlight text 2
	else
		C_CLEAR="\e[m"          # clear text
		C_MAIN="\e[;1m"         # main text
		C_OTHER="\e[1;34m"      # prefix & brackets
		C_SEPARATOR="\e[1;30m"  # separator
		C_BUSY="\e[;36m"        # busy
		C_FAIL="\e[1;31m"       # failed
		C_DONE=${C_MAIN}        # completed
		C_BKGD="\e[1;35m"       # backgrounded
		C_H1=${C_MAIN}          # highlight text 1
		C_H2="\e[1;36m"         # highlight text 2
	fi
fi

# prefixes:

PREFIX_REG="::"
PREFIX_HL=" >"

# Source additional functions at the end to allow overrides
for f in /etc/rc.d/functions.d/*; do
	[[ -e $f ]] && . "$f"
done

# End of file
# vim: set ts=2 sw=2 noet:
