#!/bin/dash

# THIS SCRIPT IS NOT MEANT TO BE RUN MANUALLY.
# 
# Pre-seed initialization script to vdevd.
# Populate /dev with extra files that vdevd won't create, but are 
# necessary for correct operation.
# Write to stdout any "$type $name $major $minor $PARAM=$VALUE..." strings for
# devices that vdevd should process.
#   $type is "c" or "b" for char or block devices, respectively 
#   $name is the relative path to the device 
#   $major is the major device number (pass 0 if not needed)
#   $minor is the minor device number (pass 0 if not needed)
#   $PARAM=$VALUE is an OS-specific parameter to be passed as a
#      VDEV_OS_${PARAM} environment variable to vdev helpers
#
# It is okay to create device nodes here, but be sure to output the above information
# for it as well (so vdevd knows to run actions for it, instead of stumble over it).
# 
# Requires /proc and /sys to be mounted
#
# $1 — VDEV mountpoint; $2 — vdev config path (passed from vdev_preseed_run()).

VDEV_MOUNTPOINT="$1"
VDEV_CONFIG_FILE="$2"

VDEV_EVENTFS_CONF="${VDEV_EVENTFS_CONF:-/etc/eventfs/eventfs.conf}"
VDEV_EVENTFS_BIN="${VDEV_EVENTFS_BIN:-/bin/eventfs}"

eventfs_serial_mp() {
   printf '%s/metadata/udev/events/serial' "$VDEV_MOUNTPOINT"
}

eventfs_prepare_udev_layout() {
   dirp=

   umask 022

   for dirp in \
      metadata/udev/tags \
      metadata/udev/data \
      metadata/udev/links \
      metadata/udev/events/global \
      metadata/udev/events/serial; do
      test -d "$VDEV_MOUNTPOINT/$dirp" ||
         /bin/mkdir -m 0755 -p "$VDEV_MOUNTPOINT/$dirp"
   done

   /bin/chmod 0750 "$VDEV_MOUNTPOINT/metadata/udev/events" 2>/dev/null || true
   /bin/chmod 0750 "$VDEV_MOUNTPOINT/metadata/udev/events/global" 2>/dev/null || true

   if [ ! -e /run/udev ]; then
      /bin/ln -sf "$VDEV_MOUNTPOINT/metadata/udev" /run/udev
   fi
}

# Kernels / libc may report eventfs as fuse, fuseblk, or fuse.* (stat -f '%T').
# Devuan Excalibur returns fuse only; treating that as "not mounted" remounts on
# top of a live queue and corrupts eventfs.
eventfs_fstype_ok() {
   case "$1" in
   fuse|fuseblk|fuse.*) return 0 ;;
   esac
   return 1
}

eventfs_is_healthy() {
   mp=
   fstype=

   mp="$(eventfs_serial_mp)"
   fstype="$(/bin/stat -f -c '%T' "$mp" 2>/dev/null)" || return 1
   eventfs_fstype_ok "$fstype" || return 1
   /bin/ls "$mp" >/dev/null 2>&1 || return 1
   return 0
}

eventfs_wait_healthy() {
   i=0

   while [ "$i" -lt 50 ]; do
      eventfs_is_healthy && return 0
      sleep 0.1
      i=$((i + 1))
   done
   return 1
}

eventfs_serial_perms() {
   mp=
   events=

   eventfs_is_healthy || return 0

   mp="$(eventfs_serial_mp)"
   events="$VDEV_MOUNTPOINT/metadata/udev/events"

   /bin/chmod 1777 "$mp" 2>/dev/null || true
   /bin/chmod -R 777 "$mp" 2>/dev/null || true
   /bin/chmod -R 777 "$events" 2>/dev/null || true
}

eventfs_mount() {
   mp=

   eventfs_prepare_udev_layout

   if eventfs_is_healthy; then
      eventfs_serial_perms
      return 0
   fi

   # vdev stop/restart must not tear down an existing FUSE mount.
   if /bin/mountpoint -q "$(eventfs_serial_mp)" 2>/dev/null; then
      echo "WARN: eventfs serial queue mount exists but is not healthy; not remounting" \
         > /proc/self/fd/2
      return 1
   fi

   if [ ! -r "$VDEV_EVENTFS_CONF" ]; then
      echo "WARN: missing $VDEV_EVENTFS_CONF" > /proc/self/fd/2
      return 1
   fi

   /sbin/modprobe fuse 2>/dev/null || true

   mp="$(eventfs_serial_mp)"
   if command -v setsid >/dev/null 2>&1; then
      setsid "$VDEV_EVENTFS_BIN" -o nonempty,allow_other -c "$VDEV_EVENTFS_CONF" "$mp" \
         </dev/null >/dev/null 2>&1 &
   else
      "$VDEV_EVENTFS_BIN" -o nonempty,allow_other -c "$VDEV_EVENTFS_CONF" "$mp" \
         </dev/null >/dev/null 2>&1 &
   fi

   eventfs_wait_healthy || return 1
   eventfs_serial_perms
   return 0
}

# mount the squashfs hardware database.
# generate the loopback device file needed to do so--create it, and inform vdevd to set it up.
# $1    hardware database file
# $2    the name of the loopback device to create
# TODO: exponential back-off, in case we race
attach_hwdb() {

   local _HWDB_PATH _HWDB_LOOP _LOOP_MINOR _RC _CREATED _MOUNT_TYPE
   
   _HWDB_PATH="$1"
   _HWDB_LOOP="$2"

   # is the hwdb already attached?
   if [ -n "$(/bin/cat /proc/mounts | /bin/grep "$VDEV_MOUNTPOINT/metadata/hwdb")" ]; then 
   
      # already mounted 
      return 0
   fi

   # deduce minor number 
   _LOOP_MINOR="${_HWDB_LOOP##loop}"
   
   _RC=0
   _CREATED=0

   # attempt to mount the hardware database
   if ! [ -b "$VDEV_MOUNTPOINT/$_HWDB_LOOP" ]; then 
      /bin/mknod "$VDEV_MOUNTPOINT/$_HWDB_LOOP" b 7 $_LOOP_MINOR
      _RC=$?

      if [ $_RC -ne 0 ]; then 
         return $_RC
      fi 

      _CREATED=1
   fi
   
   /sbin/losetup "$VDEV_MOUNTPOINT/$_HWDB_LOOP" "$_HWDB_PATH"
   _RC=$?
   
   if [ $_RC -ne 0 ]; then 
      
      if [ $_CREATED -ne 0 ]; then 
         /bin/rm -f "$VDEV_MOUNTPOINT/$_HWDB_LOOP"
      fi

      return $_RC
   fi 
   
   /bin/mount -t squashfs "$VDEV_MOUNTPOINT/$_HWDB_LOOP" "$VDEV_MOUNTPOINT/metadata/hwdb"
   _RC=$?
   
   if [ $_RC -ne 0 ]; then 
      
      /sbin/losetup -d "$VDEV_MOUNTPOINT/$_HWDB_LOOP"

      if [ $_CREATED -ne 0 ]; then 
         /bin/rm -f "$VDEV_MOUNTPOINT/$_HWDB_LOOP"
      fi

      return $_RC
   fi 

   # make sure vdevd still processes the loop device
   echo "b $_HWDB_LOOP 7 $_LOOP_MINOR"

   return 0
}


# guess the subsystem if /sys/dev/{block,char}/$major:$minor isn't helpful.
# print it to stdout
# $1  type of device (c for char, b for block)
# $2  name of device relative to /dev
guess_subsystem() {

   local devtype=$1
   local devname=$2

   if [ "$devname" = "snd/seq" ]; then 
      echo "sound"
   elif [ "$devname" == "snd/timer" ]; then 
      echo "sound"
   fi
}


