#!/bin/sh
# Filename: monitor-resize
# Location: /usr/bin
# Author: bgstack15@gmail.com
# Startdate: 2019-01-25 20:03:30
# Title: Daemon that Checks for Updated Virtual Display Size and Requests a Resize
# Purpose: Automatically resizes spice and vnc virtual displays
# Package: bgscripts
# History:
#    2017-08-23 original monitor-resize written
#    2019-01-25 rewrite of whole thing to not depend on child processes
#    2020-03-04 add daemonization support for sysvinit with --daemon flag
#    2020-04-19 remove trap 17 (breaks dash)
# Usage: 
# Reference: ftemplate.sh 2018-11-14a; framework.sh 2018-10-30a
# Improve:
fiversion="2018-11-14a"
monitorresizeversion="2019-01-25a"

usage() {
   ${PAGER:-/usr/bin/less -F} >&2 <<ENDUSAGE
usage: monitor-resize [-duV] [-c conffile] [--nopidfile|--pidfile]
version ${monitorresizeversion}
 -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.
 --nopidfile | --pidfile  Set/unset MR_NOPIDFILE.
Environment variables:
MR_DELAY=2 how many seconds between polls
MR_COMMAND=/usr/bin/resize-x    What to execute when a requested screen size has changed
MR_COMMAND_PARAMS=              Any parameters to pass
MR_PIDFILE=/var/run/monitor_resize.pid   Pidfile for simple lock management.
MR_NOPIDFILE=0    If 1, then do not handle locking. Useful for when a daemon system already handles this for you.
Return values:
 0 Normal
 1 Help or version info displayed
 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
get_displays() {
   # call: get_displays
   # output: array of values: PID,USER,EXE from ps of all application with a DISPLAY value
   # Reference: https://bgstack15.wordpress.com/2017/09/06/find-running-x-sessions/
   {
      for thispid in $( ps -eo pid,command | awk '/session|fluxbox/ {print $1}' ) ;
      do
         cat /proc/${thispid}/environ | tr '\0' '\n' | grep "DISPLAY" | \
            sed -r -e "s@^@${thispid} $( stat -c '%U' /proc/${thispid} ) $( basename $( readlink -f /proc/${thispid}/exe ) ) @;"; 
      done; 
   } 2>/dev/null | grep -iE "xfce|cinnamon|lxsess|fluxbox" | {
      # extension, to grab the requested screen size
      read wholeline ;
      tu="$( echo "${wholeline}" | awk '{print $2}' )"
      td="$( echo "${wholeline}" | awk '{print $NF}' )"
      ts="$( {
         su "${tu}" -c "${td} xrandr --current" | head -n3 | tail -n1 | awk '{print $1}'
      } )"
      echo "${wholeline} ${ts}"
   }
}

# DEFINE TRAPS

clean_monitorresize() {
   # use at end of entire script if you need to clean up tmpfiles
   # rm -f "${tmpfile1}" "${tmpfile2}" 2>/dev/null

   # Delayed cleanup
   if test -z "${MR_NO_CLEAN}" ;
   then
      nohup /bin/bash <<EOF 1>/dev/null 2>&1 &
sleep "${MR_CLEANUP_SEC:-10}" ; /bin/rm -r "${MR_TMPDIR:-NOTHINGTODELETE}" ${MR_PIDFILE} 1>/dev/null 2>&1 ;
EOF
   fi
}

CTRLC() {
   # use with: trap "CTRLC" 2
   # useful for controlling the ctrl+c keystroke
   :
}

CTRLZ() {
   # use with: trap "CTRLZ" 18
   # useful for controlling the ctrl+z keystroke
   :
}

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 1;;
      "V" | "fcheck" | "version" ) ferror "${scriptfile} version ${monitorresizeversion}"; exit 1;;
      #"i" | "infile" | "inputfile" ) getval; infile1=${tempval};;
      "c" | "conf" | "conffile" | "config" ) getval; conffile="${tempval}";;
      "nopidfile" | "no-pid-file" | "no-pidfile" ) MR_NOPIDFILE=1 ;;
      "pidfile" | "pid-file" ) MR_NOPIDFILE=0 ;;
      "daemon" ) MR_DAEMON=1 ;;
   esac

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

