#!/bin/bash
#
# restart-services - tool to restart updated or reinstalled services
#
# Copyright 2014-2020, Marc Schiffbauer <mschiff@gentoo.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License at <http://www.gnu.org/licenses/> for
# more details.

VERSION="0.17.0"

set -o pipefail
trap clean_exit EXIT
CNF="/etc/restart-services.conf"
SELF="$(basename "$0")"


function clean_exit() {
	if [[ -w /dev/tty && $- =~ .*i.* ]]; then
		stty icanon echo echok
	fi
}

function die() {
	if [[ -n $* ]]; then
		echo "$(red ERROR): $*" >&2
	fi
	exit 1
}

function include() {
	local files=( "$@" )
	for f in ${files[*]}; do
		debug "loading config file: $f"
		if [[ -f $f ]]; then
			if ! . "$f"; then
				die "config file could not be parsed ($f)"
			fi
		else
			die "config file not found ($f)"
		fi
	done
}

function detect_init() {
	# openrc is default
	debug "detect_init() called"
	INIT="openrc"

	# detect systemd
	if [[ $(basename "$(readlink /proc/1/exe)") =~ systemd.* ]]; then
		debug "detected systemd"
		INIT=systemd
	fi

	case $INIT in
		openrc)
			if selinuxenabled 2>/dev/null; then
				debug "using 'run_init' to call 'rc-service'"
				RC="run_init rc-service"
			else
				RC="rc-service"
			fi
			return 0
		;;
		systemd)
			RC="systemctl"
			return 0
		;;
	esac
}

function systemd_detect_require_daemon_reload() {
	# detect, if any unit files have changed on disk so that running 'systemctl daemon-reload' is required
	debug "systemd_detect_require_daemon_reload() called"
	local unit changed_units=()
	while read -r unit; do
		if systemctl status "$unit" |& grep "^Warning:.*changed on disk"; then
			debug "systemd_detect_require_daemon_reload(): unit changed on disk: $unit"
		fi
	done < <(systemctl | grep -F .service | awk '{print $1}')|grep -q "^Warning:.*changed on disk"
}

function sv_is_running() {
	local SV=$1; shift
	case $INIT in
		openrc)
			if rc-service "$SV" -- --ifstarted >/dev/null; then
				return 0
			else
				return 1
			fi
		;;
	esac
}

function sv_restart() {
	local SV=$1; shift
	local arg=$1; shift
	case $INIT in
		openrc)
			if [[ $arg == "nodeps" ]]; then
				nodeps="--nodeps"
			fi
			debug "running $RC $SV -- --ifstarted $* restart"
			$RC "$SV" -- --ifstarted $nodeps restart
		;;
		systemd)
			if [[ $arg == "nodeps" ]]; then
				nodeps="--job-mode=ignore-dependencies"
			fi
			debug "running $RC $SV try-restart"
			$RC $nodeps try-restart "$SV"
		;;
	esac
}

function sv_reload() {
	local SV=$1; shift
	case $INIT in
		openrc)
			debug "running $RC $SV -- --ifstarted $* reload"
			$RC "$SV" -- --ifstarted $* reload
		;;
		systemd)
			debug "running $RC try-reload-or-restart $SV"
			$RC try-reload-or-restart "$SV"
		;;
	esac
}

function sv_restart_critical() {
	local SV=$1; shift
	[[ $SV ]] || die "BUG: sv_restart_critical() called without arg"

	if [[ $NO_COUNTDOWN ]]; then
		# -N set, restart withount countdown
		sv_restart "$SV" "$@"
	else
		# user can skip restart by pressing ctrl-c (SIGINT)
		trap "echo -e '\n$(white '=>') $(yellow Skipping restart) of service $(white "$SV")'; unset SV" INT
		echo -n "$(yellow "->") Press ctrl-c within 5 seconds to skip this service "
		for i in 5 4 3 2 1; do
			if [[ -z $SV ]]; then
				break
			fi
			echo -n .
			sleep 1
		done
		trap - INT
		echo ""
		if [[ $SV ]]; then
			# restart service if user did not abort (== SV is still set)
			sv_restart "$SV" "$@"
		fi
	fi
}

function colorize() {
	local C T

	C=$1; shift
	T="$*"
	if [[ $NO_COLOR ]]; then
		echo -n "$T"
		return 0
	fi
	case $C in
		red)
			echo -en '\E[1;31;40m'"$T"
		;;
		green)
			echo -en '\E[1;32;40m'"$T"
		;;
		yellow)
			echo -en '\E[1;33;40m'"$T"
		;;
		blue)
			echo -en '\E[1;34;40m'"$T"
		;;
		white)
			echo -en '\E[1;37;40m'"$T"
		;;
	esac
	tput sgr0
}

function get_confirmation() {
	local answer
	local q=$1
	local sv
	sv=$(white "$2")
	if [[ $INTERACTIVE ]]; then
		until [[ $answer =~ [YyNn] ]]; do
			answer=""
			read -n1 -s -p "$(yellow "->") ${q//%1/$sv}? ($(green y)/$(red N))" answer
			if [[ -z $answer ]]; then
				answer=n
			fi
			echo ""
		done
		case $answer in
			Y|y) return 0;;
			N|n) return 1;;
		esac
	fi
	return 0
}

