#!/usr/bin/env bash
#
# System-Info
# Copyright (C) 2013-2024 by Thomas Dreibholz
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Contact: dreibh@simula.no

export TEXTDOMAIN="System-Info"
# export TEXTDOMAINDIR="${PWD}/locale"   # Default: "/usr/share/locale"

# shellcheck disable=SC1091
. gettext.sh


# ###### Print banners ######################################################
function print-banners ()
{
   # shellcheck disable=SC2068
   for directory in $@ ; do
      if [ -d "${directory}" ] ; then
         bannerScripts=$(find "${directory}" -maxdepth 1 -type f -name "[0-9][0-9]*[^~]" | sort -r)
         for bannerScript in ${bannerScripts} ; do
            ${bannerScript} || break
         done
      fi
   done
}


# ###### Print system information ###########################################
function print-system-information ()
{
   # ====== Obtain host information =========================================
   hostnameResult=$(hostname -f)
   uptimeResult=$(env LANGUAGE=en uptime)
   cpu=$(uname -m)
   kernel=$(uname -r)

   # ====== Obtain CPU information ==========================================
   cores=$(grep -c ^processor /proc/cpuinfo 2>/dev/null || grep -c ^processor /compat/linux/proc/cpuinfo 2>/dev/null || echo "1" )
   [ "$cores" -eq "0" ] && cores=1
   systemLoad=$(echo "${uptimeResult}" | awk -F 'average[s]*:' '{ gsub(/^[ ]*/, "", $2); print $2 }')

   if [[ "${uptimeResult}" =~ min|day|sec ]] ; then
      uptime=$(echo "${uptimeResult}" | awk '{ print $3 " "$4 }' | sed 's/,//g')
   else
      uptime=$(echo "${uptimeResult}" | awk '{ print $3 " hours" }' | sed 's/,//g')
   fi
   processes=$(ps -aex -o pid= | wc -l)

   # ====== Obtain user information =========================================
   users=$(who | awk ' { print $1 }' | sort -ud | wc -l)

   # ====== Obtain disk information =========================================
   diskRoot=$(env LANGUAGE=en df -hT /     | grep -vE "^Filesystem|shm" | awk '{ print $6 }')
   diskHome=$(env LANGUAGE=en df -hT /home | grep -vE "^Filesystem|shm" | awk '{ print $6 }')

   # ====== Obtain operating system information =============================
   DISTRIB_ID=$(eval_gettext "Unknown")
   DISTRIB_CODENAME="${DISTRIB_ID}"
   DISTRIB_RELEASE="00.00"

   # ------ Get information from /etc/os-release ----------------------------
   if [ -e /etc/os-release ] ; then
      . /etc/os-release
      DISTRIB_ID="${NAME}"
      DISTRIB_RELEASE="${VERSION_ID}"
      DISTRIB_CODENAME=""
      if [[ "${VERSION}" =~ ^.*\(([^\(\)]*)\)$ ]] ; then
         DISTRIB_CODENAME="${BASH_REMATCH[1]}"
      fi

   # ------ Legacy system: try lsb_release ----------------------------------
   elif [ -x /usr/bin/lsb_release ] ; then
      DISTRIB_ID=$(/usr/bin/lsb_release -is)
      DISTRIB_RELEASE=$(/usr/bin/lsb_release -rs)
      DISTRIB_CODENAME=$(/usr/bin/lsb_release -cs)
   fi

   # ====== Obtain memory information =======================================
   if [ "${system}" == "Linux" ] ; then
      freeOutput="$(env LANGUAGE=en free -mt)"
      freeMemOutput="$(echo "${freeOutput}" | grep "^Mem:")"
      freeSwapOutput="$(echo "${freeOutput}" | grep "^Swap:")"

      memoryUsed=$(( $(echo "${freeMemOutput}" | awk '{ print $3 }') ))
      memoryAvailable=$(( $(echo "${freeMemOutput}" | awk '{ print $4 }' ) ))
      memoryTotal=$(( $(echo "${freeMemOutput}" | awk '{ print $2 }') ))
      swapUsed=$(( $(echo "${freeSwapOutput}" | awk '{ print $3 }') ))
      swapAvailable=$(( $(echo "${freeSwapOutput}" | awk '{ print $4 }') ))
      swapTotal=$(( $(echo "${freeSwapOutput}" | awk '{ print $2 }') ))

   elif  [ "${system}" == "FreeBSD" ] ; then
      # Calculations based on:
      # * https://www.cyberciti.biz/files/scripts/freebsd-memory.pl.txt
      # * https://github.com/ocochard/myscripts/blob/master/FreeBSD/freebsd-memory.sh

      pageSize=$(sysctl -n hw.pagesize)
      memPhysical=$(sysctl -n hw.physmem)
      # vmstatAll=$(( $(sysctl -n vm.stats.vm.v_page_count)*pageSize ))
      # vmstatWired=$(( $(sysctl -n vm.stats.vm.v_wire_count)*pageSize ))
      # vmstatActive=$(( $(sysctl -n vm.stats.vm.v_active_count)*pageSize ))
      vmstatInactive=$(( $(sysctl -n vm.stats.vm.v_inactive_count)*pageSize ))
      vmstatCache=$(( $(sysctl -n vm.stats.vm.v_cache_count)*pageSize ))
      vmstatFree=$(( $(sysctl -n vm.stats.vm.v_free_count)*pageSize ))

      memoryTotal=$(( memPhysical/1048576 ))
      memoryAvailable=$(( (vmstatInactive+vmstatCache+vmstatFree)/1048576 ))
      memoryUsed=$(( memoryTotal-memoryAvailable ))

      swap=$(swapctl -sk)
      swapTotal=$(echo "${swap}" | awk '{ n=int($2/1024+0.5); print n; }')
      swapUsed=$(echo "${swap}" | awk '{ n=int($3/1024+0.5); print n; }')
      swapAvailable=$(( swapTotal-swapUsed ))
   fi

   # ====== Print system information ========================================
   echo -en "\x1b[33m"
   if [ -e /etc/slicename ] && [ -e /etc/slicefamily ] ; then
      echo -e "$(eval_gettext "Slice:            ")$(cat /etc/slicename) ($(cat /etc/slicefamily))"
   fi
   echo "$(eval_gettext "Host:             ")${hostnameResult}"
   echo "$(eval_gettext "Uptime:           ")${uptime}"
   codeNameText=""
   if [ "${DISTRIB_CODENAME}" != "" ] ; then
      codeNameText=" (${DISTRIB_CODENAME})"
   fi

   echo "$(eval_gettext "Operating System: ")${DISTRIB_ID} ${DISTRIB_RELEASE}${codeNameText} $(eval_gettext "with kernel") ${kernel}"
   printf "$(eval_gettext "Processor:        ")%d × ${cpu}; $(eval_gettext "%'d processes"); $(eval_ngettext "%'d user" "%'d users" "${users}")\n" "${cores}" "${processes}" "${users}"
   echo "$(eval_gettext "Load:             ")${systemLoad}"

   of=$(eval_gettext "of")                 # nnn MiB of mmm MiB
   available=$(eval_gettext "available")   # nnn MiB available
   on=$(eval_gettext "on")                 # xxx MiB on <mountpoint>
   printf "$(eval_gettext "Used Memory:      ")%'6.0f MiB ${of} %'6.0f MiB (%'6.0f MiB ${available})\n" "${memoryUsed}" "${memoryTotal}" "${memoryAvailable}"
   eval_gettext "Used Swap:        "
   if [ "${swapAvailable}" -gt 0 ] ; then
      printf "%'6.0f MiB ${of} %'6.0f MiB (%'6.0f MiB ${available})\n" "${swapUsed}" "${swapTotal}" "${swapAvailable}"
   else
      echo -en "\x1b[37m"
      eval_gettext "no swap available!"
      echo -e "\x1b[33m"
   fi
   echo "$(eval_gettext "Used Diskspace:   ")${diskRoot} ${on} /, ${diskHome} ${on} /home"
   echo -en "\x1b[0m"
}


