#!/bin/sh
################################################################################
#
# cme3100 daemon script.
#
################################################################################

# CMe3100 Java logging properties
LOG_PROPERTIES=/application/CMe3100/appdata/currentconfig/logger/logging.properties
LOG4J_PROPERTIES=file:///application/CMe3100/appdata/currentconfig/logger/log4j.properties

# SQLite databases
SQL_APP_DB="/application/CMe3100/appdata/currentstorage/cme3100.sqlite"
SQL_LOG_DB="/application/CMe3100/appdata/currentstorage/log_db_cme3100.sqlite"

# Java Runtime Environment
JAVA=/usr/lib/zulu11.84.18/bin/java

# Java Runtime parameters
JAVA_DEBUG_PARAMS="-Xdebug -Xrunjdwp:transport=dt_socket,address=8800,server=y,suspend=y"
JAVA_DEBUG_SSL_PARAMS="-Djavax.net.debug=ssl:handshake"
JAVA_FILE_ENC="-Dfile.encoding=UTF-8"
JAVA_HEADLESS="-Djava.awt.headless=true"
JAVA_JMX_AUTH="-Dcom.sun.management.jmxremote.authenticate=false"
JAVA_JMX_PORT="-Dcom.sun.management.jmxremote.port=9876"
JAVA_JMX_SSL="-Dcom.sun.management.jmxremote.ssl=false"
JAVA_LOGGING="-Djava.util.logging.config.file=$LOG_PROPERTIES"
JAVA_LOG4J="-Dlog4j.configuration=$LOG4J_PROPERTIES -Dlog4j2.configurationFile=$LOG4J_PROPERTIES"
JAVA_MEM="-Xms30m -Xmx30m"
JAVA_SECURITY="-Djava.security.egd=file:/dev/./urandom"
JAVA_TLS_12="-Djdk.tls.client.protocols=TLSv1.2"
JAVA_OOM="-XX:+CrashOnOutOfMemoryError"
JAVA_ERROR_LOG_DIR="/application/CMe3100/logs"
JAVA_ERROR_FILES="-XX:ErrorFile=$JAVA_ERROR_LOG_DIR/%p_hs_err_pid.log -XX:HeapDumpPath=$JAVA_ERROR_LOG_DIR"
JAVA_GC="-XX:+UseSerialGC"
JAVA_JIT="-XX:CICompilerCount=1 -XX:-BackgroundCompilation -XX:TieredStopAtLevel=1"
JAVA_CACHE="-XX:InitialCodeCacheSize=4m -XX:ReservedCodeCacheSize=16m"
JAVA_META="-XX:MaxMetaspaceSize=64m"
JAVA_STACK="-Xss256k"

# CMe3100 application
APP=/application/CMe3100/Elvaco-CMSeries-Application.jar
APP_START_PARAMETERS="$JAVA_GC $JAVA_MEM $JAVA_OOM $JAVA_HEADLESS $JAVA_FILE_ENC $JAVA_LOGGING $JAVA_LOG4J $JAVA_SECURITY $JAVA_JIT $JAVA_CACHE $JAVA_META $JAVA_STACK $JAVA_ERROR_FILES"

# Config params
DEFAULT_PLUGIN_PATH="/application/CMe3100/appdata/defaultpluginconfig/"
CURRENT_PLUGIN_PATH="/application/CMe3100/appdata/currentpluginconfig/"

# Boot logs
BOOTLOGS_PATH="/application/CMe3100/appdata/currentconfig/bootlogs"

# Java Runtime process id
CME3100_APPLICATION_PID=/tmp/cme3100.pid

# ------------------------------------------------------------------------------
# Increment the boot counter
# ------------------------------------------------------------------------------
incrementBootCounter() {
  stateConfiguration="/application/CMe3100/internal/state.config"
  if [ -f $stateConfiguration ]; then
    rebootCounter=$(grep "snmp.rebootcounter" <$stateConfiguration)
    if [ "$rebootCounter" ]; then
      reboots=$(echo "$rebootCounter" | grep -oe "[0-9]*")
      reboots=$((reboots + 1))
      content=$(cat $stateConfiguration)
      updateReboots=$(echo "$content" | sed s/snmp\.rebootcounter=[0-9]\*/snmp.rebootcounter=$reboots/g)
      updateReboots=$(echo "$updateReboots" | sed s/[[:space:]]/\\n/g)
      echo "$updateReboots" >$stateConfiguration
    else
      echo "snmp.rebootcounter=1" >>$stateConfiguration
    fi
  else
    echo "snmp.rebootcounter=1" >$stateConfiguration
  fi
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Log to console and insert message in log database. Severity is debug (-1)
#
# $1 : message to log
# ------------------------------------------------------------------------------
log() {
  echo "$1"

  NOW=$(date +%s000)
  sqlite3 $SQL_LOG_DB "INSERT INTO Log (created, severity, message, source, deviceId) VALUES ('$NOW', -1, '$1', 'System: update', -1);"
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Log to console and insert message in log database. Severity is critical (3)
#
# $1 : message to log
# $2 : name of JVM error file
# ------------------------------------------------------------------------------
logCritical() {
  echo ">>> Logging from file: $2"
  echo "$1"
  echo ">>> End log"

  NOW=$(date +%s000)
  sqlite3 $SQL_LOG_DB "INSERT INTO Log (created, severity, message, source, deviceId) VALUES ('$NOW', 3, '$1', 'System: jvm ($2)', -1);"
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Count entries in log database
#
# $1 : severity to count. If left out, all entries are counted
# ------------------------------------------------------------------------------
countEntries() {
  if [ -n "$1" ]; then
    result=$(sqlite3 $SQL_LOG_DB "SELECT COUNT(*) FROM Log WHERE Severity='$1';")
  else
    result=$(sqlite3 $SQL_LOG_DB "SELECT COUNT(*) FROM Log;")
  fi

  return "$result"
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Delete entries in log database
#
# $1 : severity of log entry to delete
# $2 : number of items to drop. If left out, all entries for severity are removed
# ------------------------------------------------------------------------------
deleteLogEntriesWithSeverity() {
  if [ -n "$2" ]; then
    sqlite3 $SQL_LOG_DB "DELETE FROM Log
WHERE logId IN (
    SELECT logId
    FROM Log
    WHERE severity = '$1'
    ORDER BY created ASC, logId ASC
    LIMIT '$2'
);"
  else
    sqlite3 $SQL_LOG_DB "DELETE FROM Log WHERE severity = '$1';"
  fi
  sqlite3 $SQL_LOG_DB "VACUUM;"
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Truncate log database to avoid greatly oversized log databases.
# ------------------------------------------------------------------------------
truncateLogDatabaseIfOversized() {
  log_db_size_mb=$(du -m "$SQL_LOG_DB" | awk '{ print $1}')

  if [ "$log_db_size_mb" -gt 100 ]; then
    log "Log database too large ($log_db_size_mb MB), truncated during application start"
    severity=-2
    numberOfCleans=0

    while [ "$log_db_size_mb" -gt 100 ]; do
      case $severity in
      -2)
        deleteLogEntriesWithSeverity $severity
        severity=$((severity + 1))
        ;;
      *)
        deleteLogEntriesWithSeverity $severity 1000
        countEntries $severity
        if [ "$?" -eq 0 ]; then
          severity=$((severity + 1))
        fi
        ;;
      esac

      numberOfCleans=$((numberOfCleans + 1))

      log_db_size_mb=$(du -m "$SQL_LOG_DB" | awk '{ print $1}')

      if [ "$numberOfCleans" -gt 50 ]; then
        break
      fi

      if [ "$severity" -gt 4 ]; then
        break
      fi
    done

    log_db_size_kb=$(du -k "$SQL_LOG_DB" | awk '{ print $1}')
    log "Truncation done (or aborted), size is now $log_db_size_mb MB ($log_db_size_kb kB)"
  else
    echo "Log database size within limits, NOT touching it"
  fi
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Get files creation time stamp
# ------------------------------------------------------------------------------
get_file_datetime() {
  file="$1"
  date -r "$file" +"%Y-%m-%d_%H-%M-%S"
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# In case Java application crashes due to a fatal error the JVM creats an error
# file with information about what happened. We want add information from that
# file into the log database so it's clear that application died and information
# about the crash gets collected.
# Also rename file(s) to avoid getting log for same error file several times.
# ------------------------------------------------------------------------------
handleJvmErrorLogFiles() {
  for file in $(find $JAVA_ERROR_LOG_DIR -maxdepth 1 -type f -regex '.*pid.log' 2>/dev/null); do
    content=""
    while IFS= read -r line; do
      if [ "$line" = "---------------  P R O C E S S  ---------------" ]; then
        break
      fi
      content="${content}${line}\n"
    done <"$file"

    if [ -n "$content" ]; then
      logCritical "$content" "$file"
    fi

    timestamp=$(get_file_datetime "$file")
    dir=$(dirname "$file")
    base=$(basename "$file")

    # Replace suffix "PID_hs_err_pid.log" with "TIMESTAMP_PID_hs_err_pid_LOGGED.log"
    newbase=$(printf '%s_%s' "$timestamp" "$base" | sed 's/_hs_err_pid\.log$/_hs_err_pid_LOGGED.log/')

    # As a safety net (if pattern didn’t match), append _LOGGED before .log
    if [ "$newbase" = "$base" ]; then
      case "$base" in
      *.log) newbase="${base%.log}_LOGGED.log" ;;
      *) newbase="${base}_LOGGED" ;;
      esac
    fi

    mv -- "$file" "$dir/$newbase"
  done
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# In case Java application crashes due to a fatal error the JVM could be
# configured to create a heap dump file.
# If such file exists, rename it to same pattern as the log files so they are
# easy to pair.
# ------------------------------------------------------------------------------
handleJvmHeapDumpFiles() {
  for file in $(find $JAVA_ERROR_LOG_DIR -maxdepth 1 -type f -regex '.*java_pid[0-9].*hprof' 2>/dev/null); do
    if [ -f "$file" ]; then
      timestamp=$(get_file_datetime "$file")
      dir=$(dirname "$file")
      base=$(basename "$file")
      pid=$(printf '%s\n' "$base" | sed -n 's/^java_pid\([[:digit:]][[:digit:]]*\)\.hprof$/\1/p')
      newname="${dir}/${timestamp}_${pid}_heapdump.hprof"
      mv "$file" "$newname"
    fi
  done
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Remove the process id of the application
# ------------------------------------------------------------------------------
removeApplicationPid() {
  rm -f $CME3100_APPLICATION_PID
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Check that Java runtime is available, exit if it is not.
# ------------------------------------------------------------------------------
checkForJavaRuntime() {
  [ -x $JAVA ] || {
    echo "Error: Java runtime '$JAVA' not found"
    exit 1
  }
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Check that CMe3100 application is available, exit if it is not.
# ------------------------------------------------------------------------------
checkForCMe3100Application() {
  [ -e $APP ] || {
    echo "Error: CMe3100 application '$APP' not found"
    exit 1
  }
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Check that the serial number of the product is OK, exit if it is not.
# ------------------------------------------------------------------------------
checkProductSerialNumber() {
  PRODUCT_SNR=$(grep -i 'product.snr' /application/CMe3100/internal/machine.config | cut -f2 -d'=')
  if [ ! "$PRODUCT_SNR" -gt 0016000000 ]; then
    echo "Error: Unable to start application due to invalid serial number '$PRODUCT_SNR'"
    exit 1
  fi
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Always use latest version of common.tdf
# ------------------------------------------------------------------------------
updateCommonTdf() {
  if [ -f /application/CMe3100/appdata/defaultconfig/common/common.tdf ]; then
    echo "Updating common.tdf"
    cp -p /application/CMe3100/appdata/defaultconfig/common/common.tdf /application/CMe3100/appdata/currentconfig/common/common.tdf
  fi
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Always check if plugin bacnet.cfg file are missing
# ------------------------------------------------------------------------------
checkForBacnetCFG() {
  if [ ! -f "$CURRENT_PLUGIN_PATH/bacnet.cfg" ]; then
    echo "Missing bacnet.cfg, copy from defaultpluginconfig"
    cp -p "$DEFAULT_PLUGIN_PATH/bacnet.cfg" "$CURRENT_PLUGIN_PATH/bacnet.cfg"
  fi
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Always check if plugin mqtt.cfg file are missing
# ------------------------------------------------------------------------------
checkForMqttCFG() {
  if [ ! -f "$CURRENT_PLUGIN_PATH/mqtt.cfg" ]; then
    echo "Missing mqtt.cfg, copy from defaultpluginconfig"
    cp -p "$DEFAULT_PLUGIN_PATH/mqtt.cfg" "$CURRENT_PLUGIN_PATH/mqtt.cfg"
  fi
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Always check if plugin stream.cfg file are missing
# ------------------------------------------------------------------------------
checkForRestCFG() {
  if [ ! -f "$CURRENT_PLUGIN_PATH/rest.cfg" ]; then
    echo "Missing rest.cfg, copy from defaultpluginconfig"
    cp -p "$DEFAULT_PLUGIN_PATH/rest.cfg" "$CURRENT_PLUGIN_PATH/rest.cfg"
  fi
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Always check if plugin modbus.cfg file are missing
# ------------------------------------------------------------------------------
checkForModbusCFG() {
  if [ ! -f "$CURRENT_PLUGIN_PATH/modbus.cfg" ]; then
    echo "Missing modbus.cfg, copy from defaultpluginconfig"
    cp -p "$DEFAULT_PLUGIN_PATH/modbus.cfg" "$CURRENT_PLUGIN_PATH/modbus.cfg"
  fi
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Always check if plugin stream.cfg file are missing
# ------------------------------------------------------------------------------
checkForStreamCFG() {
  if [ ! -f "$CURRENT_PLUGIN_PATH/stream.cfg" ]; then
    echo "Missing stream.cfg, copy from defaultpluginconfig"
    cp -p "$DEFAULT_PLUGIN_PATH/stream.cfg" "$CURRENT_PLUGIN_PATH/stream.cfg"
  fi
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Always check if plugin dlms.cfg file are missing
# ------------------------------------------------------------------------------
checkForDLMSCFG() {
  if [ ! -f "$CURRENT_PLUGIN_PATH/dlms.cfg" ]; then
    echo "Missing dlms.cfg, copy from defaultpluginconfig"
    cp -p "$DEFAULT_PLUGIN_PATH/dlms.cfg" "$CURRENT_PLUGIN_PATH/dlms.cfg"
  fi
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Always check if plugin jsonrpc.cfg file are missing
# ------------------------------------------------------------------------------
checkForJsonrpcCFG() {
  if [ ! -f "$CURRENT_PLUGIN_PATH/jsonrpc.cfg" ]; then
    echo "Missing jsonrpc.cfg, copy from defaultpluginconfig"
    cp -p "$DEFAULT_PLUGIN_PATH/jsonrpc.cfg" "$CURRENT_PLUGIN_PATH/jsonrpc.cfg"
  fi
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Running checks for missing files and updating
# ------------------------------------------------------------------------------
verifyFiles() {
  mkdir -p $JAVA_ERROR_LOG_DIR
  updateCommonTdf
  checkForBacnetCFG
  checkForMqttCFG
  checkForRestCFG
  checkForModbusCFG
  checkForStreamCFG
  checkForDLMSCFG
  checkForJsonrpcCFG
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Start the CMe3100 application with supplied parameters
#
# $1 : additional parameters to use when starting the Java application
# ------------------------------------------------------------------------------
startJavaApplication() {
  verifyFiles

  echo "Java binary used : $JAVA"

  $JAVA $APP_START_PARAMETERS $1 -jar $APP &
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Start the CMe3100 application in "normal" mode
#
# $1 : start, start-tls12 or jmx
# $2 : host address if we're starting with jmx support
# ------------------------------------------------------------------------------
startNormal() {
  truncateLogDatabaseIfOversized
  removeApplicationPid
  handleJvmErrorLogFiles
  handleJvmHeapDumpFiles
  checkForJavaRuntime
  checkForCMe3100Application
  checkProductSerialNumber
  /etc/watchdog.sh stop
  incrementBootCounter

  if [ "$1" = "start" ] || [ "$1" = "start-nodiag" ]; then
    echo "Starting with full TLS support."
    startJavaApplication
  elif [ "$1" = "start-tls12" ]; then
    echo "Starting with TLSv1.2 as maximum"
    startJavaApplication "$JAVA_TLS_12"
  else
    if [ -z "$2" ]; then
      echo "Missing host ip"
      exit 1
    fi
    echo "Starting application with JMX"
    startJavaApplication "$JAVA_JMX_AUTH $JAVA_JMX_PORT $JAVA_JMX_SSL -Djava.rmi.server.hostname=$2"
  fi

  echo $! >$CME3100_APPLICATION_PID

  /etc/watchdog.sh init

  crond -f &
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Start the CMe3100 application in debug mode
#
# $1 : debug or debug-ssl
# ------------------------------------------------------------------------------
startDebug() {
  truncateLogDatabaseIfOversized
  removeApplicationPid
  handleJvmErrorLogFiles
  handleJvmHeapDumpFiles
  checkForJavaRuntime
  checkForCMe3100Application
  checkProductSerialNumber
  /etc/watchdog.sh stop

  if [ "$1" = "debug-ssl" ]; then
    echo "Starting application - debug-ssl mode"
    startJavaApplication "$JAVA_DEBUG_SSL_PARAMS"
  else
    echo "Starting application - debug mode"
    startJavaApplication "$JAVA_DEBUG_PARAMS"
  fi

  echo $! >$CME3100_APPLICATION_PID

  crond -f &
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Stop the CMe3100 application
# ------------------------------------------------------------------------------
stopApplication() {
  PID=$(cat $CME3100_APPLICATION_PID)
  if [ -n "$PID" ]; then
    echo "killing $PID "
    kill "$PID"
    /etc/watchdog.sh stop
    removeApplicationPid
  else
    echo "Unable to stop application since process id cannot be found"
  fi
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# Print usage information
# ------------------------------------------------------------------------------
printUsage() {
  echo "Usage: $0 {start|start-tls12|debug|debug-ssl|stop|restart|force-reload|jmx}"
  echo ""
  printf "%s\t\t%s\n" "start" "starting the CMe3100 with default settings"
  printf "%s\t\t%s\n" "start-nodiag" "starting the CMe3100 with default settings and diagnostics OFF"
  printf "%s\t%s\n" "start-tls12" "starting the CMe3100 with maximum of TLSv settings (TLSv1.3)"
  printf "%s\t\t%s\n" "debug" "start CMe3100 in debug mode"
  printf "%s\t%s\n" "debug-ssl" "starting with start-tlsmax settings, but printouts in shell with tls and handshake"
  printf "%s\t\t%s\n" "stop" "stop the CMe3100 processes"
  printf "%s\t\t%s\n" "restart" "restart the CMe3100 processes, equal to running this script with stop and start"
  printf "%s\t%s\n" "force-reload" "exactly the same as running restart"
  printf "%s\t\t%s\n" "jmx <host address>" "starting the CMe3100 with JMX settings"
}
# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------
# "main thread"
# ------------------------------------------------------------------------------
case "$1" in
start | start-tlsmax | jmx)
  mkdir -p "$BOOTLOGS_PATH"
  startNormal "$1" "$2"
  ;;
start-nodiag)
  startNormal "$1" "$2"
  ;;
debug | debug-ssl)
  startDebug "$1"
  ;;
stop)
  stopApplication
  ;;
restart | force-reload)
  $0 stop
  if ! $0 start; then
    exit 1
  fi
  ;;
*)
  printUsage
  exit 1
  ;;
esac

exit 0
# ------------------------------------------------------------------------------