function top_level_ppid() {
	local pid=$1
	local stat
	local ppid_regex='^[0-9]+ \(.*\) . ([0-9]+) .*'
	# grsec denies ".... </proc/pid/stat", but "cat /proc/pid/stat" is allowed
	# iconv: see bug #726768
	stat="$(cat "/proc/$pid/stat" 2>/dev/null|iconv -f UTF-8 -t UTF-8 -c)" || return 1
	if [[ $stat =~ $ppid_regex ]]; then
		local ppid=${BASH_REMATCH[1]}
	else
		echo "$(red BUG): could not determine parent pid of $pid in '/proc/$pid/stat'." >&2
		echo "$(red BUG): Please open a bugreport for this on bugs.gentoo.org" >&2
		echo "$(red BUG): /proc/$pid/stat:" >&2
		echo "$(red BUG): $stat" >&2
		ppid=1
	fi
	if [[ $ppid -le 1 ]] ; then
		echo "$pid"
	else
		top_level_ppid "$ppid"
	fi
}

red() { colorize red "$@"; }
green() { colorize green "$@"; }
yellow() { colorize yellow "$@"; }
white() { colorize white "$@"; }
blue() { colorize blue "$@"; }

function echo_sv_action() {
	local color="$1"
	local sv="$2"
	local msg="$3"
	echo "$($color "=>") $(white "$sv"): $msg"
}

function find_rc_service() {
	local pid=$1
	local t


	case $INIT in
		openrc)
			# easiest way: process has RC_SERVICE env set, try to find that.
			# grsec denies "xargs .... </proc/pid/environ", but "cat /proc/pid/environ" is allowed
			t=$(cat "/proc/$pid/environ" 2>/dev/null|xargs -0 -n1 echo|grep -a "^RC_SERVICE="|cut -d"=" -f2-)
			if [[ $t ]]; then
				echo -n "$t"
				return 0
			else
				return 1
			fi
		;;
		systemd)
			ps --no-headers -o'unit' "$pid"
			return $?
		;;
	esac
}

function get_package_of_file() {
	local pkg
	local exe="$1"
	pkg=$(qfile --nocolor -- "$exe"|awk '{print $1}')

	if [[ -z $pkg ]]; then
		if [[ $exe =~ .*/lib(32|64)/.* ]]; then
			# sometimes file in package is just /usr/lib/foo but
			# is running as */lib(32|64)/foo
			# so test again with just /lib/ in path
			exe="${exe/\/lib32//lib}"
			exe="${exe/\/lib64//lib}"
			pkg=$(qfile --nocolor -- "$exe"|awk '{print $1}')
		fi
	fi

	# depending on portage-utils version the output differs a but, handle that
	# bug #720094
	pkg=${pkg/:/}
	if [[ $pkg ]]; then
		echo -n "$pkg"
		return 0
	else
		return 1
	fi
}

function debug() {
	if [[ $DEBUG ]]; then
		if [[ $lu_pid ]]; then
			echo "debug: $lu_pid: $*" >&2
		else
			echo "debug: $*" >&2
		fi
	fi
}

sp="/-\|"
sc=0
function spin() {
	if [[ ! $DEBUG ]]; then
		printf "\b%s" "${sp:sc++:1}"
		((sc==${#sp})) && sc=0
	fi
}
function spin_done() {
	if [[ ! $DEBUG ]]; then
		printf "\b"
	fi
}

args=$(getopt -n "$SELF" -o cdhilNuV -l critical,no-color,no-countdown,debug,force-unknown,help,interactive,list-only,version -- "$@") || exit 1
set -- $args

for i; do
	case "$i" in
		-c|--critical)
			shift
			CRITICAL=1
		;;
		-d|--debug)
			shift
			DEBUG=1
			if [[ ! $1 =~ ^-.* ]]; then
				# use lib_users output from a file intead
				# from current system
				DEBUG_FILE="$1"
				shift
			fi

		;;
		-h|--help)
			shift
			cat<<-EOF

			  Usage: $SELF [options]

			    -c, --critical	also restart 'critical' services
			    			defined in SV_CRITICAL or SV_CRITICAL_WITH_NODEPS
			    			in $CNF
			    -i, --interactive	ask before restarting any service
			    -u, --force-unknown	enable restarting of unknown/unclassified services
			    -d, --debug		enable debug messages
			    -h, --help		show help and exit
			    -l, --list-only	only list services that need to be restarted
			    -N, --no-countdown	do not wait before restarting a critical service
			    -V, --version	show version and exit
			    --no-color		do not use colors

			EOF
			exit 0
		;;
		-i|--interactive)
			shift
			INTERACTIVE=1
		;;
		-l|--list-only)
			shift
			LIST_ONLY=1
		;;
		--no-color)
			shift
			NO_COLOR=1
		;;
		-N|--no-countdown)
			shift
			NO_COUNTDOWN=1
		;;
		-u|--force-unknown)
			shift
			FORCE_UNKNOWN=1
		;;
		-V|--version)
			shift
			echo "$SELF $VERSION"
			echo "Copyright 2014-2020 by Marc Schiffbauer <mschiff@gentoo.org>"
			exit
		;;
	esac
done

# make sure we are running as root
if [[ $UID -ne 0 ]]; then
	die "$SELF needs to be run as root"
fi

# check /detect init system
detect_init || die "unsupported init system: $INIT"
debug "INIT is set to $INIT"

if [[ $CRITICAL ]]; then
	echo "$(white "->") Restart of critical services enabled."
fi

# before reading config
declare -A CUSTOM_PROCESS_MAP=()

# load config
include $CNF

# init assoc arrays
declare -A KNOWN_PROCESSES_EXE=() UNKNOWN_PROCESSES_EXE=() REALLY_UNKNOWN_PROCESSES_EXE=() TODO_PROCESSES_EXE=()
declare -A KNOWN_PROCESSES_PROC=() UNKNOWN_PROCESSES_PROC=() REALLY_UNKNOWN_PROCESSES_PROC=() TODO_PROCESSES_PROC=()
declare -A INITTAB_PROCESSES=() FOUND_INITTAB_PROCESSES=() FOUND_INITTAB_PROCESSES_UNKNOWN=()
declare -A UNKNOWN_PROCESSES_ROOT=() REALLY_UNKNOWN_PROCESSES_ROOT=()

ROOT=/
INIT_SCRIPTS=()
FOUND_SV_CRITICAL=()
FOUND_SV_CRITICAL_WITH_NODEPS=()
FOUND_SV_ALWAYS_RELOAD_ONLY=()
FOUND_SV_ALWAYS_WITH_NODEPS=()
FOUND_SV_ALWAYS=()
FOUND_SV_IGNORE=()
FOUND_SV_UNKNOWN=()

echo -n "$(white "->") Searching for services that need to be restarted ...  "

if [[ $DEBUG ]]; then
	debug "this is $SELF version $VERSION"
	debug "using $(lib_users --version)"
fi

