#!/bin/sh
# Filename: systemctl
# Location: /usr/sbin/systemctl
#    /usr/bin/hostnamectl
#    /usr/bin/systemd-detect-virt
# Author: bgstack15@gmail.com
# Startdate: 2020-01-10 13:02:14
# SPDX-License-Identifier: CC-BY-SA-4.0
# Title: 
# Purpose: 
# Package: systemctl-service-shim
# History: 
#    2020-05-14 place framework.sh contents inline so as not to depend on it.
#    2021-01-10 adapted for inclusion in devuan-sanity
#    2021-10-20 add /bin/systemctl symlink control logic
# Usage: 
#    Should be mostly like systemctl from systemd.
# Reference: ftemplate.sh 2019-05-02a ; framework.sh 2018-05-02a
#    man 1 systemctl
# Improve:
#    add preset command
#    Return 1 if status output is failed
# Dependencies:
#    req-devuan: moreutils
# Documentation:
#    Be aware that real systemd systemctl is file /bin/systemctl but
#    this systemdtl is file /usr/sbin/systemctl to prevent a recursive loop
#    in some service scripts that look for /bin/systemctl
# vim: set sw=3 sts=3 ts=3 et:
fiversion="2019-05-02a"
systemctlversion="2021-10-20a"

usage() {
   ${PAGER:-/usr/bin/less -F} >&2 <<ENDUSAGE
usage: systemctl [-duV] [-c conffile]
Provides a systemctl-like interface to sysvinit. Simulates various
actions on services: start, stop, restart, enable, disable, status,
mask, unmask, is-enabled, is-active, condrestart.
version ${systemctlversion}
 -d debug   Show debugging info, including parsed variables.
 -u usage   Show this usage block.
 -V version Show script version number.
 -c conf    Read in this config file.
Return values:
 0 Normal
 1 Queried service is disabled/inactive
 2 Count or type of flaglessvals is incorrect
 3 Incorrect OS type
 4 Unable to find dependency
 5 Not run as root or sudo
ENDUSAGE
}

# DEFINE FUNCTIONS
log_to_file() {
   flecho "${@}" >> "${logfile}"
}

parseFlag() {
   flag="$1"
   hasval=0
   case ${flag} in
      # INSERT FLAGS HERE
      "d" | "debug" | "DEBUG" | "dd" ) setdebug ; ferror "debug level ${debug}" ; __debug_set_by_param=1 ;;
      "u" | "usage" | "help" | "h" ) usage ; exit 0 ;;
      "V" | "fcheck" | "version" ) ferror "${scriptfile} version ${systemctlversion}" ; exit 0 ;;
      "c" | "conf" | "conffile" | "config" ) getval ; conffile="${tempval}" ;;
      "now" ) export SYSTEMCTL_NOW=1 ;;
      "full") export SYSTEMCTL_FULL=1 ;;
      "system") export SYSTEMCTL_SYSTEM=1 ;;
   esac

   debuglev 10 && { test ${hasval} -eq 1 && ferror "flag: ${flag} = ${tempval}" || ferror "flag: ${flag}" ; }
}

# INITIALIZE VARIABLES
# variables set in framework:
# server scriptdir scriptfile scripttrim

### BEGIN IMPORT OF FRAMEWORK.SH
fversion="2020-04-24x"

# DEFINE FUNCTIONS

isflag() {
   # input: $1=word to parse
   case "$1" in
      --*) retval=2 ;;
      -*) retval=1 ;;
      *) retval=0 ;;
   esac
   echo $retval
}

parseParam() {
   # determines if --longname or -shortflagS that need individual parsing
   trimParam=$( printf '%s' "${param}" | sed -n 's/--//p' )
   _rest=
   if test -n "$trimParam" ;
   then
      parseFlag $trimParam
   else
      #splitShortStrings
      _i=2
      while test ${_i} -le ${#param} ;
      do
         _j=$( expr ${_i} + 1)
         #_char=$(expr substr "$param" $_i 1)
         #_rest=$(expr substr "$param" $_j 255)
         _char=$( printf '%s' "${param}" | cut -c ${_i})
         _rest=$( printf '%s' "${param}" | cut -c ${_j}-255)
         parseFlag $_char
         _i=$( expr ${_i} + 1)
      done
   fi
}

getval() {
   tempval=
   if test -n "${_rest}" ;
   then
      tempval="${_rest}"
      hasval=1
      _i=255   # skip rest of splitShortStrings because found the value!
   elif test -n "$nextparam" && test $(isflag "$nextparam") -eq 0 ;
   then
      tempval="$nextparam"
      hasval=1 #DNE ; is affected by ftemplate!
      paramnum=$nextparamnum
   fi
}

debuglev() {
   # call: debuglev 5 && ferror "debug level is at least a five!"
   # added 2015-11-17
   localdebug=0 ; localcheck=0 ;
   fisnum ${debug} && localdebug=${debug}
   fisnum ${1} && localcheck=${1}
   test $localdebug -ge $localcheck && return 0 || return 1
}

debuglevoutput() {
   # call: commandthatgeneratesstdout | debuglevoutput 8
   # output: output to standard error prepended with "debug8: " the contents of the pipe
   ___dlo_threshold="${1}"
   ___dlo_silent="${2}"
   if debuglev "${___dlo_threshold}" ;
   then
      if test -n "${___dlo_silent}" ;
      then
         cat 1>&2
      else
         sed -r -e "s/^/debug${___dlo_threshold}: /;" 1>&2
      fi
   else
      cat 1>/dev/null 2>&1
   fi
}

fisnum() {
   # call: fisnum $1 && debug=$1 || debug=10
   fisnum= ;
   case $1 in
      ''|*[!0-9]*) fisnum=1 ;; # invalid
      *) fisnum=0 ;; # valid number
   esac
   return ${fisnum}
}

fistruthy() {
   # call: if fistruthy "$val" ; then
   local _return=
   case "$( echo "${1}" | tr '[:upper:]' '[:lower:]' )" in
      yes|1|y|true|always) _return=true ;;
   esac
   test -n "${_return}" ; return $?
}

setval() {
   # call: setval 0 value1 value2 value3 ... <<EOFOPTIONS
   # /bin/foo1 --optforfoo1
   # /usr/bin/foo2 --optforfoo2
   # EOFOPTIONS
   #              ^ 0 = soft fail, 1 = critical-fail
   quitonfail="${1}" ; shift
   _vars="${@}"
   #echo "_vars=${_vars}"
   _varcount=0
   for _word in ${_vars} ; do _varcount=$( expr $_varcount + 1 ) ; eval "_var${_varcount}=${_word}" ; done
   _usethis=0
   while read line ;
   do
      _varcount=0
      if test ! "${_usethis}x" = "0x" ; then break ; fi
      #echo "line=${line}" ;
      for _word in ${line} ;
      do
         _varcount=$( expr $_varcount + 1 )
         #echo "word ${_varcount}=${_word}" ;
         case "${_varcount}" in
            1)
               #echo "Testing for existence of file ${_word}"
               if test -f "${_word}" ;
               then
                  _usethis=1
                  #echo "${_var1}=${_word}"
                  eval "${_var1}=${_word}"
               fi
               ;;
            *)
               #echo "just an option: ${_word}"
               if test "${_usethis}x" = "1x" ;
               then
                  #eval echo "\${_var${_varcount}}=${_word}"
                  eval eval "\${_var${_varcount}}=${_word}"
               fi
               ;;
         esac
      done
   done
   #eval echo "testfile=\$${_var1}"
   eval _testfile=\$${_var1}
   if test ! -f "${_testfile}" ;
   then
      case "${quitonfail}" in 1) _failval="critical-fail" ;; *) _failval="fail" ;; esac
      eval "${_var1}=${_failval}"
      setvalout=${_failval}
   else
      eval setvalout="valid-${_var1}"
   fi
}