# ###### Print SSH public key fingerprints ##################################
function print-ssh-key-fingerprints ()
{
   echo -en "\x1b[33m"
   keys=$(find /etc/ssh -maxdepth 1 -name "ssh_host_*.pub" | sort)
   keyNumber=1
   for key in $keys ; do
      if [ ${keyNumber} -eq 1 ] ; then
         gettext "SSH Keys:         #1 "
      else
         eval_gettext "                  #\${keyNumber} "
      fi
      ssh-keygen -lf "$key" | sed -e "s/^\([0-9]*\) \([^ ]*\) \(.*\) (\(.*\))$/\2 (\4 \1)/g"
      keyNumber=$((keyNumber+1))
   done
   echo -en "\x1b[0m"
}


# ###### Convert FreeBSD hexadecimal netmask to prefix length ###############
function hex-netmask-to-prefixlen ()
{
   local N=$(($1))
   local prefixlen=0
   local i=31
   while [ $i -ge 0 ] ; do
      local b=$((1<<i))
      if [ $((N & b)) -ne 0 ] ; then
         prefixlen=$(( prefixlen+1 ))
      else
         break
      fi
      i=$(( i-1 ))
   done
   echo "${prefixlen}"
}


# ###### Print addresses ####################################################
function showAddresses ()
{
   lastInterface=""
   hasIPv4=0
   while read -r interface up protocol address prefixlen ; do
      # ====== Get interface status =========================================
      if [ "${up}" == "1" ] ; then
         # Interface is UP
         colorA="\x1b[34m"
         colorP="\x1b[94m"
      else
         # Interface is DOWN
         colorA="\x1b[37m"
         colorP="\x1b[37m"
      fi
      echo -en "${colorA}"

      # ====== Print addresses ==============================================
      if [ "${lastInterface}" != "${interface}" ] ; then
         if [ "${lastInterface}" != "" ] ; then
            echo ""
         fi
         lastInterface="${interface}"
         hasIPv4=0
         printf "   %-14s " "${interface}:"
      fi

      if [ "${protocol}" == "4" ] ; then
         echo -en "${address} ${colorP}/ ${prefixlen}${colorA}   "
         hasIPv4=1
      elif [ "${protocol}" == "6" ] ; then
         if [ ${hasIPv4} -eq 0 ] ; then
            gettext "(No IPv4)"
         fi
         echo -en "\n                  ${address} ${colorP}/ ${prefixlen}${colorA} "
      fi
      echo -en "\x1b[0m"
   done <<< "$1"
   echo ""
}


# ###### Print network addresses of each interface ##########################
function print-network-addresses ()
{
   # ====== Get Linux network configuration =================================
   if [ "${system}" == "Linux" ] ; then
      allTunnelInterfaces=" $( (ip -4 tunnel show ; ip -6 tunnel show ) | cut -d':' -f1 | sort -u | xargs ) "
      addressList=$(ip addr show | (
         interface="BAD!"
         hidden=1
         up=0
         while read -r line ; do
            # ------ Interface name and flags -------------------------------
            if [[ "${line}" =~ ^([0-9]+)(: )([a-zA-Z0-9\.-]+)(@[a-zA-Z0-9\.-]+|)(: <)([^>]*)(>) ]] ; then
               interface="${BASH_REMATCH[3]}"
               flags=" ${BASH_REMATCH[6]//,/ } "
               if [[ " ${flags} " =~ " UP " ]] ; then
                  up=1
               else
                  up=0
               fi
               if [[ " ${flags} " =~ " LOOPBACK " ]] ; then
                  hidden=1
               elif [[ "${allTunnelInterfaces} " =~ ( )${interface}( ) ]] ; then
                  hidden=1
               else
                  hidden=0
               fi
            elif [ ${hidden} -eq 0 ] ; then
               # ------ IPv4 ------------------------------------------------
               if [[ "${line}" =~ ^(inet )([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\/([0-9]{1,2}) ]] ; then
                  echo -e "${interface}\t${up}\t4\t${BASH_REMATCH[2]}\t${BASH_REMATCH[3]}"
               # ------ IPv6 ---------------------------------------------------
               elif [[ "${line}" =~ ^(inet6 )([0-9a-f:]+)\/([0-9]{1,3})( scope [gsh]) ]] ; then
                  # This already filters out link local by scope!
                  echo -e "${interface}\t${up}\t6\t${BASH_REMATCH[2]}\t${BASH_REMATCH[3]}"
               fi
               # ------ Not relevant -------------------------------------------
               # else
               #    echo "no match: ${line}"
            fi
         done
      ) | sort -k1,2)

   # ====== Get FreeBSD network configuration ===============================
   else
      allTunnelInterfaces=""   # FIXME!
      addressList=$(ifconfig | (
         interface="BAD!"
         hidden=1
         up=0
         while read -r line ; do
            # ------ Interface name and flags -------------------------------
            if [[ "${line}" =~ ^([a-zA-Z0-9\.-]+)(@[a-zA-Z0-9\.-]+|())(: flags=[0-9]+<)([^>]*)(>) ]] ; then
               interface="${BASH_REMATCH[1]}"
               flags=" ${BASH_REMATCH[5]//,/ } "
               if [[ " ${flags} " =~ " UP " ]] ; then
                  up=1
               else
                  up=0
               fi
               if [[ " ${flags} " =~ " LOOPBACK " ]] ; then
                  hidden=1
               elif [[ "${allTunnelInterfaces} " =~ ( )${interface}( ) ]] ; then
                  hidden=1
               else
                  hidden=0
               fi
            elif [ ${hidden} -eq 0 ] ; then
               # ------ IPv4 ------------------------------------------------
               if [[ "${line}" =~ ^(inet )([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})( netmask )(0x[0-9a-f]{8}) ]] ; then
                  netmask=$(hex-netmask-to-prefixlen "${BASH_REMATCH[4]}")
                  echo -e "${interface}\t${up}\t4\t${BASH_REMATCH[2]}\t${netmask}"
               # ------ IPv6 ------------------------------------------------
               elif [[ "${line}" =~ ^(inet6 )([0-9a-f:]+)( prefixlen )([0-9]{1,3}) ]] ; then
                  # This already filters out link local: fe80:...%<interface>!
                  echo -e "${interface}\t${up}\t6\t${BASH_REMATCH[2]}\t${BASH_REMATCH[4]}"
               # ------ Not relevant ----------------------------------------
               # else
               #    echo "no match: ${line}"
               fi
            fi
         done
      ) | sort -k1,2)

   fi

   # ====== Print network information =======================================
   echo -en "\x1b[33m"
   eval_gettext "Network:"
   echo
   showAddresses "${addressList}"
   echo -en "\x1b[0m"
}



# ###### Main program #######################################################

# ====== Handle arguments ===================================================
# shellcheck disable=SC2034
now=$(date)
system=$(uname)
scriptDirectories="/etc/system-info.d /usr/local/etc/system-info.d"

while [ $# -gt 0 ] ; do
   if [ "$1" == "[--scripts" ] || [ "$1" == "-S" ] ; then
      scriptDirectories="$2"
      shift
   else
      echo >&2 "Usage: $0 [--help|-h] [--scripts|-S scripts_directory]"
      exit 1
   fi
   shift
done


# ====== Startup ============================================================
echo ""
eval_gettext "System information as of \${now}"
echo

# ====== Obtain and print information =======================================
# Getting the network information may take a few hundreds of ms on a router
# => prepare it in background, while printing other information.
print-network-addresses 2>&1 | (
   echo ""
   # shellcheck disable=SC2086
   print-banners ${scriptDirectories}
   print-system-information
   print-ssh-key-fingerprints
   wait
   cat
) | \
(
   # Try output via mbuffer. This is faster for console printing, particularly
   # with complex banners and long interface lists. If unavailable, try buffer.
   # Just use cat as fallback, if neither mbuffer nor buffer are available.
   if ! mbuffer -q -s 16k 2>/dev/null ; then
      if ! buffer -s 16k 2>/dev/null ; then
         cat
      fi
   fi
)
