#!/bin/bash

# chkconfig: 2345 98 2
# description: shellbox service for spawning programs

HOME=/root
. /etc/profile
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin

if [ -f /usr/local/bin/getIocBootEnv ]
then
        . /usr/local/bin/getIocBootEnv
        #echo ""
        #echo "shellbox:  IOC=$IOC"
        #echo "shellbox:    FACILITY=$FACILITY"
        #echo "shellbox:    ROOTFSSERVERIP=$ROOTFSSERVERIP"
        #echo "shellbox:    BOOTPC=$BOOTPC"
        #echo "shellbox:    CMDLINE_HOST=$CMDLINE_HOST"
        #echo "shellbox:    CMDLINE_ETH=$CMDLINE_ETH"
        #echo "shellbox:    BOOTIF_BCAST=$BOOTIF_BCAST"
        #echo "shellbox:    BOOTIF_IP=$BOOTIF_IP"
        #echo ""
fi
if [ "$IOC" == "" ]
then
  HOSTNAME=$(hostname -s)
else
  HOSTNAME=$IOC
fi
export HOSTNAME

fail () {
  echo $@ >&2
  exit 1
}

options="-k ^X --killsig 15 -x ^D -i ^C --allow -c"
prog=shellbox
conf=/etc/shellbox.conf
logdir=/var/log/$prog
shells=/var/run/$prog

# color only if printing to terminal
declare -A COLOR
if [ -t 1 ]
then
  COLOR[RUNNING]=
  COLOR[STOPPED]=$'\033[46;30m'
  COLOR[DISABLED]=$'\033[47;38;5;8m'
  COLOR[OBSOLETE]=$'\033[48;5;8;37m'
  COLOR[MODIFIED]=$'\033[43;30m'
  COLOR[DEAD]=$'\033[41;37m'
  COLOR[NORMAL]=$'\033[0;39m'
  COLOR[HEADER]=$'\033[0;1;4m'
fi


echo_failure () {
  echo " [failed]"
}

checkpid () {
  [ -d /proc/$* ]
}