flecho() {
   # requires moreutils
   if echo "${@}" | grep -qiE '.' ;
   then
      printf "%s\n" "${@}" | TZ=UTC ts "[%FT%TZ]${USER}@${server}:"
   else
      printf '' | TZ=UTC ts "[%FT%TZ]${USER}@${server}"
   fi
}

ferror() {
   # call: ferror "$scriptfile: 2. Something bad happened-- error message 2."
   echo "$@" 1>&2
}

setdebug() {
   # call: setdebug
   debug=10
   getval
   if test $hasval -eq 1 ;
   then
      if fisnum ${tempval} ;
      then
         debug=${tempval}
      else
         #test paramnum -le paramcount && paramnum=$( expr ${paramnum} - 1 )
         hasval=0
      fi
   elif fisnum ${_rest} ;
   then
      debug=${_rest}
      _i=255
   else
      test $paramnum -le $paramcount && test -z ${nextparam} && paramnum=$( expr ${paramnum} - 1 )
   fi
}

define_if_new() {
   # call: define_if_new IFW_IN_LOG_FILE "/var/log/messages"
   eval thisval="\${${1}}"
   test -z "${thisval}" && eval "$1"=\"$2\"
}

# INITIALIZE VARIABLES
server=$( hostname -s )
thisos="$( uname -s )"
# get thisflavor and thisflavorversion. Examples: centos, ubuntu, redhat
if test -f /etc/os-release ;
then
   eval thisflavor=$( grep -iE "^\s*ID=" /etc/os-release 2>/dev/null | sed 's/^.*=//;' | tr 'A-Z' 'a-z' )
   eval thisflavorversion=$( grep -iE "^\s*PRETTY_NAME=" /etc/os-release 2>/dev/null | sed -e 's/^.*=//;' | tr -dc '0-9.' )
elif test -f /etc/system-release && test $( wc -l < /etc/system-release 2>/dev/null ) -eq 1 ;
then
   eval thisflavor=$( awk '{print $1}' < /etc/system-release 2>/dev/null | tr 'A-Z' 'a-z' )
   eval thisflavorversion=$( </etc/system-release sed -e 's/^.*=//;' 2>/dev/null | tr -dc '0-9.' )
else
   if test "${thisos}" = "FreeBSD" ; then
      thisflavor="$( uname -i )" ; thisflavorversion="$( uname -r )" ;
   else
      thisflavor="other"
      thisflavorversion="unknown"
   fi
fi
case "${thisos}" in FreeBSD) sed=gsed ;; *) sed=sed ;; esac

# if framework is dot sourced then $0 will be "-bash" and screw things up
case ${0} in
   "-bash")
      scriptdir="$( pwd )"
      scriptfile="dot-sourced" ;;
   *)
      scriptdir="$( cd $( dirname ${0} ) ; pwd )"
      scriptfile="$( basename ${0} | sed 's!/./!/!g;s!\./!!g' )"
      scripttrim="${scriptfile%%.sh}"
      ;;
esac

# SPECIAL RUNTIME-RELATED VARIABLES
{ test "$USER" = "root" || test "$( stat -c '%u' /proc/$$/exe 2>/dev/null )" = 0 ; } && is_root=1
test -n "$SUDO_USER" && is_root="sudo"