debug "analyzing lib_users output ..."
while IFS=";" read -r lu_pids lu_libs lu_proc; do
	unset lu_pid
	debug "analyzing $lu_proc (pids $lu_pids)"
	# bug #678500: ignore tmp-files (lib_users -i '/tmp/*' will not match anything ... lib_users bug? "*/tmp/*" *will* match
	# but also will match /var/tmp/portage for example which we need ...
	# so for now we look at the deleted libs ourself for false positive filtering instead of relying on the lib_users filter
	#
	# deleted lib should never be just "/"
	if [[ $lu_libs =~ ^/(,.*)?$ ]]; then
		# ex. bug #678820: ignore bpfilter_umh
		debug "ignoring false positive (proc=$lu_proc with pid(s) $lu_pids): seems to be a kernel process"
		continue
	fi

	# ignore current proc if *all* lu_libs reside in /tmp
	is_tmp=1
	for lu_lib in ${lu_libs//,/ }; do
		spin
		if ! [[ $lu_lib =~ ^/tmp/.* ]]; then
			unset is_tmp
		fi
	done
	if [[ $is_tmp ]]; then
		debug "ignoring false positive (proc=$lu_proc with pid(s) $lu_pids): all deleted files reside in /tmp"
		continue
	fi

	# check for custom process mappings vis CUSTOM_PROCESS_MAP
	debug "checking for custom process mapping ..."
	for custom_proc in "${!CUSTOM_PROCESS_MAP[@]}"; do
		spin
		unset match
		if [[ ${custom_proc:0:2} == "E@" ]]; then
			debug "trying ere match on ${custom_proc:2}"
			if [[ $lu_proc =~ ${custom_proc:2} ]]; then
				match=1
			fi
		else
			debug "trying pattern match on $custom_proc"
			if [[ $lu_proc == $custom_proc ]]; then
				match=1
			fi
		fi
		if [[ $match ]]; then
			debug "custom proc match ($custom_proc): $lu_proc"
			if [[ -e /etc/init.d/${CUSTOM_PROCESS_MAP[$custom_proc]} ]]; then
				debug "adding defined init script: ${CUSTOM_PROCESS_MAP[$custom_proc]}"
				INIT_SCRIPTS+=( "${CUSTOM_PROCESS_MAP[$custom_proc]}" )
				continue 2
			else
				debug "defined service '${CUSTOM_PROCESS_MAP[$custom_proc]}' not found in /etc/init.d"
				debug "trying to find service automatically"
				echo "$(yellow "=> WARNING")"": defined custom service '${CUSTOM_PROCESS_MAP[$custom_proc]}' not found. File /etc/init.d/${CUSTOM_PROCESS_MAP[$custom_proc]} does not exist. Switching to autodetection."

			fi
		else
			debug "no match found"
		fi
	done

	debug "checking processes of $lu_proc (pids: $lu_pids)"
	for lu_pid in ${lu_pids//,/ }; do
		spin
		debug "checking pid $lu_pid"
		orig_lu_pid=$lu_pid

		# always look at parent pid
		lu_pid=$(top_level_ppid "$lu_pid")

		# sometimes pids disappear:
		if ! [[ $lu_pid ]]; then
			debug "process disappeared, skipping"
			continue
		fi

		if [[ $orig_lu_pid != $lu_pid ]]; then
			debug "lu_pid set to its ppid (was $orig_lu_pid)"
		fi

		R="$(readlink "/proc/$lu_pid/root")"
		unset lu_exe
		lu_exe="$(readlink "/proc/$lu_pid/exe")"

		# remove /var/tmp/portage/*/*/image prefix, see kernel bug #10856
		# https://bugzilla.kernel.org/show_bug.cgi?id=10856
		lu_exe="${lu_exe#\/var\/tmp\/portage\/*\/*\/image}"
		
		# remove " (deleted) suffix
		lu_exe="${lu_exe/ (deleted)/}"

		# normally we do not want to try to restart services that run within another root
		# but some exceptions exist, where config and/or init script will put a service into
		# a chroot. So we will consider these well known chroots.
		# The regex CHROOT_EXCEPTIONS_RE matches all those well known chroots
		CHROOT_EXCEPTIONS_RE='^(/etc/unbound'
		CHROOT_EXCEPTIONS_RE+="$(test -f /etc/conf.d/named && { . /etc/conf.d/named; echo "|$CHROOT"; })"
		CHROOT_EXCEPTIONS_RE+='|/run/lldpd'
		CHROOT_EXCEPTIONS_RE+='|/var/empty)/?$'

		# skip processes that are already known
		for i in "${!KNOWN_PROCESSES_EXE[@]}"; do
			spin
			if [[ $i == "$lu_pid" ]]; then
				debug "skipping $lu_exe (pid $orig_lu_pid): we already found the service for that ppid ($lu_pid)"
				debug "WARNING: Including child-process environment. This might contain sensitive data. Please revise carefully before including it in bugreports."
				debug "/proc/$orig_lu_pid/environ: $(xargs -0 -n1 echo < "/proc/$orig_lu_pid/environ" 2>/dev/null|paste -s -d ' ')"
				continue 2
			fi
		done
		# skip processes that are already known within another root
		for i in "${!UNKNOWN_PROCESSES_EXE[@]}"; do
			spin
			if [[ $i == "$lu_pid" ]]; then
				debug "skipping $lu_exe (pid $orig_lu_pid): we already found the ppid ($lu_pid) in another root"
				debug "WARNING: Including child-process environment. This might contain sensitive data. Please revise carefully before including it in bugreports."
				debug "/proc/$orig_lu_pid/environ: $(xargs -0 -n1 echo < "/proc/$orig_lu_pid/environ" 2>/dev/null|paste -s -d ' ')"
				continue 2
			fi
		done

		# in some cases a child process may be the 'real' parent process of a service and thus having RC_SERVICE set in its env
		for i in "${!TODO_PROCESSES_EXE[@]}"; do
			spin
			if [[ $i == "$lu_pid" ]]; then
				# process already in TODO_PROCESSES_*, lets check this child
				debug "checking $lu_exe (pid $orig_lu_pid): we already found the ppid ($lu_pid) but did not find an init script yet"
				debug "checking env of pid $orig_lu_pid"
				t=$(find_rc_service $orig_lu_pid)
				if [[ $t ]]; then
					debug "child process hit: found $t for $lu_exe (pid $orig_lu_pid)"
					debug "adding to INIT_SCRIPTS: $t"
					debug "moving from TODO_PROCESSES to KNOWN_PROCESSES: $lu_exe ($lu_pid)"
					INIT_SCRIPTS+=( "$t" )
					KNOWN_PROCESSES_EXE+=( ["$lu_pid"]="$lu_exe" )
					KNOWN_PROCESSES_PROC+=( ["$lu_pid"]="$lu_proc" )
					unset TODO_PROCESSES_EXE["$lu_pid"]
					unset TODO_PROCESSES_PROC["$lu_pid"]
				else
					debug "no luck, forgetting $orig_lu_pid"
				fi
				continue 2
			fi
		done

		debug "process root is $R"

		if [[ $R == "$ROOT" || $R =~ $CHROOT_EXCEPTIONS_RE ]]; then
			t=$(find_rc_service $lu_pid)
			if [[ $t ]]; then
				debug "direct hit: found $t for $lu_exe"
				debug "adding to INIT_SCRIPTS: $t"
				debug "adding to KNOWN_PROCESSES: $lu_exe ($lu_pid)"
				INIT_SCRIPTS+=( "$t" )
				KNOWN_PROCESSES_EXE+=( ["$lu_pid"]="$lu_exe" )
				KNOWN_PROCESSES_PROC+=( ["$lu_pid"]="$lu_proc" )
				continue
			else
				debug "no direct hit: process $lu_proc has no RC_SERVICE env set, adding to TODO_PROCESSES"
				debug "WARNING: Including process environment. This might contain sensitive data. Please revise carefully before including it in bugreports."
				debug "/proc/$lu_pid/environ: $(xargs -0 -n1 echo < "/proc/$lu_pid/environ" 2>/dev/null|paste -s -d ' ')"
				TODO_PROCESSES_EXE+=( ["$lu_pid"]="$lu_exe" )
				TODO_PROCESSES_PROC+=( ["$lu_pid"]="$lu_proc" )
				continue
			fi
		else
			debug "pid $orig_lu_pid not considered because it runs in a different root (root=$R)"
			UNKNOWN_PROCESSES_EXE+=( ["$lu_pid"]="$lu_exe" )
			UNKNOWN_PROCESSES_PROC+=( ["$lu_pid"]="$lu_proc" )
			UNKNOWN_PROCESSES_ROOT+=( ["$lu_pid"]="$R" )
		fi
	done
# lib_users <= 0.12 reports postgres' "/anon_hugepage" as deleted lib, so tell it to ignore this manually (see bug #648356)
# bug #678500: ignore tmp-files
done < <(if [[ $DEBUG && $DEBUG_FILE ]]; then cat "$DEBUG_FILE"; else lib_users -m -I /anon_hugepage -i '/tmp/*'; fi)

debug "analyzing remaining processes (not direct hits) ..."
debug "TODO_PROCESSES_EXE: ${TODO_PROCESSES_EXE[*]}"
# analyze remaining procs that do not have any RC_SERVICE
while read lu_pid; do
	spin
	lu_exe="${TODO_PROCESSES_EXE[$lu_pid]}"
	lu_proc="${TODO_PROCESSES_PROC[$lu_pid]}"

	debug "checking exe $lu_exe"
	unset PKG

	# DO NOT manipulate lu_exe here because
	# they have to match known processes so that we can
	# find false positives at the end
	# if we need to do that: the prior loop is the right place

	# try to find out whether lu_pid is a script
	SCRIPT_RE="^(/usr/bin/(perl|ruby)([0-9.]+)?|/usr/bin/python([0-9.]+[a-z]*)?|/bin/(bash|ksh|zsh))$"

	if [[ "$lu_exe" =~ $SCRIPT_RE ]]; then
		debug "process looks like a script"
		# $lu_pid is propably a script
		# try to determine script name

		script_candidates=( $lu_proc )
		if [[ ${#script_candidates[@]} -ge 2 ]]; then
			# swap index 0<->1 because second part is more likely
			# to be the script name
			tmp=${script_candidates[0]}
			script_candidates[0]=${script_candidates[1]}
			script_candidates[1]=$tmp

			unset lu_exe_script
			for s in "${script_candidates[@]}"; do
				s=${s//\"/}
				debug "checking proc part '$s'"
				EXEC_PATH_RE="/(usr/)?((s)?bin|lib(32|64|exec)?)?/"
				if [[ $s =~ $EXEC_PATH_RE ]]; then
					debug "'$s' may be script, looking for package"
					if PKG=$(get_package_of_file "$s"); then
						debug "bingo. '$s' belongs to a package"
						lu_exe_script="$s"
						break
					fi
				fi
			done
		fi
	else
		debug "process does not look like a script"
		PKG=$(get_package_of_file "$lu_exe")
	fi

	if [[ $PKG ]]; then
		debug "found package: $PKG"

		# some processes have their init scripts in a seperate package :-/
		# so as a last resort we add hardcoded well known cases
		case $PKG in
			dev-lang/php) PKG="app-admin/eselect-php" ;;
		esac

		_init_scripts=( $(qlist -e $PKG|grep /etc/init.d/|paste -s -d" ") )
		if [[ ${#_init_scripts} -gt 0 ]]; then
			debug "found init scripts: ${_init_scripts[*]}"
			for i in "${_init_scripts[@]}"; do
				if sv_is_running "$i"; then
					debug "found started service, adding init script: $i"
					INIT_SCRIPTS+=( "$i" )
					continue 2
				else
					debug "no started service found."
				fi
			done
		elif [[ -e /etc/inittab ]] && grep -Eq ":respawn:$lu_exe([[:space:]]+|$)" /etc/inittab; then
			debug "inittab process found: $lu_exe"
			debug "adding to INITTAB_PROCESSES: $lu_pid:$lu_exe"
			INITTAB_PROCESSES+=( ["$lu_pid"]="$lu_exe" )
			continue
		fi

		# try package/exe/script name as init script names
		debug "no init scripts found. trying some kind of fuzzy search"
		init_candidates=( $(find /etc/init.d -regex "/etc/init.d/\(${PKG#*/}\|$(basename "${lu_exe_script:-$lu_exe}")\)\(-[0-9.-]*\)?\(d\)?" -printf '%P') )
		debug "other candidates: ${init_candidates[*]}"
		for i in "${init_candidates[@]}"; do
			debug "checking if $i is started"
			if sv_is_running "$i"; then
				debug "found started service, adding init script: $i"
				INIT_SCRIPTS+=( "$i" )
			else
				debug "no other started init scripts found for that process"
			fi
		done
	else
		debug "does not belong to any package: $lu_exe"
	fi
	debug "adding to UNKNOWN_PROCESSES: $lu_exe"
	UNKNOWN_PROCESSES_EXE+=( ["$lu_pid"]="$lu_exe" )
	UNKNOWN_PROCESSES_PROC+=( ["$lu_pid"]="$lu_proc" )
	UNKNOWN_PROCESSES_ROOT+=( ["$lu_pid"]="$(readlink "/proc/$lu_pid/root")" )
done < <([[ ${#TODO_PROCESSES_EXE[@]} -gt 0 ]] && xargs -n1 <<<"${!TODO_PROCESSES_EXE[@]}")
printf "\bdone\n"

# cleanup list of init scripts
INIT_SCRIPTS=( $([[ ${#INIT_SCRIPTS[@]} -gt 0 ]] && printf '%s\0' "${INIT_SCRIPTS[@]}" | sort -uz | xargs -0n1) )

for I in "${INIT_SCRIPTS[@]}"; do
	spin
	SV="$(basename "$I")"
	# for systemd, we allow .service units to be specified in config without suffix (without .service)
	# so we add optional (\.service)? to every regex to support this
	case "$SV" in
		$(for re in "${SV_IGNORE[@]}"; do grep -E "^$re(\.service)?$" <<<"$SV" && break;done))
			FOUND_SV_IGNORE+=( "$SV" )
		;;
		$(for re in "${SV_CRITICAL_WITH_NODEPS[@]}"; do grep -E "^$re(\.service)?$" <<<"$SV" && break;done))
			FOUND_SV_CRITICAL_WITH_NODEPS+=( "$SV" )
		;;
		$(for re in "${SV_CRITICAL[@]}"; do grep -E "^$re(\.service)?$" <<<"$SV" && break;done))
			FOUND_SV_CRITICAL+=( "$SV" )
		;;
		$(for re in "${SV_ALWAYS_RELOAD_ONLY[@]}"; do grep -E "^$re(\.service)?$" <<<"$SV" && break;done))
			FOUND_SV_ALWAYS_RELOAD_ONLY+=( "$SV" )
		;;
		$(for re in "${SV_ALWAYS_WITH_NODEPS[@]}"; do grep -E "^$re(\.service)?$" <<<"$SV" && break;done))
			FOUND_SV_ALWAYS_WITH_NODEPS+=( "$SV" )
		;;
		$(for re in "${SV_ALWAYS[@]}"; do grep -E "^$re(\.service)?$" <<<"$SV" && break;done))
			FOUND_SV_ALWAYS+=( "$SV" )
		;;
		*)
			FOUND_SV_UNKNOWN+=( "$SV" )
		;;
	esac
done
while read lu_pid; do
	spin
	lu_exe=${INITTAB_PROCESSES[$lu_pid]}
	debug "checking if $lu_exe is a known inittab process"
	case $lu_exe in
		$(for re in "${INITTAB_KILLALL[@]}"; do grep -E "^$re$" <<<"$lu_exe" && break;done))
			FOUND_INITTAB_PROCESSES+=( ["$lu_pid"]="$lu_exe" )
			debug "'$lu_exe' added to FOUND_INITTAB_PROCESSES"
		;;
		*)
			FOUND_INITTAB_PROCESSES_UNKNOWN+=( ["$lu_pid"]="$lu_exe" )
			debug "'$lu_exe' added to FOUND_INITTAB_PROCESSES_UNKNOWN"
		;;
	esac
done < <([[ ${#INITTAB_PROCESSES[@]} -gt 0 ]] && printf '%s\0' "${!INITTAB_PROCESSES[@]}" | sort -uz | xargs -0n1)

spin_done

if [[ $INIT == systemd ]]; then
	if systemd_detect_require_daemon_reload; then
		debug "systemd: daemon-reload required"
		if [[ $LIST_ONLY ]]; then
			echo "$(white "=>") systemd: detected changed unit files, daemon-reload will be executed"
		else
			echo_sv_action green systemd "systemctl daemon-reload"
			systemctl daemon-reload
		fi
	else
		debug "systemd: daemon-reload NOT required"
	fi
fi

if [[ $LIST_ONLY ]]; then
	echo "$(white "=>") Found ${#INIT_SCRIPTS[*]} services that need to be restarted or reloaded"
fi

# process different classes of services we found
if [[ ${#FOUND_SV_ALWAYS_RELOAD_ONLY[@]} -gt 0 ]]; then
	if [[ $LIST_ONLY ]]; then
		echo "$(white "=->") class $(green SV_ALWAYS_RELOAD_ONLY) (${#FOUND_SV_ALWAYS_RELOAD_ONLY[*]}): ${FOUND_SV_ALWAYS_RELOAD_ONLY[*]}"
	else
		for SV in "${FOUND_SV_ALWAYS_RELOAD_ONLY[@]}"; do
			if get_confirmation "Reload %1" "$SV"; then
				echo_sv_action green "$SV" "Reloading..."
				sv_reload "$SV"
			fi
		done
	fi
fi
if [[ ${#FOUND_SV_ALWAYS_WITH_NODEPS[@]} -gt 0 ]]; then
	if [[ $LIST_ONLY ]]; then
		echo "$(white "=->") class $(green SV_ALWAYS_WITH_NODEPS) (${#FOUND_SV_ALWAYS_WITH_NODEPS[*]}): ${FOUND_SV_ALWAYS_WITH_NODEPS[*]}"
	else
		for SV in "${FOUND_SV_ALWAYS_WITH_NODEPS[@]}"; do
		if get_confirmation "Restart %1 without dependencies" "$SV"; then
			echo_sv_action green "$SV" "Restarting $(yellow without dependencies) ..."
			sv_restart "$SV" "nodeps"
		fi
		done
	fi
fi
if [[ ${#FOUND_SV_ALWAYS[@]} -gt 0 ]]; then
	if [[ $LIST_ONLY ]]; then
		echo "$(white "=->") class $(green SV_ALWAYS) (${#FOUND_SV_ALWAYS[*]}): ${FOUND_SV_ALWAYS[*]}"
	else
		for SV in "${FOUND_SV_ALWAYS[@]}"; do
			if get_confirmation "Restart %1" "$SV"; then
				echo_sv_action green "$SV" "Restarting..."
				sv_restart "$SV"
			fi
		done
	fi
fi
if [[ ${#FOUND_SV_CRITICAL[@]} -gt 0 ]]; then
	if [[ $LIST_ONLY ]]; then
		echo "$(white "=->") class $(red SV_CRITICAL) (${#FOUND_SV_CRITICAL[*]}): ${FOUND_SV_CRITICAL[*]}"
	elif [[ $CRITICAL ]]; then
		# restart only if forced by user (critical services)
		for SV in "${FOUND_SV_CRITICAL[@]}"; do
			if get_confirmation "Restart $(red critical) service %1" "$SV"; then
				echo_sv_action green "$SV" "$(yellow Restarting...)"
				sv_restart_critical "$SV"
			fi
		done
	else
		for SV in "${FOUND_SV_CRITICAL[@]}"; do
			echo_sv_action red "$SV" "Not restarting $(red critical) service (requires -c) ..."
		done
	fi
fi
if [[ ${#FOUND_SV_CRITICAL_WITH_NODEPS[@]} -gt 0 ]]; then
	if [[ $LIST_ONLY ]]; then
		echo "$(white "=->") class $(red SV_CRITICAL_WITH_NODEPS) (${#FOUND_SV_CRITICAL_WITH_NODEPS[*]}): ${FOUND_SV_CRITICAL_WITH_NODEPS[*]}"
	elif [[ $CRITICAL ]]; then
		# restart only if forced by user (critical services)
		for SV in "${FOUND_SV_CRITICAL_WITH_NODEPS[@]}"; do
			if get_confirmation "Restart $(red critical) service %1 without dependencies" "$SV"; then
				echo_sv_action green "$SV" "$(yellow Restarting without dependencies ...)"
				sv_restart_critical "$SV" "nodeps"
			fi
		done
	else
		for SV in "${FOUND_SV_CRITICAL_WITH_NODEPS[@]}"; do
			echo_sv_action red "$SV" "Not restarting $(red critical) service (requires -c) ..."
		done
	fi
fi
if [[ ${#FOUND_SV_UNKNOWN[@]} -gt 0 ]]; then
	if [[ $LIST_ONLY ]]; then
		echo "$(white "=->") $(yellow unclassified) services (${#FOUND_SV_UNKNOWN[*]}): ${FOUND_SV_UNKNOWN[*]}"
	elif [[ $FORCE_UNKNOWN ]]; then
		for SV in "${FOUND_SV_UNKNOWN[@]}"; do
			echo "$(yellow "-> Hint:") Classify service '$SV' by adding it to a service class in '$CNF'"
			if get_confirmation "Restart $(yellow unclassified) service %1" "$SV"; then
				echo_sv_action green "$SV" "$(yellow Restarting...)"
				sv_restart_critical "$SV"
			fi
		done
	else
		for SV in "${FOUND_SV_UNKNOWN[@]}"; do
			echo_sv_action red "$SV" "Not restarting $(yellow unclassified) service (requires -u) ..."
		done
	fi
fi
if [[ ${#FOUND_SV_IGNORE[@]} -gt 0 ]]; then
	if [[ $LIST_ONLY ]]; then
		echo "$(white "=->") class $(red SV_IGNORE) (${#FOUND_SV_IGNORE[*]}): ${FOUND_SV_IGNORE[*]}"
	fi
fi

if [[ ${#INIT_SCRIPTS} -eq 0 ]]; then
	echo "No known services need to be restarted."
fi

if [[ $LIST_ONLY ]]; then
	echo "$(white "=>") Found $((${#FOUND_INITTAB_PROCESSES[*]}+${#FOUND_INITTAB_PROCESSES_UNKNOWN[*]})) inittab processes that need to be restarted"
fi

# process different classes of services we found
if [[ ${#FOUND_INITTAB_PROCESSES[@]} -gt 0 ]]; then
	debug "processing inittab processes"
	if [[ $LIST_ONLY ]]; then
		echo -n "$(white "=->") class $(green INITTAB_KILLALL) (${#FOUND_INITTAB_PROCESSES[*]}): "
		for p in "${!FOUND_INITTAB_PROCESSES[@]}"; do
			echo -n "${FOUND_INITTAB_PROCESSES[$p]}($p) "
		done
		echo ""
	else
		for p in "${!FOUND_INITTAB_PROCESSES[@]}"; do
			exe="${FOUND_INITTAB_PROCESSES[$p]} ($p)"
			if get_confirmation "Kill respawn init processes %1" "$exe"; then
				echo_sv_action green "$exe" "Restarting..."
				if kill "$p"; then
					echo_sv_action green "$exe" "OK"
				else
					echo_sv_action red "$exe" "Failed"
				fi
			fi
		done
	fi
fi
if [[ ${#FOUND_INITTAB_PROCESSES_UNKNOWN[@]} -gt 0 ]]; then
	if [[ $LIST_ONLY ]]; then
		echo -n "$(white "=->") $(yellow unclassified) (${#FOUND_INITTAB_PROCESSES_UNKNOWN[*]}): "
		for p in "${!FOUND_INITTAB_PROCESSES_UNKNOWN[@]}"; do
			echo -n "${FOUND_INITTAB_PROCESSES_UNKNOWN[$p]}($p) "
		done
		echo ""
	elif [[ $FORCE_UNKNOWN ]]; then
		for p in "${!FOUND_INITTAB_PROCESSES_UNKNOWN[@]}"; do
			exe="${FOUND_INITTAB_PROCESSES_UNKNOWN[$p]} ($p)"
			if get_confirmation "Kill respawn init processes %1" "$exe"; then
				echo_sv_action green "$exe" "Killing..."
				if kill "$p"; then
					echo_sv_action green "$exe" "OK"
				else
					echo_sv_action red "$exe" "failed"
				fi
			fi
		done
	else
		for p in "${!FOUND_INITTAB_PROCESSES_UNKNOWN[@]}"; do
			exe="${FOUND_INITTAB_PROCESSES_UNKNOWN[$p]} ($p)"
			echo_sv_action red "$exe" "Not killing $(yellow unclassified) inittab process (requires -u) ..."
		done
	fi
fi

# cleanup UNKNOWN_PROCESSES: remove pids where we have the same process name in KNOWN_PROCESSES
debug "looking for false positives in unknown processes"
for pid in "${!UNKNOWN_PROCESSES_EXE[@]}"; do
	# skip unknown process if we know another pid of it
	uex="${UNKNOWN_PROCESSES_EXE[$pid]}"
	uep="${UNKNOWN_PROCESSES_PROC[$pid]}"
	debug "looking at unknown process $uex ($pid)"
	for k in "${KNOWN_PROCESSES_EXE[@]}"; do
		if [[ $k == "$uex" ]]; then
			debug "$uex is not unknown, removing from list"
			continue 2
		fi
	done
	REALLY_UNKNOWN_PROCESSES_PROC+=( ["$pid"]="$uep" )
	REALLY_UNKNOWN_PROCESSES_EXE+=( ["$pid"]="$uex" )
	REALLY_UNKNOWN_PROCESSES_ROOT+=( ["$pid"]="$(readlink "/proc/$pid/root")" )
done

# handle pid 1
if [[ ${REALLY_UNKNOWN_PROCESSES_EXE[1]} ]]; then
	debug "pid 1 needs to be restarted"
	if [[ $LIST_ONLY ]]; then
		echo "$(white "=>") Found pid 1 (${REALLY_UNKNOWN_PROCESSES_EXE[1]}) that needs to be restarted."
	else
		if get_confirmation "Restart %1" "${REALLY_UNKNOWN_PROCESSES_EXE[1]}"; then
			echo_sv_action green "${REALLY_UNKNOWN_PROCESSES_EXE[1]}" "Restarting..."
			telinit u
		fi
	fi
	unset REALLY_UNKNOWN_PROCESSES_EXE[1]
fi

if [[ ${#REALLY_UNKNOWN_PROCESSES_EXE[@]} -gt 0 ]]; then
	echo ""
	echo "$(yellow "-> Warning:") The following running processes did not match any known service but have been updated or deleted (or some deps)"
	echo "$(yellow "-> ") If any of those match a custom service you can configure a service manually using 'CUSTOM_PROCESS_MAP'"
	for pid in "${!REALLY_UNKNOWN_PROCESSES_EXE[@]}"; do
		if [[ $pid -eq 1 ]]; then
			continue
		fi
		owner="$(ps -o user= -p $pid)"
		echo "$(yellow "=> $pid"): " "$(white ${REALLY_UNKNOWN_PROCESSES_PROC[$pid]} "[")""exe=${REALLY_UNKNOWN_PROCESSES_EXE[$pid]}""$(white "]")"" (owner: $(white $owner), root: $(white ${REALLY_UNKNOWN_PROCESSES_ROOT[$pid]}))"
		if [[ $pid == $(top_level_ppid $$) ]]; then
			echo "  $(yellow "-> This is you! You may need to relogin/reconnect and/or restart your screen/tmux session to get rid of this")"
		fi
	done
fi
# EOF