# DETERMINE LOCATION OF FRAMEWORK
f_needed=20200305
___frameworkpath="$( find $( echo "${FRAMEWORKPATH}" | tr ':' ' ' ) -maxdepth 1 -mindepth 0 -name 'framework.sh' 2>/dev/null )"
while read flocation ; do if test -e ${flocation} ; then __thisfver="$( sh ${flocation} --fcheck 2>/dev/null )" ; if test ${__thisfver:-0} -ge ${f_needed} ; then frameworkscript="${flocation}" ; break; elif test -n "${___thisfver}" ; then printf "Obsolete: %s %s\n" "${flocation}" "${__thisfver}" 1>&2 ; fi ; fi ; done <<EOFLOCATIONS
${FRAMEWORKBIN:-/bin/false}
${___frameworkpath:-/bin/false}
./framework.sh
${scriptdir}/framework.sh
$HOME/bin/bgscripts/framework.sh
$HOME/bin/framework.sh
$HOME/bgscripts/framework.sh
$HOME/framework.sh
$HOME/.local/share/bgscripts/framework.sh
/usr/local/bin/bgscripts/framework.sh
/usr/local/bin/framework.sh
/usr/bin/bgscripts/framework.sh
/usr/bin/framework.sh
/bin/bgscripts/framework.sh
/usr/local/share/bgscripts/framework.sh
/usr/share/bgscripts/framework.sh
/usr/libexec/bgscripts/framework.sh
EOFLOCATIONS
test -z "${frameworkscript}" && echo "$0: framework ${f_needed} not found. Try setting FRAMEWORKPATH. Aborted." 1>&2 && exit 4

# INITIALIZE VARIABLES
# variables set in framework:
# today server thistty scriptdir scriptfile scripttrim
# is_cronjob stdin_piped stdout_piped stderr_piped sendsh sendopts
. ${frameworkscript} || echo "$0: framework did not run properly. Continuing..." 1>&2
infile1=
outfile1=
logfile=${scriptdir}/${scripttrim}.${today}.out
define_if_new interestedparties "bgstack15@gmail.com"
# SIMPLECONF
define_if_new default_conffile /etc/bgscripts/monitor-resize.conf
define_if_new defuser_conffile ~/.config/bgscripts/monitor-resize.conf
test -z "${MR_TMPDIR}" && MR_TMPDIR="$( mktemp -d )"
MR_tmpfile_blue="$( TMPDIR="${MR_TMPDIR}" mktemp )"
MR_tmpfile_green="$( TMPDIR="${MR_TMPDIR}" mktemp )"
MR_tmpfile_displays="$( TMPDIR="${MR_TMPDIR}" mktemp )"

# REACT TO OPERATING SYSTEM TYPE
case $( uname -s ) in
   Linux) : ;;
   FreeBSD) : ;;
   *) echo "${scriptfile}: 3. Indeterminate OS: $( uname -s )" 1>&2 && exit 3;;
esac

# SET CUSTOM SCRIPT AND VALUES
#setval 1 sendsh sendopts<<EOFSENDSH     # if $1="1" then setvalout="critical-fail" on failure
#/usr/local/share/bgscripts/send.sh -hs  # setvalout maybe be "fail" otherwise
#/usr/share/bgscripts/send.sh -hs        # on success, setvalout="valid-sendsh"
#/usr/local/bin/send.sh -hs
#/usr/bin/mail -s
#EOFSENDSH
#test "${setvalout}" = "critical-fail" && ferror "${scriptfile}: 4. mailer not found. Aborted." && exit 4

# 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 - "$@"

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

# CONFIRM TOTAL NUMBER OF FLAGLESSVALS IS CORRECT
#if test ${thiscount} -lt 2;
#then
#   ferror "${scriptfile}: 2. Fewer than 2 flaglessvals. Aborted."
#   exit 2
#fi

# LOAD CONFIG FROM SIMPLECONF
# This section follows a simple hierarchy of precedence, with first being used:
#    1. parameters and flags
#    2. environment
#    3. config file
#    4. default user config: ~/.config/script/script.conf
#    5. default config: /etc/script/script.conf
if test -f "${conffile}";
then
   get_conf "${conffile}"
else
   if test "${conffile}" = "${default_conffile}" || test "${conffile}" = "${defuser_conffile}"; then :; else test -n "${conffile}" && ferror "${scriptfile}: Ignoring conf file which is not found: ${conffile}."; fi
fi
test -f "${defuser_conffile}" && get_conf "${defuser_conffile}"
test -f "${default_conffile}" && get_conf "${default_conffile}"

# CONFIGURE VARIABLES AFTER PARAMETERS
define_if_new MR_DELAY=2
define_if_new MR_PIDFILE /var/run/monitor_resize.pid
define_if_new MR_DEVTTY "${DEVTTY:-/dev/null}"
define_if_new MR_COMMAND /usr/bin/resize-x
define_if_new MR_COMMAND_PARAMS ""

## REACT TO BEING A CRONJOB
#if test ${is_cronjob} -eq 1;
#then
#   :
#else
#   :
#fi

# SET TRAPS
#trap "CTRLC" 2
#trap "CTRLZ" 18
trap '__ec=$? ; clean_monitorresize ; trap "" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 20 ; exit ${__ec} ;' 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

# DEBUG SIMPLECONF
debuglev 5 && {
   ferror "Using values"
   # used values: EX_(OPT1|OPT2|VERBOSE)
   set | grep -iE "^MR_" 1>&2
}

# REACT TO ROOT STATUS
case ${is_root} in
   1) # proper root
      : ;;
   sudo) # sudo to root
      : ;;
   "") # not root at all
      ferror "${scriptfile}: 5. Please run as root or sudo. Aborted."
      exit 5
      ;;
esac

# EXIT IF PIDFILE EXISTS AND IS POPULATED
if ! fistruthy "${MR_NOPIDFILE}" && test -e "${MR_PIDFILE}";
then
   if /bin/ps -ef | awk '/monitor-re[s]ize/{print $2}' | grep -qiE "$( cat "${MR_PIDFILE}" )";
   then
      ferror "Already running (pid $( cat "${MR_PIDFILE}" )). Aborted."
      exit 7
   else
      ferror "Previous instance did not exit cleanly."
   fi
fi

# CREATE PIDFILE
if ! fistruthy "${MR_NOPIDFILE}" && ! touch "${MR_PIDFILE}";
then
   ferror "Could not create pidfile ${MR_PIDFILE}. Aborted."
   exit 7
else
   echo "$$" > "${MR_PIDFILE}"
fi

# MAIN LOOP
{

   echo "Begin checking for X displays."
   while ! test -e /tmp/kill_monitor-resize.tmp ;
   do
      MR_output_blue="$( get_displays )"

      # if there has been a change
      if test "${MR_output_blue}" != "${MR_output_green:-}" ;
      then
         # calculate and show changes from last time
         echo "${MR_output_blue}" > "${MR_tmpfile_blue}"
         echo "${MR_output_green}" > "${MR_tmpfile_green}"
         #MR_changes="$( diff -s "${MR_tmpfile_blue}" "${MR_tmpfile_green}" 2>/dev/null | sed -r -e '1d' -e 's/^</Added/;' -e 's/^>/Removed/;' | awk 'NF>1' )"
         MR_changes="$( diff "${MR_tmpfile_blue}" "${MR_tmpfile_green}" 2>/dev/null | sed -r -n -e '/^</{s/^<\s*//;p}' | awk 'NF>1' )"
         echo "${MR_changes}" > "${MR_tmpfile_displays}"
         echo "${MR_changes}"
  
         # now, each line in MR_changes is a changed screen resolution
         # send request to each changed resolution
         _MR_x=0
         _MR_changes_max="$( echo "${MR_changes}" | wc -l )"
         while test ${_MR_x} -lt ${_MR_changes_max} ;
         do
            _MR_x=$(( _MR_x + 1 ))
            tl="$( head -n "${_MR_x}" "${MR_tmpfile_displays}" | tail -n1 )"
            tu="$( echo "${tl}" | awk '{print $2}' )"
            td="$( echo "${tl}" | awk '{print $(NF-1)}' )"
            ts="$( echo "${tl}" | awk '{print $(NF)}' )"
            su "${tu}" -c "${td} ${MR_COMMAND:-true} ${MR_COMMAND_PARAMS:-}"
         done

         # at end of processing the difference
         MR_output_green="${MR_output_blue}"
      fi
   
      sleep "${MR_DELAY:-2}"
   done

   # filesystem-based request to terminate, really just so the above while loop is not while true
   rm -f /tmp/kill_monitor-resize.tmp
   ferror "${scriptfile}: Ultimate kill switch used."
   exit 0

} | {
   if test "${MR_DAEMON}" = "1" ;
   then
      while read thisin ;
      do
         logger -e --id=$$ -p user.notice -t "${scriptfile}" "${thisin}"
      done
      logger -e --id=$$ -p user.notice -t "${scriptfile}" "Stopping"
   else
      tee "${MR_DEVTTY}"
   fi
}