nullflagcount=0
validateparams() {
   # VALIDATE PARAMETERS
   # scroll through all parameters and check for isflag.
   # if isflag, get all flags listed. Also grab param#.
   paramcount=$#
   thiscount=0 ;thisopt=0 ;freeopt=0 ;
   varsyet=0
   paramnum=0
   debug=0
   fallopts=
   while test $paramnum -lt $paramcount ;
   do
      paramnum=$( expr ${paramnum} + 1 )
      eval param=\${$paramnum}
      nextparamnum=$( expr ${paramnum} + 1 )
      eval nextparam=\${$nextparamnum}
      case $param in
         "-")
            if test "$varsyet" = "0" ;
            then
               # first instance marks beginning of flags and parameters.
               #Until then it was the names of variables to fill.
               varsyet=1
            else
               nullflagcount=$( expr ${nullflagcount} + 1 ) #useful for separating flags from something else?
               debuglev 10 && ferror "null flag!" # second instance is null flag.
            fi
            ;;
      esac
      if test -n "$param" ;
      then
         # parameter $param exists.
         if test $(isflag $param) -gt 0 ;
         then
            # IS FLAG
            parseParam
         else
            # IS VALUE
            if test "$varsyet" = "0" ;
            then
               thisopt=$( expr ${thisopt} + 1 )
               test "${param}" = "DEBUG" && debug=10 && thisopt=$( expr ${thisopt} - 1 ) || \
                  eval "varname${thisopt}=${param}"
                  #varname[${thisopt}]="${param}"
               debuglev 10 && ferror "var \"${param}\" named"
            else
               thiscount=$( expr ${thiscount} + 1 )
               test $thiscount -gt $thisopt && freeopt=$( expr ${freeopt} + 1 )
               #eval ${varname[${thiscount}]:-opt${freeopt}}="\"${param}\""
               eval "thisvarname=\${varname${thiscount}}"
                  test -z "${thisvarname}" && eval "thisvarname=opt${freeopt}"
               eval "${thisvarname}=\"${param}\""
               eval fallopts=\"${fallopts} ${param}\"
               debuglev 10 && ferror "${thisvarname} value: ${param}"
            fi
         fi
      fi
   done
   fallopts="${fallopts# }"
   if debuglev 10 ;
   then
      ferror "thiscount=$thiscount"
      ferror "fallopts=$fallopts"
      ferror "Framework $fversion"
      ferror "Finput $fiversion"
   fi
}
### END IMPORT OF FRAMEWORK.SH

define_if_new logfile "/var/log/systemctl.log"

# VALIDATE PARAMETERS
# objects before the dash are options, which get filled with the optvals
# to debug flags, use option DEBUG. Variables set in framework: fallopts
validateparams action - "$@"

# LEARN EX_DEBUG
test -z "${__debug_set_by_param}" && fisnum "${SYSTEMCTL_DEBUG}" && debug="${SYSTEMCTL_DEBUG}"
debug=10

# CONFIGURE VARIABLES AFTER PARAMETERS

_trimmed="$( echo "${@}" | tr -d '\r\n' )"
log_to_file "${0} ${_trimmed}"
case "${0}" in
   *hostnamectl|*systemd-detect-virt) exit 0 ;; # always just short-circuit
esac

# MAIN LOOP