# feed ALSA sequencer/timer static nodes (modules.devname: snd/seq, snd/timer).
# Always inform vdevd so permissions/metadata are applied even if devtmpfs already
# created the node.
feed_alsa_static_nodes() {

   local _MODDEVNAME _DEVNAME _TYPEMAJMIN _MAJOR _MINOR _FULLPATH

   [ -e /lib/modules/$(uname -r)/modules.devname ] || return 0

   test -d "$VDEV_MOUNTPOINT/snd" || /bin/mkdir -p "$VDEV_MOUNTPOINT/snd"

   /bin/grep -E '[[:space:]]snd/(seq|timer)[[:space:]]' /lib/modules/$(uname -r)/modules.devname | \
   while read _MODDEVNAME _DEVNAME _TYPEMAJMIN; do

      _MAJOR=$(echo "$_TYPEMAJMIN" | /bin/sed 's/^c//; s/:.*//')
      _MINOR=$(echo "$_TYPEMAJMIN" | /bin/sed 's/^c[0-9]\+://')

      [ -n "$_MAJOR" ] && [ -n "$_MINOR" ] || continue

      _FULLPATH="$VDEV_MOUNTPOINT/$_DEVNAME"

      if ! [ -e "$_FULLPATH" ]; then
         /bin/mknod "$_FULLPATH" c "$_MAJOR" "$_MINOR"
         /bin/chown root:audio "$_FULLPATH" 2>/dev/null
         /bin/chmod 0660 "$_FULLPATH"
      fi

      echo "c $_DEVNAME $_MAJOR $_MINOR SUBSYSTEM=sound"
   done
}


# feed static device nodes into vdevd.
# use kmod to query all device files for modules for the currently-running kernel version.
# see kmod(8)
feed_static_nodes_kmod() {

  [ -e /lib/modules/$(uname -r)/modules.devname ] || return 0
  [ -x /bin/kmod ] || return 0

  /bin/kmod static-nodes --format=tmpfiles --output=/proc/self/fd/1 | \
  while read type name mode uid gid age arg; do

    # strip /dev/ from $name
    name=${name#/dev/}
   
    # strip bang from $type 
    type=${type%%\!}

    # snd/* nodes are always fed to vdevd (permissions, metadata) even when present
    case "$name" in
    snd/*) ;;
    *) [ -e "$VDEV_MOUNTPOINT/$name" ] && continue ;;
    esac

    # skip directories--vdevd can make them 
    if [ "$type" = "d" ]; then 
       continue
    fi

    # only process character and block devices 
    if [ "$type" != "c" -a "$type" != "b" ]; then 
       echo "Invalid device type $type" >/proc/self/fd/2
       continue
    fi

    # look up major/minor 
    major=$(echo $arg | /bin/sed 's/^\([^:]\+\):.*/\1/g')
    minor=$(echo $arg | /bin/sed 's/^[^:]\+:\(.*\)/\1/g')

    # look up devpath and subsystem
    sys_link=
    devpath=
    params=
    case "$type" in
      "c")

         sys_link="/sys/dev/char/$major:$minor"
         ;;

      "b")
         
         sys_link="/sys/dev/block/$major:$minor"
         ;;
    esac

    # is there a sysfs entry for this device?  there might not be...
    if ! [ -L $sys_link ]; then 

      subsystem=$(guess_subsystem $type $name)
    
    else

      devpath=$(/bin/readlink "$sys_link" | /bin/sed 's/\.\.\///g')
      devpath="/$devpath"

      subsystem=$(/bin/readlink "/sys/$devpath/subsystem" | /bin/sed "s/[^/]*\///g")
    fi

    # build up params list 
    if [ -n "$devpath" ]; then 

       params="$params DEVPATH=$devpath"
    fi 

    if [ -n "$subsystem" ]; then 

       params="$params SUBSYSTEM=$subsystem"
    fi

    # feed into vdevd via stdout
    echo "$type $name $major $minor $params"
    
    # keep SELinux happy
    if [ -x /sbin/restorecon ]; then

      /sbin/restorecon "$VDEV_MOUNTPOINT/$name"
    fi
  done
}


umask 022

# add /dev/fd
test -L "$VDEV_MOUNTPOINT/fd" || /bin/ln -sf /proc/self/fd "$VDEV_MOUNTPOINT/fd"

# add /dev/core 
test -L "$VDEV_MOUNTPOINT/core" || /bin/ln -sf /proc/kcore "$VDEV_MOUNTPOINT/core"

# feed /dev/null into vdev, and make it locally
if ! [ -c "$VDEV_MOUNTPOINT/null" ]; then 
   echo "c null 1 3"
   /bin/mknod "$VDEV_MOUNTPOINT/null" c 1 3
   /bin/chmod 0666 "$VDEV_MOUNTPOINT/null"
fi

# add /dev/stdin, /dev/stdout, /dev/stderr
test -L "$VDEV_MOUNTPOINT/stdin" || /bin/ln -sf /proc/self/fd/0 "$VDEV_MOUNTPOINT/stdin"
test -L "$VDEV_MOUNTPOINT/stdout" || /bin/ln -sf /proc/self/fd/1 "$VDEV_MOUNTPOINT/stdout"
test -L "$VDEV_MOUNTPOINT/stderr" || /bin/ln -sf /proc/self/fd/2 "$VDEV_MOUNTPOINT/stderr"

# add MAKEDEV
if ! [ -e "$VDEV_MOUNTPOINT/MAKEDEV" ]; then 
   if [ -e /sbin/MAKEDEV ]; then
      /bin/ln -sf /sbin/MAKEDEV "$VDEV_MOUNTPOINT/MAKEDEV"
   else
      /bin/ln -sf /bin/true "$VDEV_MOUNTPOINT/MAKEDEV"
   fi
fi

# add /dev/shm
test -d "$VDEV_MOUNTPOINT/shm" || /bin/ln -sf /run/shm "$VDEV_MOUNTPOINT/shm"

# add events 
for dirp in "events" "events/global"; do
   test -d "$VDEV_MOUNTPOINT/$dirp" || /bin/mkdir -p "$VDEV_MOUNTPOINT/$dirp"
done

/bin/chmod 1777 "$VDEV_MOUNTPOINT/events"
/bin/chmod 0750 "$VDEV_MOUNTPOINT/events/global"

# add vdevd's metadata directories 
for dirp in "metadata/dev" "metadata/hwdb"; do
   test -d "$VDEV_MOUNTPOINT/$dirp" || /bin/mkdir -m 0755 -p "$VDEV_MOUNTPOINT/$dirp"
done

# libudev-compat event queue (eventfs on metadata/udev/events/serial).
eventfs_mount || echo "WARN: eventfs serial queue not available" > /proc/self/fd/2

# attach the hardware database, if we have one
HWDB_VAR="$(/bin/fgrep "hwdb=" "$VDEV_CONFIG_FILE")"
LOOP_VAR="$(/bin/fgrep "hwdb_loop=" "$VDEV_CONFIG_FILE" | /bin/egrep "=loop[0-9]+$")"

VDEV_HWDB_PATH=
LOOP_NAME=

if [ -n "$HWDB_VAR" ]; then 
   eval "$HWDB_VAR"
   VDEV_HWDB_PATH="$hwdb"
fi

if [ -n "$LOOP_VAR" ]; then 
   eval "$LOOP_VAR"
   LOOP_NAME="$hwdb_loop"
fi

if [ -z "$LOOP_NAME" ]; then 
   # pick the next free one 
   # TODO: exponential back-off, in case we race
   LOOP_NAME="$(/sbin/losetup -f | /bin/sed "s/[^/]*\///g")"
fi 

if [ -n "$VDEV_HWDB_PATH" ] && [ -x /sbin/losetup ]; then 
   
   attach_hwdb "$VDEV_HWDB_PATH" "$LOOP_NAME"
   RC=$?
   if [ $RC -ne 0 ]; then 
      echo "WARN: Failed to mount hardware database" > /proc/self/fd/2
   fi
fi

feed_alsa_static_nodes
feed_static_nodes_kmod

exit 0

