#!/bin/bash

# Written by Jason Mehring (nrgaway@gmail.com)
# Modified by Patrick Schleizer (adrelanos@whonix.org)

#set -x
set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose
export LC_ALL=C

true "$0 INFO: start"

if [ "${EUID}" != "0" ]; then
   printf '%s\n' "$0: ERROR: This MUST be run as root (sudo)!" >&2
   exit 1
fi

MYDIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" && pwd )"

## Skip a leading '--' end-of-options separator if present.
[ "${1:-}" = '--' ] && shift

file_system_object="${1:-}"

if [ "${file_system_object:-}" = "" ]; then
   printf '%s\n' "$0: ERROR: no parameter given!" >&2
   exit 1
fi

if [ "${file_system_object:-}" = "/" ]; then
   printf '%s\n' "$0: ERROR: file_system_object is set to / which is probably wrong (would kill all processes including this script)!" >&2
   exit 1
fi

if ! test -e "${file_system_object}" ; then
   true "$0: INFO: file_system_object does not exist. Skip checking if processes are running there, ok."
   true "$0: INFO: end"
   exit 0
fi

real_path=$(realpath -- "${file_system_object}") || true

if [ "${real_path:-}" = "" ] || [ "${real_path:-}" = "/" ]; then
   printf '%s\n' "$0: ERROR: could not canonicalize file_system_object '${file_system_object}' (realpath returned '${real_path:-}')." >&2
   exit 1
fi

if [ "${file_system_object:-}" = "${real_path}" ]; then
   true "INFO: file_system_object = real_path, ok."
else
   if test -L "${file_system_object}" ; then
      true "INFO: symlink"
   else
      printf '%s\n' "INFO: real_path: '${real_path}'"
      printf '%s\n' "INFO: file_system_object: '${file_system_object}'"
      printf '%s\n' "WARNING: file_system_object is different from real_path!" >&2
   fi
fi

skip_name_list="pts dev proc sys hostname resolv.conf hosts"

base_name="${file_system_object##*/}"

for skip_name_item in ${skip_name_list} ; do
   if [ "${base_name:-}" = "${skip_name_item}" ]; then
      ## Most likely just mounted host /dev in chroot can be ignored.
      ## Would otherwise show a long, confusing lsof.
      true "$0: INFO: base_name: '${skip_name_item}'. Skip checking if processes are running there, ok."
      true "$0: INFO: end"
      exit 0
   fi
done

true "INFO: Checking if there are any processes still running in file_system_object: '${file_system_object}'"

## lsof only shows processes that have a file open, but we care about a lot
## more cases:
##
## * open files
## * mmap'd files
## * files providing a running binary
## * chroot'ed dirs
## * dirs with a process whose current working directory is inside them
##
## Therefore, using a more complex scanning mechanism.
maps_file_matches() {
   local maps_file maps_address maps_perms maps_offset maps_dev maps_inode maps_pathname

   maps_file="${1}"
   while read -r maps_address maps_perms maps_offset maps_dev maps_inode maps_pathname; do
      [ -n "${maps_pathname}" ] || continue
      maps_pathname="${maps_pathname% (deleted)}"
      case "${maps_pathname}" in
         "${real_path}"|"${real_path}"/*)
            return 0
            ;;
      esac
   done < "${maps_file}"
   return 1
}

pids_using_path() {
   local proc_entry proc_pid link_target_lines link_target

   for proc_entry in /proc/[0-9]*; do
      proc_pid="${proc_entry##*/}"
      ## Never list this script itself or its invoking parent ('sudo').
      if [ "${proc_pid}" = "$$" ] || [ "${proc_pid}" = "${PPID}" ]; then
         continue
      fi
      link_target_lines="$(readlink -- "${proc_entry}/root" "${proc_entry}/cwd" "${proc_entry}/exe" "${proc_entry}"/fd/* 2>/dev/null)" || true
      while IFS="" read -r link_target; do
         case "${link_target}" in
            "${real_path}"|"${real_path}"/*)
               printf '%s\n' "${proc_pid}"
               continue 2
               ;;
         esac
      done <<< "${link_target_lines}"
      ## mmaps don't necessarily appear in the above list of links.
      ##
      ## grep acts as a quick scan before doing a more careful one. We could
      ## use grep alone here, but we would have to escape the path string,
      ## which is fragile and potentially dangerous.
      if grep --fixed-strings -- "${real_path}" "${proc_entry}/maps" >/dev/null 2>&1; then
         if maps_file_matches "${proc_entry}/maps"; then
            printf '%s\n' "${proc_pid}"
         fi
      fi
   done
}

pids="$(pids_using_path)"

if [ "${pids:-}" = "" ]; then
   true "INFO: Okay, no pids still running in '${file_system_object}', no need to kill any."
else
   printf '%s\n' "INFO: Okay, the following pids are still running inside '${file_system_object}', which will now be killed."

   ## Debugging.
   ## Overwrite with '|| true' to avoid race condition if these processes already
   ## terminated themselves.
   # shellcheck disable=SC2086
   ps -p ${pids} || printf '%s\n' "WARNING: Command 'ps -p ${pids}' exited non-zero." >&2

   ## SIGTERM first, allows applications to shut down gracefully.
   # shellcheck disable=SC2086
   kill -s TERM -- ${pids} || printf '%s\n' "WARNING: Command 'kill -s TERM -- ${pids}' exited non-zero." >&2

   ## NOTE: This will likely take longer than 5 seconds, the PID scanning
   ## process takes some time each time through the loop.
   grace_seconds_left=5
   pids="$(pids_using_path)"
   while [ ! "${pids}" = "" ] && [ "${grace_seconds_left}" -gt 0 ]; do
      sleep 1
      grace_seconds_left=$((grace_seconds_left - 1))
      pids="$(pids_using_path)"
   done

   if [ ! "${pids}" = "" ]; then
      printf '%s\n' "WARNING: The following pids survived SIGTERM and the grace period, sending SIGKILL: ${pids}" >&2
      # shellcheck disable=SC2086
      kill -s KILL -- ${pids} || printf '%s\n' "WARNING: Command 'kill -s KILL -- ${pids}' exited non-zero." >&2
      ## Killing processes is not instant; give the kernel a moment before
      ## callers proceed to unmount / delete the tree.
      sleep 3
   fi
fi

## Unmount all submounts. All callers consider it an error for any submount
## unmount to fail.
"${MYDIR}"/unmount-tree -- "${file_system_object}"

true "$0 INFO: end"
