#!/bin/sh
# vim: set sw=3 sts=3 ts=3 et:
# Filename: systemctl
# Location:
#    /bin/systemctl (pre-usrmerge)
#    /usr/bin/systemctl (usrmerge)
# Author: bgstack15@gmail.com
# Author: davidpaul@librem.one
# Startdate: 2020-01-10 13:02:14
# SPDX-License-Identifier: LGPL-2.1-only
# 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
#    2022-07-12 Convert try-restart to restart
#    2022-07-14 Add preset, which runs update-rc.d ${service} defaults
#    2023-02-11 Add /usr/bin/systemctl symlink control
#    2024-03-08 Replace moreutils usage with POSIX shell
#    2024-04-11 Remove symlink logic. Use dh_movetousr for usrmerge handling
#    2024-04-16 Revert symlink logic removal. Its purpose was misunderstood.
#    2024-05-01 Relicense to LGPLv2.1. Fix looping when calling update-rc.d.
#    2025-07-02 Remove hostnamectl and systemd-detect-virt behavior
#    2025-07-11 bring in all patches from Plasma
# Usage:
#    Should be mostly like systemctl from systemd.
# Reference: ftemplate.sh 2019-05-02a ; framework.sh 2018-05-02a
#    man 1 systemctl
#    https://salsa.debian.org/debian/init-system-helpers/-/merge_requests/7
#    https://salsa.debian.org/systemd-team/systemd/-/merge_requests/18
# Improve:
#    Return 1 if status output is failed
fiversion="2019-05-02a"
systemctlversion="2025-07-11a"

usage() {
   cat >&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
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

isoption() {
   case "$1" in
      -*) true ;;
      *) false ;;
   esac
}

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" && isoption "$nextparam" ;
   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
}

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

log_to_file() {
   (date -u +[%Y-%m-%dT%TZ] | tr -d '\n'
   printf "%s@%s" "$(whoami)" "${server}"
   if echo "${@}" | grep -q '.' ;
   then
      printf ":%s\n" "${@}"
   else
      printf "\n"
   fi) >> "${logfile}"
}

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
}

# INITIALIZE VARIABLES
server=$( hostname -s )

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

nullflagcount=0
validateparams() {
   # VALIDATE PARAMETERS
   # scroll through all parameters and check for isoption.
   # if isoption, get all options 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 isoption "$param" ;
         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

logfile="${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}"

# 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"|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
      ;;

   preset)
      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} defaults;" )"
         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" "_SKIP_SYSTEMD_NATIVE=1 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

# 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

# exit cleanly
:
