#!/bin/sh

#
# iSIGNAGE device health telemetry collector
#
# Collects a single health sample and optionally sends it to a remote HTTP
# endpoint. It is designed to be run by a systemd timer.
#

set -u

CONFIG_FILE="/etc/isignage/health-monitor.conf"

if [ -r "${CONFIG_FILE}" ]; then
    # shellcheck disable=SC1090
    . "${CONFIG_FILE}"
fi

HEALTH_ENDPOINT="${HEALTH_ENDPOINT:-}"
HEALTH_API_TOKEN="${HEALTH_API_TOKEN:-}"
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-15}"
SIGNAGE_PROCESS="${SIGNAGE_PROCESS:-signage}"
CURL_EXTRA_OPTIONS="${CURL_EXTRA_OPTIONS:-}"
OUTPUT_JSON="${OUTPUT_JSON:-no}"

read_first_line()
{
    if [ -r "$1" ]; then
        head -n 1 "$1" 2>/dev/null
    fi
}

json_escape()
{
    printf '%s' "$1" |
        awk '
        BEGIN {
            ORS = ""
        }

        {
            if (NR > 1)
                printf "\\n"

            gsub(/\\/, "\\\\")
            gsub(/"/, "\\\"")
            gsub(/\t/, "\\t")
            gsub(/\r/, "\\r")

            printf "%s", $0
        }
        '
}

json_number()
{
    value="$1"

    case "${value}" in
        ''|*[!0-9.-]*)
            printf 'null'
            ;;
        *)
            printf '%s' "${value}"
            ;;
    esac
}

read_meminfo()
{
    key="$1"

    awk -v requested_key="${key}" '
        $1 == requested_key ":" {
            print $2
            exit
        }
    ' /proc/meminfo 2>/dev/null
}

read_status()
{
    pid="$1"
    key="$2"

    awk -v requested_key="${key}" '
        $1 == requested_key ":" {
            print $2
            exit
        }
    ' "/proc/${pid}/status" 2>/dev/null
}

read_status_text()
{
    pid="$1"
    key="$2"

    awk -v requested_key="${key}" '
        $1 == requested_key ":" {
            $1 = ""
            sub(/^[ \t]+/, "")
            print
            exit
        }
    ' "/proc/${pid}/status" 2>/dev/null
}

find_process_pid()
{
    process_name="$1"

    if command -v pidof >/dev/null 2>&1; then
        pidof "${process_name}" 2>/dev/null |
            awk '{ print $1 }'
        return
    fi

    for process_directory in /proc/[0-9]*; do
        [ -r "${process_directory}/comm" ] || continue

        process_comm="$(read_first_line "${process_directory}/comm")"

        if [ "${process_comm}" = "${process_name}" ]; then
            basename "${process_directory}"
            return
        fi
    done
}

read_gpu_bo_statistics()
{
    maximum_bo_count=0
    maximum_bo_size_kb=0
    found=0

    #
    # A DRM device can appear under both card and render nodes. Taking the
    # maximum avoids counting the same BO allocations twice.
    #
    for statistics_file in /sys/kernel/debug/dri/*/bo_stats; do
        [ -r "${statistics_file}" ] || continue

        bo_count="$(
            awk '
                /^allocated bos:/ {
                    print $3
                    exit
                }
            ' "${statistics_file}" 2>/dev/null
        )"

        bo_size_kb="$(
            awk '
                /^allocated bo size \(kb\):/ {
                    print $5
                    exit
                }
            ' "${statistics_file}" 2>/dev/null
        )"

        case "${bo_count}" in
            ''|*[!0-9]*)
                bo_count=0
                ;;
        esac

        case "${bo_size_kb}" in
            ''|*[!0-9]*)
                bo_size_kb=0
                ;;
        esac

        if [ "${bo_count}" -gt "${maximum_bo_count}" ]; then
            maximum_bo_count="${bo_count}"
        fi

        if [ "${bo_size_kb}" -gt "${maximum_bo_size_kb}" ]; then
            maximum_bo_size_kb="${bo_size_kb}"
        fi

        found=1
    done

    if [ "${found}" -eq 0 ]; then
        printf '%s %s\n' "" ""
    else
        printf '%s %s\n' \
            "${maximum_bo_count}" \
            "${maximum_bo_size_kb}"
    fi
}

hostname_value="$(hostname 2>/dev/null || printf 'unknown')"
machine_id="$(read_first_line /etc/machine-id)"
boot_id="$(read_first_line /proc/sys/kernel/random/boot_id)"

timestamp="$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null)"
epoch="$(date '+%s' 2>/dev/null)"

uptime_seconds="$(
    awk '{
        printf "%.0f\n", $1
    }' /proc/uptime 2>/dev/null
)"

load_values="$(read_first_line /proc/loadavg)"
load_1="$(printf '%s\n' "${load_values}" | awk '{ print $1 }')"
load_5="$(printf '%s\n' "${load_values}" | awk '{ print $2 }')"
load_15="$(printf '%s\n' "${load_values}" | awk '{ print $3 }')"
running_processes="$(
    printf '%s\n' "${load_values}" |
        awk '{
            split($4, values, "/")
            print values[1]
        }'
)"
total_processes="$(
    printf '%s\n' "${load_values}" |
        awk '{
            split($4, values, "/")
            print values[2]
        }'
)"

mem_total_kb="$(read_meminfo MemTotal)"
mem_available_kb="$(read_meminfo MemAvailable)"
mem_free_kb="$(read_meminfo MemFree)"
buffers_kb="$(read_meminfo Buffers)"
cached_kb="$(read_meminfo Cached)"
swap_total_kb="$(read_meminfo SwapTotal)"
swap_free_kb="$(read_meminfo SwapFree)"
cma_total_kb="$(read_meminfo CmaTotal)"
cma_free_kb="$(read_meminfo CmaFree)"

temperature_millidegrees="$(
    read_first_line /sys/class/thermal/thermal_zone0/temp
)"

temperature_celsius=""

case "${temperature_millidegrees}" in
    ''|*[!0-9]*)
        ;;
    *)
        temperature_celsius="$(
            awk -v temperature="${temperature_millidegrees}" '
                BEGIN {
                    printf "%.1f", temperature / 1000
                }
            '
        )"
        ;;
esac

root_disk_values="$(
    df -Pk / 2>/dev/null |
        awk '
            NR == 2 {
                print $2, $3, $4, $5
            }
        '
)"

root_total_kb="$(printf '%s\n' "${root_disk_values}" | awk '{ print $1 }')"
root_used_kb="$(printf '%s\n' "${root_disk_values}" | awk '{ print $2 }')"
root_available_kb="$(printf '%s\n' "${root_disk_values}" | awk '{ print $3 }')"
root_used_percent="$(
    printf '%s\n' "${root_disk_values}" |
        awk '{
            gsub(/%/, "", $4)
            print $4
        }'
)"

home_disk_values="$(
    df -Pk /home 2>/dev/null |
        awk '
            NR == 2 {
                print $2, $3, $4, $5
            }
        '
)"

home_total_kb="$(printf '%s\n' "${home_disk_values}" | awk '{ print $1 }')"
home_used_kb="$(printf '%s\n' "${home_disk_values}" | awk '{ print $2 }')"
home_available_kb="$(printf '%s\n' "${home_disk_values}" | awk '{ print $3 }')"
home_used_percent="$(
    printf '%s\n' "${home_disk_values}" |
        awk '{
            gsub(/%/, "", $4)
            print $4
        }'
)"

set -- $(read_gpu_bo_statistics)

gpu_bo_count="${1:-}"
gpu_bo_size_kb="${2:-}"

signage_pid="$(find_process_pid "${SIGNAGE_PROCESS}")"
signage_running=false
signage_rss_kb=""
signage_virtual_kb=""
signage_threads=""
signage_fd_count=""
signage_state=""
signage_start_ticks=""

if [ -n "${signage_pid}" ] && [ -r "/proc/${signage_pid}/status" ]; then
    signage_running=true
    signage_rss_kb="$(read_status "${signage_pid}" VmRSS)"
    signage_virtual_kb="$(read_status "${signage_pid}" VmSize)"
    signage_threads="$(read_status "${signage_pid}" Threads)"
    signage_state="$(read_status_text "${signage_pid}" State)"

    if [ -d "/proc/${signage_pid}/fd" ]; then
        signage_fd_count="$(
            find "/proc/${signage_pid}/fd" \
                -mindepth 1 \
                -maxdepth 1 \
                2>/dev/null |
                wc -l |
                awk '{ print $1 }'
        )"
    fi

    if [ -r "/proc/${signage_pid}/stat" ]; then
        signage_start_ticks="$(
            awk '{
                print $22
            }' "/proc/${signage_pid}/stat" 2>/dev/null
        )"
    fi
else
    signage_pid=""
fi

network_receive_bytes=0
network_transmit_bytes=0

for network_directory in /sys/class/net/*; do
    interface_name="$(basename "${network_directory}")"

    [ "${interface_name}" = "lo" ] && continue

    receive_value="$(
        read_first_line "${network_directory}/statistics/rx_bytes"
    )"

    transmit_value="$(
        read_first_line "${network_directory}/statistics/tx_bytes"
    )"

    case "${receive_value}" in
        ''|*[!0-9]*)
            receive_value=0
            ;;
    esac

    case "${transmit_value}" in
        ''|*[!0-9]*)
            transmit_value=0
            ;;
    esac

    network_receive_bytes=$((network_receive_bytes + receive_value))
    network_transmit_bytes=$((network_transmit_bytes + transmit_value))
done

json="$(
    cat <<EOF
{
  "type": "deviceHealth",
  "version": 1,
  "timestamp": "$(json_escape "${timestamp}")",
  "epoch": $(json_number "${epoch}"),
  "device": {
    "hostname": "$(json_escape "${hostname_value}")",
    "machineId": "$(json_escape "${machine_id}")",
    "bootId": "$(json_escape "${boot_id}")",
    "uptimeSeconds": $(json_number "${uptime_seconds}")
  },
  "memory": {
    "totalKB": $(json_number "${mem_total_kb}"),
    "availableKB": $(json_number "${mem_available_kb}"),
    "freeKB": $(json_number "${mem_free_kb}"),
    "buffersKB": $(json_number "${buffers_kb}"),
    "cachedKB": $(json_number "${cached_kb}"),
    "swapTotalKB": $(json_number "${swap_total_kb}"),
    "swapFreeKB": $(json_number "${swap_free_kb}"),
    "cmaTotalKB": $(json_number "${cma_total_kb}"),
    "cmaFreeKB": $(json_number "${cma_free_kb}")
  },
  "gpu": {
    "allocatedBOs": $(json_number "${gpu_bo_count}"),
    "allocatedBOMemoryKB": $(json_number "${gpu_bo_size_kb}")
  },
  "load": {
    "oneMinute": $(json_number "${load_1}"),
    "fiveMinutes": $(json_number "${load_5}"),
    "fifteenMinutes": $(json_number "${load_15}"),
    "runningProcesses": $(json_number "${running_processes}"),
    "totalProcesses": $(json_number "${total_processes}")
  },
  "temperature": {
    "celsius": $(json_number "${temperature_celsius}")
  },
  "disk": {
    "root": {
      "totalKB": $(json_number "${root_total_kb}"),
      "usedKB": $(json_number "${root_used_kb}"),
      "availableKB": $(json_number "${root_available_kb}"),
      "usedPercent": $(json_number "${root_used_percent}")
    },
    "home": {
      "totalKB": $(json_number "${home_total_kb}"),
      "usedKB": $(json_number "${home_used_kb}"),
      "availableKB": $(json_number "${home_available_kb}"),
      "usedPercent": $(json_number "${home_used_percent}")
    }
  },
  "network": {
    "receivedBytes": $(json_number "${network_receive_bytes}"),
    "transmittedBytes": $(json_number "${network_transmit_bytes}")
  },
  "signage": {
    "processName": "$(json_escape "${SIGNAGE_PROCESS}")",
    "running": ${signage_running},
    "pid": $(json_number "${signage_pid}"),
    "state": "$(json_escape "${signage_state}")",
    "rssKB": $(json_number "${signage_rss_kb}"),
    "virtualMemoryKB": $(json_number "${signage_virtual_kb}"),
    "threads": $(json_number "${signage_threads}"),
    "fileDescriptors": $(json_number "${signage_fd_count}"),
    "startTimeTicks": $(json_number "${signage_start_ticks}")
  }
}
EOF
)"

if [ "${OUTPUT_JSON}" = "yes" ]; then
    printf '%s\n' "${json}"
fi

if [ -z "${HEALTH_ENDPOINT}" ]; then
    exit 0
fi

curl_arguments="
    --silent
    --show-error
    --fail
    --connect-timeout ${HEALTH_TIMEOUT}
    --max-time ${HEALTH_TIMEOUT}
    --request POST
    --header Content-Type:application/json
"

if [ -n "${HEALTH_API_TOKEN}" ]; then
    curl_arguments="${curl_arguments}
        --header Authorization:Bearer\ ${HEALTH_API_TOKEN}
    "
fi

# Intentional word splitting is used to pass configured curl arguments.
# shellcheck disable=SC2086
curl ${curl_arguments} ${CURL_EXTRA_OPTIONS} \
    --data-binary "${json}" \
    "${HEALTH_ENDPOINT}"
	 