# actions
actionlist=""
case "${action}" in

   restart|start|stop|status|reload|condrestart|try-restart|reload-or-try-restart)
      # re-map a few actions
      case "${action}" in
         "reload-or-try-restart") action=restart ;;
      esac
      x=1
      while test ${x:-${thiscount}} -le $(( thiscount - 1 )) && test ${thiscount} -gt 1 ;
      do
         eval thisopt="\${opt${x}}"
         thisopt="$( echo "${thisopt}" | sed -r -e 's/\.service$//;' )"
         actionstatement="$( printf "%s" "service ${thisopt} ${action};" )"
         actionlist="${actionlist:+${actionlist} }${actionstatement}"
         x=$(( x + 1 ))
      done
      ;;

   enable|disable|mask|unmask)
      case "${action}" in
         mask) action=disable ;;
         unmask) action=enable ;;
      esac
      x=1
      while test ${x:-${thiscount}} -le $(( thiscount - 1 )) && test ${thiscount} -gt 1 ;
      do
         eval thisopt="\${opt${x}}"
         thisopt="$( echo "${thisopt}" | sed -r -e 's/\.service$//;' )"
         actionstatement="$( printf "%s" "update-rc.d ${thisopt} ${action};" )"
         actionlist="${actionlist:+${actionlist} }${actionstatement}"
         test "${SYSTEMCTL_NOW}" = "1" && {
            case "${action}" in
               enable)
                  nowaction=start
                  ;;
               disable)
                  nowaction=stop
                  ;;
            esac
            actionstatement="$( printf "%s" "service ${thisopt} ${nowaction:-stop};" )"
            actionlist="${actionlist:+${actionlist} }${actionstatement}"
         }
         x=$(( x + 1 ))
      done
      ;;

   daemon-reload)
      debuglev 1 && echo "${action} is a NOP."
      ;;

   list-unit-files)
      # Future improvement: can consume --full, but I do not care enough to deal with it now.
      ls -Al /etc/init.d
      ;;

   is-enabled)
      currentrunlevel="$( who -r | grep -oE 'run-level\s+[[:digit:]]+' | awk '{print $NF}' )"
      responsenumber=1

      # loop through each service on the command line
      x=1
      while test ${x:-${thiscount}} -le $(( thiscount - 1 )) && test ${thiscount} -gt 1 ;
      do
         eval thisopt="\${opt${x}}"
         thisopt="$( echo "${thisopt}" | sed -r -e 's/\.service$//;' )"
         #actionstatement="$( printf "%s" "service ${thisopt} ${action};" )"
         scriptfile="$( find "/etc/rc${currentrunlevel}.d" -mindepth 1 -maxdepth 1 -name "S??${thisopt}" 2>/dev/null )"
         responsetext="disabled"
         # if file exists, let us return 0.
         if test -n "${scriptfile}" ;
         then
            debuglev 2 && echo "${scriptfile}"
            responsenumber=0 # any "enabled" response makes systemctl return 0
            responsetext="enabled"
         fi
         echo "${responsetext:-UNKNOWN}"
         x=$(( x + 1 ))
      done
      exit "${responsenumber}"
      ;;

   is-active)
      responsenumber=3
      x=1
      while test ${x:-${thiscount}} -le $(( thiscount - 1 )) && test ${thiscount} -gt 1 ;
      do
         eval thisopt="\${opt${x}}"
         thisopt="$( echo "${thisopt}" | sed -r -e 's/\.service$//;' )"
         #actionstatement="$( printf "%s" "service ${thisopt} ${action};" )"
         servicestatus="$( service "${thisopt}" status 1>/dev/null 2>&1 ; echo "${?}" )"
         responsetext="stopped"
         # if file exists, let us return 0.
         if test ${servicestatus:-1} -eq 0 ;
         then
            responsenumber=0
            responsetext="active"
         fi
         echo "${responsetext:-unknown}"
         x=$(( x + 1 ))
      done
      exit "${responsenumber}"
      ;;

   *)
      ferror "Fatal! 2. Unable to understand action ${action}. Aborted."
      exit 2
      ;;
esac

if test "${0}" = "/bin/systemctl" && test "$( readlink -f /bin/systemctl )" = "/usr/sbin/systemctl" ;
then
   log_to_file "META: removing /bin/systemctl symlink"
   unlink /bin/systemctl
   export FIX_BIN_SYSTEMCTL=1
fi

# list of actions
if test -n "${actionlist}" ;
then
   #debuglev 1 && ferror "Full list: ${actionlist}"
   printf "%s" "${actionlist}" | tr ';' '\n' | while read thisaction ;
   do
      log_to_file "ACTION: ${thisaction}"
      debuglev 5 && ferror "${thisaction}"
      eval "${thisaction}"
   done
fi

if test "${FIX_BIN_SYSTEMCTL}" = "1" ;
then
   log_to_file "META: restoring /bin/systemctl symlink"
   ln -s /usr/sbin/systemctl /bin/systemctl
fi
# exit cleanly
:
