#!/bin/bash

####################################################################################
# s3_sleep
# A utility to set conditional S3 sleep mode
# This script has been adapted from the original S3 script available on the Limetech
# forum. It accepts parameter options to overwrite the default settings.
# Copied some parts from "cache_dirs" to get a similar background behaviour.
#
# Version 1.0.0   Initial version
# Version 1.1.0   Corrections in HDD, TCP and IP monitoring and other adjustments
# Version 1.1.1   Added -t <time> option to set network/device inactivity interval
# Version 1.1.2   Added -e <eth> option to set ethernet interface to monitor
# Version 1.1.3   Added -w option to set wol options prior to sleep
#                 Added -S option to sleep now
# Version 1.1.4   Added -b option to execute shell script before sleep
#                 Added -p option to execute shell script after wake-up
#
# Bergware International
####################################################################################
version=1.1.4
program_name=`basename $0`

usage() {
 echo
 echo "Usage: $program_name [-V] [-a] [-n] [-i ip] [-d day] [-h hour] [-m time] [-t time] [-e eth] [-w wol] [-R] [-F] [-S] [-b name] [-p name] [-q]"
 echo " -V      = print program version "
 echo " -a      = wait for array inactivity"
 echo " -n      = wait for network inactivity"
 echo " -i ip   = IP address to ping (may be repeated as many times as desired)"
 echo " -d day  = Excluded day (may be repeated as many times as desired)"
 echo " -h hour = Excluded hour (may be repeated as many times as desired)"
 echo " -m time = extra delay after array inactivity"
 echo " -t time = interval of network / device inactivity"
 echo " -e eth  = select interface to monitor"
 echo " -w wol  = set WOL options before sleep"
 echo " -R      = do DHCP renewal after wake-up"
 echo " -F      = force gigabit speed after wake-up"
 echo " -S      = sleep NOW"
 echo " -b name = execute shell script 'name' before sleep"
 echo " -p name = execute shell script 'name' after wake-up"
 echo " -q      = terminate running background instance of s3_sleep"
}

# default interface
eth="eth0"

# general variables
no="no"
yes="yes"

# before going to sleep
intrnlTimeoutTicks=30  # ticks after HDD spindown before checking for external activity
extrnlTimeoutTicks=5   # ticks of no external activity before sleep; only after spindown+internal countdown

# control of internal timeout
checkHDD=$no   # check if all HDDs are parked before counting down towards sleep
stopDay=""     # only countdown towards sleep outside these days
               # example: <stopDay="0 6"> (skip Sunday and Saturday)
stopHour=""    # only countdown towards sleep outside these hours
               # example: <stopHour="07 08 19 20">
               # always countdown: <stopDay=""> and <stopHour="">

# control of external timeout
checkTCP=$no   # check for TCP activity
pingIP=""      # do not sleep if <$pingsIPs> are pingable
               # example: <pingIP="192.168.1.4 192.168.1.5">
               # no ping-check: <pingIP="">

# before sleep
wolCmd=""      # set wol options before sleep
preRun=""      # no additional commands to run

# after waking up
dhcpRenew=$no  # <no> for servers w/static IP address
forceGb=$no    # might not be needed; probably always safe
postRun=""     # no additional commands to run

# program control
quit_flag=$no
sleepNow=$no  # force sleep now

# options to overwrite defaults
while getopts "ani:d:h:m:t:e:w:RFqVSb:p:" opt; do
  case $opt in
  a ) checkHDD=$yes ;;
  n ) checkTCP=$yes ;;
  i ) pingIP="$pingIP $OPTARG" ;;
  d ) stopDay="$stopDay $OPTARG" ;;
  h ) stopHour="$stopHour $OPTARG" ;;
  m ) intrnlTimeoutTicks=$OPTARG ;;
  t ) extrnlTimeoutTicks=$OPTARG ;;
  e ) eth=$OPTARG ;;
  w ) wolCmd="$wolCmd $OPTARG" ;;
  R ) dhcpRenew=$yes ;;
  F ) forceGb=$yes ;;
  S ) sleepNow=$yes ;;
  b ) preRun=$OPTARG ;;
  p ) postRun=$OPTARG ;;
  q ) quit_flag=$yes ;;
  \?) usage >&2 ; exit ;;
  V ) echo $program_name version: $version ; exit ;;
  esac
done

# implementation stuff
ticklengthSecs=60  # probe hardware + count down every minute, aka a tick

# obtain ID of flash drive
flash=/dev/$(ls -l /dev/disk/by-label | grep UNRAID | cut -d'/' -f3 | cut -c 1-3)

HDD_activity() {
# preset no hard disk checking
  disks=0
  if [ $checkHDD = $yes ]; then
    disks=$(for d in $(ls /dev/[hs]d? | grep -v "$flash"); do hdparm -C $d | grep active ; done | wc -l)
  fi
  echo $disks
}

TCP_activity() {
# preset no TCP checking
  idle=0
  if [ $checkTCP = $yes ]; then
    idle=$(bwm-ng -o csv -c 1 -d 0 -T avg -I $eth | grep $eth | cut -d';' -f5 | sed 's:[.,]::')
  fi
  echo $idle
}

IP_activity() {
# preset no IP checking
  online=0
# ping each of the destinations to determine if online
  if [ "$pingIP" != "" ]; then
    for ip in $pingIP; do
      # ping single destination; if it answers, it is online
      online=$(ping -q -c 2 $ip | grep received | cut -d' ' -f4)
      if [ $online -ne 0 ]; then
        # if one is online, we do not need to ping
        # any others, break out of the "for" loop.
        break
      fi
    done
  fi
  echo $online
}

pre_sleep_activity() {
# Set WOL MagicPacket options
  if [ "$wolCmd" != "" ]; then
    ethtool -s $eth wol $wolCmd
  fi
# Additional commands to run
  if [ -x "$preRun" ]; then
    $preRun
  fi
  echo DONE >/dev/null
}

post_sleep_activity() {
# Force NIC to use gigabit networking
  if [ $forceGb = $yes ]; then
    ethtool -s $eth speed 1000
  fi
# Force a DHCP renewal (do not use for static-ip boxes)
  if [ $dhcpRenew = $yes ]; then
    /sbin/dhcpcd -n
  fi
# Additional commands to run
  if [ -x "$postRun" ]; then
    $postRun
  fi
  echo DONE >/dev/null
}

go_to_sleep() {
# Do pre-sleep activities
  pre_sleep_activity
  sleep 5
# Go to sleep
  echo -n mem >/sys/power/state
# Do post-sleep activities
  post_sleep_activity
  sleep 5
  echo DONE >/dev/null
}

# Immediate sleep
if [ $sleepNow = $yes ]; then
  go_to_sleep
  exit 0
fi

lockfile="/var/lock/s3_sleep.LCK"
if [ -f "${lockfile}" ]; then
# The file exists so read the PID to see if it is still running
  lock_pid=$(head -n 1 "${lockfile}")
  if [ -z "$(ps -p "${lock_pid}" | grep ${lock_pid})" ]; then
    if [ $quit_flag = $no ]; then
      # The process is not running
      # Echo current PID into lock file
      echo $$ > "${lockfile}"
    else
      echo "$program_name ${lock_pid} is not currently running "
      rm "${lockfile}"
      exit 0
    fi
  else
    if [ $quit_flag = $yes ]; then
      echo killing $program_name process "$lock_pid"
      echo killing $program_name process "$lock_pid" | logger -t$program_name
      kill "$lock_pid"
      rm "${lockfile}"
      exit 0
    else
      echo "$program_name is already running [${lock_pid}]"
      exit 2
    fi
  fi
else
  if [ $quit_flag = $yes ]; then
    echo "$program_name not currently running "
    exit 0
  else
    echo $$ > "${lockfile}"
  fi
fi

# main
intrnlCountdown=$intrnlTimeoutTicks
extrnlCountdown=$extrnlTimeoutTicks
while [ -f "$lockfile" ]; do
  # do not countdown during specific days and hours
  days=$(echo $stopDay | grep "$(date +%w)" | wc -l)
  hours=$(echo $stopHour | grep "$(date +%H)" | wc -l)
  if [ $days -eq 0 -a $hours -eq 0 ]; then
    # count number of HDDs that are not parked
    if [ $(HDD_activity) -eq 0 ]; then
      # tick-tock for time since last spindown
      if [ $intrnlCountdown -gt 0 ]; then
        intrnlCountdown=$[$intrnlCountdown-1]
      fi
    else
      # reset countdown, following HDD activity
      intrnlCountdown=$intrnlTimeoutTicks
      extrnlCountdown=$extrnlTimeoutTicks
    fi
    if [ $intrnlCountdown -le 0 ]; then
      # check for persistent external activity
      if [ $(TCP_activity) -eq 0 -a $(IP_activity) -eq 0 ]; then
        if [ $extrnlCountdown -le 0 ]; then
          go_to_sleep
          intrnlCountdown=$intrnlTimeoutTicks
          extrnlCountdown=$extrnlTimeoutTicks
        else
          # tick-tock for persistent external activity
          if [ $extrnlCountdown -gt 0 ]; then
            extrnlCountdown=$[$extrnlCountdown-1]
          fi
        fi
      else
        # reset countdown, following external activity
        extrnlCountdown=$extrnlTimeoutTicks
      fi
    fi
  fi
  # Wait a tick
  sleep $ticklengthSecs
done &

# while loop was put into background, now disown it
# so it will continue to run when you log off
# to get it to stop, type: rm /var/lock/s3_sleep.LCK
background_pid=$!
echo $background_pid > "${lockfile}"
echo "$program_name process ID $background_pid started, To terminate it, type: $program_name -q" >&2
echo "$program_name process ID $background_pid started, To terminate it, type: $program_name -q" | logger -t$program_name
disown %%