launch () {
  [ -f /ioc/${HOSTNAME}/DoNotStartEpics ] && fail "Not starting EPICS because /ioc/${HOSTNAME}/DoNotStartEpics exists"
  if [ "$1" = "-reload" ]
  then
    reload=YES
    shift
  fi

  [ -d /var/run/procServ ] || mkdir -pm 755 /var/run/procServ
  [ -z "$logdir" -o -d "$logdir" ] || mkdir -pm 755 "$logdir"

  temp=$(mktemp -p $(dirname $shells)) || fail "can't create temporary file"
  # delete entries for dead shell that we are going to restart
  if [ -f $shells ]
  then
    while read -r PID PORT LINE
    do
      if ! checkpid $PID
      then
        [ -z "$*" ] && continue
        for p in "$@"; do [ "$p" = "$PORT" ] && continue 2; done
      fi
      # keep all other shell entries (dead or running)
      echo "$PID $PORT $LINE" >> $temp
      _PID[$PORT]=$PID
      _LINE[$PORT]="$LINE"
    done < $shells
  fi

  # add entries for shells we start
  while read -r PORT USER DIR COMMAND
  do
    # ignore empty lines and comments
    [ "${PORT%#*}" ] && STATUS=STOPPED || STATUS=DISABLED
    PORT="${PORT#\#}"
    test "$PORT" -ge 0 2>/dev/null || continue

    # skip if line does not match given port number
    if [ "$*" ]
    then
      for p in "$@" end; do [ "$p" = "$PORT" ] && break; done
      [ $p = end ] && continue
    fi

    if [ $STATUS = DISABLED ]
    then
      echo "Shell $PORT has been disabled in the configuration" >&2
      continue
    fi

    DIR=${DIR//_IOCNAME_/$HOSTNAME}
    ARGS=${ARGS//_IOCNAME_/$HOSTNAME}

    # check if this shell had already been started
    if [ "${_PID[$PORT]}" ]
    then
      # warn if trying to start already running shell
      if [ -z "$reload" ]
      then
        if [ "$USER $DIR $COMMAND" != "${_LINE[$PORT]}" ]
        then
          echo "Running but modified: ${_LINE[$PORT]}" >&2
          echo "New configuration   : $USER $DIR $COMMAND" >&2
          echo "You may want to restart $PORT" >&2
          mod=1
        else
          echo "Already running: $PORT ${_LINE[$PORT]}" >&2
        fi
      fi
      # skip already running shells
      continue
    fi

    if [ -n "$logdir" ]
    then
      LOG=$logdir/$PORT
      rm -f $LOG
    else
      LOG=/dev/null
    fi

    # start shell
    echo "Starting: $PORT $USER $DIR $COMMAND"
    if [ "$UID" -eq 0 ]
    then
        # sudo may be installed in different locations
        # and may delete important environment variables
        SUDO=$(which sudo) && SUDO+=" -H -u $USER SHELLBOX=$HOSTNAME:$PORT EPICS_HOST_ARCH=$EPICS_HOST_ARCH PATH=$PATH"
    fi

    pidfile=/var/run/procServ/$PORT.pid
    rm -f $pidfile
    logpipe=/tmp/procServ-$PORT.log
    mknod -m 666 $logpipe p 2>/dev/null
    cat $logpipe | logger -p local0.info -t shellbox[$PORT] 2>/dev/null &
    $exe -p $pidfile $options $DIR -L $logpipe $PORT $SUDO $COMMAND >> $LOG 2>&1 < /dev/null
    # check if starting worked or failed
    usleep 100000
    if [ -e $pidfile ]
    then
      PID=$(<$pidfile)
      _PID[$PORT]=$PID
      echo "$PID $PORT $USER $DIR $COMMAND" >> $temp
    else
      _PID[$PORT]=fail
      echo_failure
      echo
      cat $LOG
    fi
  done < $conf
  mv $temp $shells
  chmod 0444 $shells
  for p in "$@"
  do
    if [ -z ${_PID[$p]]} ]
    then
      echo "No configuration for $p found" >&2
    fi
  done

  [ "$mod" ] && fail "To apply all modifcations use reload"
}

start () {
  [ -r $conf ] || fail "$conf not readable"
  exe=$(which procServ) || fail "procServ not found"
  [ -x $exe ] || fail "$exe is not executable"
  mkdir -pm 755 /var/lock/subsys
  touch /var/lock/subsys/$prog
  launch $*
}

stopshell() {
  PID=$1
  PORT=$2
  shift
  echo -n "Stopping: $*"
  kill $PID 2> /dev/null || echo_failure
  echo
  if [ $logdir ]
  then
      echo -e "\n**** stopped ****" >> $logdir/$PORT
  fi
}

stop () {
  # anything to stop?
  if [ ! -f $shells ]
  then
    echo "$prog: No shells started."
    exit 0
  fi
  if [ -z "$1" ]
  then
    # kill all shellboxes
    while read -r PID PORT ARGS
    do
      stopshell $PID $PORT $ARGS
    done < $shells
    rm -f $shells
    rm -f /var/lock/subsys/$prog
  else
    # kill only selected shellboxes
    temp=$(mktemp -p $(dirname $shells)) || fail "can't create temporary file"
    while read -r PID PORT ARGS
    do
      echo "$*" | grep -qE "(^|[[:space:]])$PORT([[:space:]]|$)" && \
        stopshell $PID $PORT $ARGS || \
        echo "$PID $PORT $ARGS" >> $temp
    done < $shells
    mv $temp $shells
    chmod 0444 $shells
  fi
}

reload () {
  echo "Reloading $conf... "
  [ -f $conf ] || fail "not readable"
  # anything to stop?
  if [ -f $shells ]
  then
    # first kill all shells that are modified or not configured any more
    temp=$(mktemp -p $(dirname $shells)) || fail "can't create temporary file"
    while read -r PID LINE
    do
      while read -r PORT USER DIR COMMAND ARGS
      do
        DIR=${DIR//_IOCNAME_/$HOSTNAME}
        ARGS=${ARGS//_IOCNAME_/$HOSTNAME}

        if [ "$PORT $USER $DIR $COMMAND $ARGS" = "$LINE" ]
        then
          echo "Keeping: $LINE"
          echo "$PID $LINE" >> $temp
          continue 2
        fi
      done < $conf
      stopshell $PID $PORT $LINE
    done < $shells
    mv $temp $shells
    chmod 0444 $shells
  fi
  # now start all new or modified shells
  sleep 2
  launch -reload
}

status () {
  while [ "$1" ]
  do
    case "$1" in
      -log | --log) log=1; shift ;;
      --json5) json5=1; shift ;;
      --) shift; break;;
      -* ) echo "unknown option $1 ignored" >&2; shift ;;
      *) break
    esac
  done

  # First read all configured shells
  if [ -f $conf ]
  then
    while read -r PORT USER DIR CMD
    do
      # Assume stopped until proven otherwise
      [ "${PORT%#*}" ] && STATUS=STOPPED || STATUS=DISABLED
      # check for empty lines, comments, rubbish
      PORT="${PORT#\#}"
      test "$PORT" -ge 0 2>/dev/null || continue
      _STATUS[$PORT]=$STATUS
      _USER[$PORT]=$USER
      _DIR[$PORT]=${DIR//_IOCNAME_/$HOSTNAME}
      _CMD[$PORT]=${CMD//_IOCNAME_/$HOSTNAME}
      _CFG[$PORT]="${_USER[$PORT]} ${_DIR[$PORT]} ${_CMD[$PORT]}"
    done < $conf
  fi

  # Now check all started shells
  if [ -f $shells ]
  then
    while read -r PID PORT USER DIR CMD
    do
      CFG="$USER $DIR $CMD"
      _PID[$PORT]=$PID
      _USER[$PORT]=$USER
      _DIR[$PORT]=$DIR
      _CMD[$PORT]=$CMD
      if checkpid $PID
      then
        if [ "${_STATUS[$PORT]}" = DISABLED ]
        then
          _STATUS[$PORT]=OBSOLETE
        elif [ -z "${_CFG[$PORT]}" ]
        then
          _STATUS[$PORT]=OBSOLETE
        elif [ "${_CFG[$PORT]}" != "$CFG" ]
        then
          _STATUS[$PORT]=MODIFIED
        else
          _STATUS[$PORT]=RUNNING
        fi
      else
        _STATUS[$PORT]=DEAD
        _PID[$PORT]=
      fi
    done < $shells
  fi


  if [ "$json5" ]
  then
    echo '{ shells: ['
    for PORT in ${!_STATUS[*]}
    do
      echo '  {'
      echo '   port: '$PORT','
      [ "${_PID[$PORT]}" ] && echo '   pid: '${_PID[$PORT]}','
      echo '   status: "'${_STATUS[$PORT]}'",'
      echo '   user: "'${_USER[$PORT]}'",'
      dir=${_DIR[$PORT]//\\/\\\\}
      echo '   dir: "'${dir//\"/\\\"}'",'
      cmd=${_CMD[$PORT]//\\/\\\\}
      echo '   cmd: "'${cmd//\"/\\\"}'"'
      echo '  },'
    done
    echo ']}'
    exit
  fi

  USERLEN=0
  for v in "${_USER[@]}"
  do
    [ ${#v} -gt $USERLEN ] && USERLEN=${#v}
  done
  DIRLEN=0
  for v in "${_DIR[@]}"
  do
    [ ${#v} -gt $DIRLEN ] && DIRLEN=${#v}
  done
  CMDLEN=0
  for v in "${_CMD[@]}"
  do
    [ ${#v} -gt $CMDLEN ] && CMDLEN=${#v}
  done

  printf "${COLOR[HEADER]}#%-5s  %-8s  %-5s  %-*s  %-*s  %-*s ${COLOR[NORMAL]}\n" pid status port $USERLEN user $DIRLEN dir $CMDLEN command

  for PORT in ${!_STATUS[*]}
  do
    # check if we have to report all shells
    [ "$*" ] &&  echo "$*" | grep -qvE "(^|[[:space:]])$PORT([[:space:]]|$)" && continue
    printf "${COLOR[${_STATUS[$PORT]}]}%-6s  %-8s  %5d  %-*s  %-*s  %-*s ${COLOR[NORMAL]}\n" "${_PID[$PORT]}" ${_STATUS[$PORT]} $PORT $USERLEN ${_USER[$PORT]} $DIRLEN ${_DIR[$PORT]} $CMDLEN "${_CMD[$PORT]}"
    if [ "$logdir" -a "$log" ]
    then
      grep '\*\*\*\*' $logdir/$PORT 2>/dev/null
    fi
  done
}

CMD=$1
shift
case "$CMD" in
  (start)         start $*;;
  (stop)          stop $*;;
  (restart)       stop $*; sleep 2; start $*;; # kill all shells, then start again
  (reread|reload) reload $*;; # reload shellbox.conf without killing too much
  (status)        status $*;;
  (*)             echo "Usage: $0 {start [ports]|stop [ports]|restart [ports]|reload|status [-log] [ports]}" ;;
esac

