#!/bin/sh -e
### BEGIN INIT INFO
# Provides:          vdev udev
# Required-Start:    mountkernfs vdev-modprobe.sh
# Required-Stop:     $local_fs
# Default-Start:     S
# Default-Stop:      0 6
# Short-Description: Start vdevd, populate /dev and load drivers.
# Description:	     Manages the /dev directory tree
### END INIT INFO

PATH="/sbin:/bin:/usr/bin"

# Avoid any attempt to kill vdev by the shebang operator /lib/runit/invoke-run
# specified at the beginning of the runit run script
if [ -d /proc/1/. ] && 
   [ "$(cat /proc/1/comm)" = 'runit' ] && 
   [ "$(echo $@ | cut -d' ' -f1,2)" = "stop --force-sysv" ]; then exit 0
fi

SCRIPTNAME=$0

# fill defaults 
if [ -z "$VDEV_BIN" ]; then 
    VDEV_BIN=/sbin/vdevd
fi

[ -x $VDEV_BIN ] || exit 0

if [ -z "$VDEV_MOUNTPOINT" ]; then
    VDEV_MOUNTPOINT=/dev
fi

# Top priority: find out the JSON file
# If there is no environment variable, we search the system's default path
_json="${VDEV_GLOBALS_JSON:-/etc/vdev/vdevd.globals.json}"

if [ -r "$_json" ] && command -v jq >/dev/null 2>&1; then
    # Clean up comments // and also /* */
	VDEV_JSON_DATA="$(sed -E 's|//.*||g' "$_json" |
        sed 's|/\*.*\*/||g' |
        grep -v '^$' |
        jq -r .)"
    export VDEV_JSON_DATA
else
    # Fallback if there is no JSON
    VDEV_SYSFS="/sys"
    VDEV_CONFIG_DIR="/etc/vdev"
    VDEV_HELPERS_DIR="/lib/vdev"
    VDEV_EARLY="vdevd.1.conf"
    VDEV_EARLY_RELOAD="vdevd.2.conf"
    VDEV_LONG_RUNNING="vdevd.conf"
    VDEV_MODPROBE_STAMP="/run/vdev/vdevd-0-coldplug.ready"
    VDEV_EARLY_STAMP="/run/vdev/vdevd-1-coldplug.ready"
    VDEV_MODPROBE="vdevd.0.conf"
    VDEV_INITRD=""
fi

get_json_get_key() {
    printf '%s' "$VDEV_JSON_DATA" | jq -r --arg k "$1" '
      .globals[$k] // empty'
}

get_json_get_config() {
    printf '%s' "$VDEV_JSON_DATA" | jq -r --arg s "$1" '
      .states[$s].config // empty'
}

get_json_get_reload_by_config() {
    printf '%s' "$VDEV_JSON_DATA" | jq -r --arg c "$1" '
      .states[] | select(.config == $c) | .reload.config // empty'
}

get_json_get_stamp() {
    printf '%s' "$VDEV_JSON_DATA" | jq -r --arg s "$1" '
      .states[$s].stamp // empty'
}

get_json_get_stamp_by_config() {
    printf '%s' "$VDEV_JSON_DATA" | jq -r --arg c "$1" '
      .states[] | 
      if .config == $c then .stamp 
      elif .reload.config == $c then .reload.stamp 
      else empty end' | head -n 1
}

if [ -n "$VDEV_JSON_DATA" ]; then
   
    _sysfs="$(get_json_get_key "sysfs")"
    VDEV_SYSFS="${_sysfs:-/sys}"

    _config_dir="$(get_json_get_key "config-dir")"
    VDEV_CONFIG_DIR="${_config_dir:-/etc/vdev}"
    
    _helpers_dir="$(get_json_get_key "helpers-dir")"
    VDEV_HELPERS_DIR="${_helpers_dir:-/lib/vdev}"

    _early="$(get_json_get_config "early")"
    VDEV_EARLY="${_early:-vdevd.1.conf}"
    
    _early_reload="$(get_json_get_reload_by_config "$_early")"
    VDEV_EARLY_RELOAD="${_early_reload:-vdevd.2.conf}"

    _long_running="$(get_json_get_config "long-running")"
    VDEV_LONG_RUNNING="${_long_running:-vdevd.conf}"
    
    _stamp_dir="$(get_json_get_key "states-stamp-dir")"
    VDEV_STAMP_DIR="${_stamp_dir:-/run/vdev}"

    _stamp_modprobe="$(get_json_get_stamp "modprobe")"
    VDEV_MODPROBE_STAMP="${VDEV_STAMP_DIR}/${_stamp_modprobe:-vdevd-0-coldplug.ready}"

    _stamp_early="$(get_json_get_stamp "early")"
    VDEV_EARLY_STAMP="${VDEV_STAMP_DIR}/${_stamp_early:-vdevd-1-coldplug.ready}"

    _modprobe_cfg="$(get_json_get_config "modprobe")"
    VDEV_MODPROBE="${_modprobe_cfg:-vdevd.0.conf}"

    _initrd_cfg="$(get_json_get_config "initrd")"
    VDEV_INITRD="${_initrd_cfg:-}"
fi

if [ -e "$VDEV_MODPROBE_STAMP" ] && [ -e "$VDEV_EARLY_STAMP" ]; then
    VDEV_CONFIG="$VDEV_CONFIG_DIR"/"$VDEV_LONG_RUNNING"
else
    VDEV_CONFIG="$VDEV_CONFIG_DIR"/"$VDEV_EARLY"
fi

# defaults
tmpfs_size="10M"

. /lib/lsb/init-functions

# load an ini file as a set of namespaced environment variables, echoing them to stdout
# $1 is the path to the file 
# $2 is the namespace to be prepended to each variable name 
source_ini_file() {

    local FILE_PATH NAMESPACE line KEY VALUE
   
    FILE_PATH=$1
    NAMESPACE=$2
   
    /bin/cat $FILE_PATH | /bin/sed "s/.*\[.*\].*//g" | \
    while read line; do 
   
        KEY=$(echo $line | /bin/sed "s/\(^.*\)=.*/\1/g");
        VALUE=$(echo $line | /bin/sed "s/^.*=\(.*\)$/\1/g");
      
        if [ -n "$KEY" ]; then 
      
            echo "${NAMESPACE}${KEY}=${VALUE}"
        fi
    done
}

# system-wide vdevd needs /sys to be mounted
if [ ! -d /sys/class/ ]; then
    log_failure_msg "vdev requires a mounted sysfs, not started"
    log_end_msg 1
fi

# system-wide vdevd needs "modern" sysfs
if [ -d /sys/class/mem/null -a ! -L /sys/class/mem/null ] || \
   [ -e /sys/block -a ! -e /sys/class/block ]; then
    log_warning_msg "CONFIG_SYSFS_DEPRECATED must not be selected"
    log_warning_msg "Booting will continue in 30 seconds but many things will be broken"
    sleep 30
fi

# load vdev config variables as vdev_config_*
eval $(source_ini_file $VDEV_CONFIG "vdev_config_")

# config sanity check 
if [ -z "$vdev_config_pidfile" ]; then 
    log_warning_msg "No PID file defined in $VDEV_CONFIG.  Please set the pidfile= directive."
    log_warning_msg "You will be unable stop or restart vdevd with this script."
fi

# Bounded poll until a stamp path exists (coldplug / os-queue-tracking markers).
# $1 = stamp path; $2 = max iterations (default 25); $3 = sleep seconds per step (default 0.2).
# Returns 0 if the stamp appeared, 1 on timeout.
vdev_wait_for_stamp() {
    _vs_path="$1"
    _vs_max="${2:-250}"
    _vs_sleep="${3:-0.2}"
    _vs_i=0
    while [ "$_vs_i" -lt "$_vs_max" ]; do
        [ -e "$_vs_path" ] && return 0
        sleep "$_vs_sleep"
        _vs_i=$((_vs_i + 1))
    done
    return 1
}

subshell() {
	
    # vdevd.0: wait for os-queue-tracking stamp (VDEV_OS_QUEUE_TRACKING_FLAG=1).
    vdev_wait_for_stamp "$VDEV_MODPROBE_STAMP" || true

    # vdevd.1: wait for os-queue-tracking stamp (VDEV_OS_QUEUE_TRACKING_FLAG=2).
    vdev_wait_for_stamp "$VDEV_EARLY_STAMP" || true
    
    if [ -d /proc/1/. ] && [ "$(cat /proc/1/comm)" != 'runit' ] ; then
        # Final mutation: the subshell disappears.
        # vdevd.1 (mixed) must keep running until it yields netlink to this
        # hotplug_only instance.
        unset VDEV_JSON_STATE 2>/dev/null || true
        exec "$VDEV_BIN" -H -c "$VDEV_CONFIG_DIR"/"$VDEV_LONG_RUNNING" "$VDEV_MOUNTPOINT"
    fi
}

vdevd_start() {

    if [ -e "$VDEV_MODPROBE_STAMP" ] && [ -e "$VDEV_EARLY_STAMP" ]; then
        VDEV_START_CONFIG="$VDEV_CONFIG_DIR"/"$VDEV_LONG_RUNNING"
    else
        VDEV_START_CONFIG="$VDEV_CONFIG_DIR"/"$VDEV_EARLY"
    fi

    if [ "$VDEV_START_CONFIG" != "$VDEV_CONFIG" ]; then
        eval $(source_ini_file "$VDEV_START_CONFIG" "vdev_config_")
    fi

    if [ "$(basename -- "$VDEV_START_CONFIG")" = "$VDEV_EARLY" ]; then
        rm -f "$VDEV_EARLY_STAMP" 2>/dev/null || true
    fi

    # clear uevent helper--vdevd subsumes its role
    if [ -w /sys/kernel/uevent_helper ]; then
        echo > /sys/kernel/uevent_helper
    fi
   
    # set the SELinux context for devices created in the initramfs
    [ -x /sbin/restorecon ] && /sbin/restorecon -R "$VDEV_MOUNTPOINT"
   
    # make sure log directory exists...
    if [ -n "$vdev_config_logfile" ]; then 
      
        vdev_log_dir="$(echo "$vdev_config_logfile" | sed 's/[^/]\+$//g')"

        if [ -n "$vdev_log_dir" ]; then 
            mkdir -p "$vdev_log_dir"
        fi
    fi

    # make sure the pid directory exists 
    if [ -n "$vdev_config_pidfile" ]; then

        vdev_pid_dir="$(echo "$vdev_config_pidfile" | sed 's/[^/]\+$//g')"

        if [ -n "$vdev_pid_dir" ]; then 
            mkdir -p "$vdev_pid_dir"
        fi
    fi
   
    # When states.modprobe and states.early share the same config basename, vdevd
    # uses VDEV_JSON_STATE to pick the sysfs subsystem queue (see linux_sysfs.inc).
    if [ "$(basename -- "$VDEV_START_CONFIG")" = "$VDEV_LONG_RUNNING" ]; then
        unset VDEV_JSON_STATE 2>/dev/null || true
    else
        export VDEV_JSON_STATE=early
    fi
    
    # vdevd.0 coldplug: wait for os-queue-tracking stamp (VDEV_OS_QUEUE_TRACKING_FLAG=1).
    vdev_wait_for_stamp "$VDEV_MODPROBE_STAMP" || true

    # Second synchronous modprobe-phase coldplug (same INI as vdev-modprobe, typically
    # vdevd.0.conf): drop the stamp and run vdevd.0 again so devices that appear only
    # after the first sysfs walk (firmware, composite USB, re-enumeration) still get
    # a full modprobe pass before the mixed early vdevd.1 instance starts. Site policy
    # often narrows the modprobe-phase subsystem list (e.g. omitting pci) and loads
    # KMS (i915/nouveau) in vdev-modprobe.sh to reduce races with the first pass.
    export VDEV_JSON_STATE=modprobe
    rm -f "$VDEV_MODPROBE_STAMP"
    /sbin/vdevd -c "$VDEV_CONFIG_DIR/$VDEV_MODPROBE" "$VDEV_MOUNTPOINT"
    
    log_daemon_msg "Starting the system device event dispatcher" "vdevd\n"
    
    # start vdev
    "$VDEV_BIN" -c "$VDEV_START_CONFIG" $@ "$VDEV_MOUNTPOINT"

    if [ "$(basename -- "$VDEV_START_CONFIG")" = "$VDEV_EARLY" ]; then
        ( subshell ) &
    fi
}

case "$1" in
   start)
      vdevd_start
      ;;
   stop)
      "$VDEV_BIN" -k
      ;;
   restart)
      "$VDEV_BIN" -k
      sleep 1
      vdevd_start
      ;;
   status)
      _vdev_peers="$("$VDEV_BIN" -k --dry-run 2>/dev/null)"
      if [ -n "$_vdev_peers" ]; then
         _peer_list="$(echo "$_vdev_peers" | tr '\n' ' ' | sed 's/[[:space:]]*$//')"
         echo "vdevd running (peer PIDs: $_peer_list)"
         exit 0
      fi
      echo "vdevd is not running."
      exit 3
      ;;
   *)
      echo "Usage: $SCRIPTNAME {start|stop|once|restart}" >&2
      exit 1
      ;;
esac
