#!/usr/bin/env bash
#
# Auto-mount and unmount an operational chroot from an existing base root file-system
# Copyright 2014 TJ <hacker@iam.tj>
# Licensed on the terms of the GNU General Public License version 3
#

if [ -n "$DEBUG" ]; then
  DEBUG="echo debug: "
fi

if [ $# -lt 2 ]; then
  echo "usage: $0 [-f | --force] /path/to/mountpoint /dev/path/to/root-filesystem" >&2
  echo "       -f | --force create missing mountpoints" 2>&1
  exit 1
fi

quicksort_alphanum()
{
  local a=( "$@" )
  local -a l g e
  local x=

  if [ ${#a[@]} -lt 2 ]; then
    echo -n ${a[@]}
  else
    local pivot=${a[0]}
    for x in ${a[@]}; do
      if [[ "${x}" < "${pivot}" ]]; then
        l=( ${l[@]} $x )
      elif [[ "${x}" > "${pivot}" ]]; then
        g=( ${g[@]} $x )
      else
        e=( ${e[@]} $x )
      fi
    done
    echo "$(quicksort_alphanum "${l[@]}") ${e[@]} $(quicksort_alphanum "${g[@]}")"
  fi
}

TRUE=0
FALSE=1

# read command-line options
FORCE=FALSE
case "$1" in
  (--f|--force)
   FORCE=TRUE
   shift
esac

ROOTFS=$2
MOUNTPOINT=$1

# need to be root for mount and chroot commands
if [ $(id -u) -ne 0 ]; then
  echo "error: need superuser (root) privileges" >&2
  exit 2
fi

# ensure the file-system is a block special device
if [ ! -b "$ROOTFS" ]; then
  echo "error: no such device '$ROOTFS'" >&2
  exit 3
fi

# ensure no existing file-system is mounted at the same mount point
if mount | grep -q "$MOUNTPOINT "; then
  echo "error: some file-system already mounted at '$MOUNTPOINT'" >&2
  exit 4
fi

# ensure the host mountpoint exists
if [ "$FORCE" = "$TRUE" ]; then
  mkdir -p "$1"
elif [ ! -d "$1" ]; then
  echo "error: mountpoint doesn't exist '$1'" >&2
  exit 5
fi

# ensure the mountpoint is a directory
if [ -e "$MOUNTPOINT" -a  ! -d "$MOUNTPOINT" ]; then
  echo "error: mountpoint must be a directory" >&2
  exit 6
fi

# ensure the mountpoint directory is empty
if [ $(ls -a "$MOUNTPOINT" | wc -l) -ne 2 ]; then
  echo "error: mountpoint directory must be empty" >&2
  exit 7
fi

if ! mount "$ROOTFS" "$MOUNTPOINT"; then
  echo "error: failed to mount '$ROOTFS' at '$MOUNTPOINT'" >&2
  exit 8
fi

# mount the kernel file-systems
$DEBUG mount -t proc proc "$MOUNTPOINT/proc"
$DEBUG mount -t sysfs sysfs "$MOUNTPOINT/sys"
$DEBUG mount -t devtmpfs udev "$MOUNTPOINT/dev"
$DEBUG mount -t devpts devpts "$MOUNTPOINT/dev/pts"
# ensure the chroot can resolve DNS hosts
$DEBUG mount --bind /etc/resolv.conf "$MOUNTPOINT/etc/resolv.conf"

# create array listing active (auto-mounted) block special device file-system mountpoints
declare -a MP
if [ -r "$MOUNTPOINT/etc/fstab" ]; then
  echo "info: reading $MOUNTPOINT/etc/fstab" >&2
  while read filesystem mountpoint type options dump pass; do
   if [ -b "$filesystem" -a "$type" != "swap" -a "$mountpoint" != "/" ]; then
     if ! grep -q noauto < <(echo $options); then
       MP=( ${MP[@]} $mountpoint )
     fi
   fi
  done < <(egrep -v '^(#|$)' "$MOUNTPOINT/etc/fstab")
else
  echo "error: cannot read filesystem table in chroot '$MOUNTPOINT/etc/fstab'" >&2
  exit 9
fi

# sort mount point paths such that longer, possibly sub-mountpoints, always follow their (grand)parent mountpoints
# e.g. '/var' before '/var/cache' before '/var/cache/ccache'
MP=( $(quicksort_alphanum ${MP[@]}) )

# mount all active chroot file-systems *from inside* chroot
MP_COUNT=0
for mp in ${MP[@]}; do
  ((MP_COUNT++))
  if [ "$FORCE" = "$TRUE" ]; then
    $DEBUG mkdir -p "$MOUNTPOINT/$mp"
  fi
  if [ ! -d "$MOUNTPOINT/$mp" ]; then
    echo "warning: mountpoint does not exist in chroot '$mp'" >&2
    continue
  fi
  $DEBUG chroot "$MOUNTPOINT" mount "$mp"
done

# run an interactive chroot shell
echo "'$ROOTFS' now active at chroot '$MOUNTPOINT'"
echo "Entering interactive chroot shell session"
echo "Use 'exit' or Ctrl+D to terminate session and auto-unmount the chroot"

$DEBUG chroot "$MOUNTPOINT" $SHELL -i

echo "info: auto-unmounting chroot at '$MOUNTPOINT'" >&2

# unmount all chroot file-systems in reverse order (for correct sub-mountpoint handling)
while [ $MP_COUNT -gt 0 ]; do
  ((MP_COUNT--))
  $DEBUG chroot "$MOUNTPOINT" umount "${MP[$MP_COUNT]}"
done

$DEBUG umount "$MOUNTPOINT/etc/resolv.conf"
# unmount the kernel file-systems
$DEBUG umount "$MOUNTPOINT/dev/pts"
$DEBUG umount "$MOUNTPOINT/dev"
$DEBUG umount "$MOUNTPOINT/sys"
$DEBUG umount "$MOUNTPOINT/proc"
umount "$MOUNTPOINT"

echo "info: chroot unmounted" >&2
exit 0

