#!/bin/sh
# Copyright (C) 2026 Christian Wirtz <doc@snowheaven.de>
# License: GPL-2.0-only, see LICENSE in the repository root.
#
# Checkmk agent plug-in "linux_hardware": dumps raw hardware data from sysfs/procfs,
# no logic and no thresholds - naming, levels and state all live in the check plug-ins
# (cmk_addons.plugins.linux_hardware.agent_based), so they can change without
# redeploying the agent. Plain POSIX sh, reads files only; `iw`/`nmcli` are used for
# the Wi-Fi link if installed. Cheap enough to run on every agent call.
#
# Sections (separator "|", one value per line: <object>|<key>|<value>):
#   linux_hardware_hwmon    every /sys/class/hwmon chip: name, device path, temp*/fan*
#   linux_hardware_thermal  every /sys/class/thermal zone: type, temp, trip points
#   linux_hardware_power    every /sys/class/power_supply: uevent + charge limits
#   linux_hardware_wifi     every wireless interface: state, rfkill, signal, link
#   linux_hardware_cpu      per CPU: thermal throttle counters, frequency
#   linux_hardware_edac     per memory controller: corrected/uncorrected ECC errors
#   linux_hardware_drm      per display connector: status, mode, EDID (hex); lid state
#   linux_hardware_sleep    can the host suspend? logind lid actions, sleep targets
#   linux_hardware_aer      per PCIe device: AER error totals (correctable/non-fatal/fatal)
#   linux_hardware_rapl     RAPL energy counters (CPU package, platform "psys")
#   linux_hardware_profile  platform profile, CPU governor, energy performance preference
#   linux_hardware_mmc      per eMMC/SD card (mmcblk*): card data incl. the eMMC's own wear
#                           estimate (EXT_CSD life_time, pre_eol_info - eMMC has no SMART),
#                           size, I/O counters, kernel log error counts, its mounts, boot time
#
# Test hooks: LINUX_HARDWARE_SYS / LINUX_HARDWARE_PROC replace /sys and /proc,
# LINUX_HARDWARE_NO_TOOLS=1 skips iw/nmcli, LINUX_HARDWARE_DMESG names a file with the
# kernel log instead of running dmesg (needs root, like the real agent).

SYS=${LINUX_HARDWARE_SYS:-/sys}
PROC=${LINUX_HARDWARE_PROC:-/proc}

# first line of a file; "ERR" if it exists but cannot be read (e.g. a Wi-Fi card's
# temperature while the card is down: "No data available")
val() {
    _v=""
    if { IFS= read -r _v || [ -n "$_v" ]; } 2>/dev/null <"$1"; then
        printf '%s' "$_v"
    else
        printf 'ERR'
    fi
}

# <object>|<key>|<value of file> for every existing file
emit() {
    _obj=$1
    shift
    for _f in "$@"; do
        [ -f "$_f" ] && printf '%s|%s|%s\n' "$_obj" "${_f##*/}" "$(val "$_f")"
    done
}

find_tool() {
    [ "${LINUX_HARDWARE_NO_TOOLS:-0}" = "1" ] && return 1
    for _d in /usr/sbin /sbin /usr/bin /bin /usr/local/sbin /usr/local/bin; do
        [ -x "$_d/$1" ] && { printf '%s' "$_d/$1"; return 0; }
    done
    return 1
}

section_hwmon() {
    echo '<<<linux_hardware_hwmon:sep(124)>>>'
    for h in "$SYS"/class/hwmon/hwmon*; do
        [ -d "$h" ] || continue
        id=${h##*/}
        dev=$(readlink -f "$h/device" 2>/dev/null)
        printf '%s|name|%s\n' "$id" "$(val "$h/name")"
        [ -n "$dev" ] && printf '%s|device|%s\n' "$id" "$dev"
        if [ -n "$dev" ]; then
            for b in "$dev"/block/*; do [ -e "$b" ] && printf '%s|block|%s\n' "$id" "${b##*/}"; done
            for n in "$dev"/net/*; do [ -e "$n" ] && printf '%s|net|%s\n' "$id" "${n##*/}"; done
            [ -f "$dev/model" ] && printf '%s|model|%s\n' "$id" "$(val "$dev/model")"
        fi
        for f in "$h"/temp*_* "$h"/fan*_*; do
            case ${f##*/} in
                *_input | *_label | *_max | *_crit | *_min | *_alarm | *_fault | *_target)
                    emit "$id" "$f" ;;
            esac
        done
    done
}

section_thermal() {
    echo '<<<linux_hardware_thermal:sep(124)>>>'
    for z in "$SYS"/class/thermal/thermal_zone*; do
        [ -d "$z" ] || continue
        id=${z##*/}
        emit "$id" "$z/type" "$z/temp" "$z/mode"
        # registered as a hwmon chip too - the hwmon section already has it
        for h in "$z"/hwmon*; do [ -d "$h" ] && printf '%s|hwmon|%s\n' "$id" "${h##*/}"; done
        for t in "$z"/trip_point_*_type; do
            [ -f "$t" ] || continue
            emit "$id" "$t" "${t%_type}_temp"
        done
    done
}

section_power() {
    echo '<<<linux_hardware_power:sep(124)>>>'
    for p in "$SYS"/class/power_supply/*; do
        [ -d "$p" ] || continue
        id=${p##*/}
        if [ -r "$p/uevent" ]; then
            while IFS= read -r line || [ -n "$line" ]; do
                case $line in
                    POWER_SUPPLY_*=*) k=${line%%=*}; printf '%s|%s|%s\n' "$id" "${k#POWER_SUPPLY_}" "${line#*=}" ;;
                esac
            done <"$p/uevent"
        fi
        # not part of uevent; type/scope again for drivers that leave them out
        emit "$id" "$p/type" "$p/scope" "$p/charge_control_start_threshold" \
            "$p/charge_control_end_threshold" "$p/charge_types" "$p/charge_behaviour"
    done
}

section_wifi() {
    echo '<<<linux_hardware_wifi:sep(124)>>>'
    iw=$(find_tool iw)
    nmcli=$(find_tool nmcli)
    for n in "$SYS"/class/net/*; do
        [ -d "$n/wireless" ] || [ -e "$n/phy80211" ] || continue
        id=${n##*/}
        emit "$id" "$n/operstate" "$n/address"
        drv=$(readlink -f "$n/device/driver" 2>/dev/null)
        [ -n "$drv" ] && printf '%s|driver|%s\n' "$id" "${drv##*/}"
        if [ -r "$n/device/uevent" ]; then
            while IFS= read -r line || [ -n "$line" ]; do
                case $line in PCI_ID=* | PCI_SUBSYS_ID=* | PCI_SLOT_NAME=*) printf '%s|%s|%s\n' "$id" "${line%%=*}" "${line#*=}" ;; esac
            done <"$n/device/uevent"
        fi
        for r in "$n"/phy80211/rfkill*; do
            [ -d "$r" ] || continue
            printf '%s|rfkill_soft|%s\n' "$id" "$(val "$r/soft")"
            printf '%s|rfkill_hard|%s\n' "$id" "$(val "$r/hard")"
        done
        if [ -r "$PROC/net/wireless" ]; then
            while IFS= read -r line; do
                set -- $line
                [ "$1" = "$id:" ] && printf '%s|proc|%s %s %s\n' "$id" "$3" "$4" "$5"
            done <"$PROC/net/wireless"
        fi
        if [ -n "$iw" ]; then
            "$iw" dev "$id" link 2>/dev/null | while IFS= read -r line; do
                printf '%s|iw|%s\n' "$id" "$line"
            done
        elif [ -n "$nmcli" ]; then
            # active network only; -t escapes ":" inside values as "\:"
            "$nmcli" -t -f IN-USE,SIGNAL,FREQ,RATE,SSID device wifi list ifname "$id" --rescan no 2>/dev/null |
                while IFS= read -r line; do
                    case $line in '*:'*) printf '%s|nmcli|%s\n' "$id" "$line" ;; esac
                done
        fi
    done
}

section_cpu() {
    echo '<<<linux_hardware_cpu:sep(124)>>>'
    for c in "$SYS"/devices/system/cpu/cpu[0-9]*; do
        [ -d "$c" ] || continue
        id=${c##*/}
        t="$c/thermal_throttle"
        emit "$id" "$c/topology/physical_package_id" \
            "$t/core_throttle_count" "$t/core_throttle_total_time_ms" \
            "$t/package_throttle_count" "$t/package_throttle_total_time_ms" \
            "$c/cpufreq/scaling_cur_freq" "$c/cpufreq/cpuinfo_max_freq"
    done
}

section_edac() {
    echo '<<<linux_hardware_edac:sep(124)>>>'
    for m in "$SYS"/devices/system/edac/mc/mc[0-9]*; do
        [ -d "$m" ] || continue
        emit "${m##*/}" "$m/mc_name" "$m/size_mb" "$m/ce_count" "$m/ue_count" \
            "$m/ce_noinfo_count" "$m/ue_noinfo_count"
    done
}

section_drm() {
    echo '<<<linux_hardware_drm:sep(124)>>>'
    for c in "$SYS"/class/drm/card*-*; do
        [ -f "$c/status" ] || continue
        id=${c##*/}
        emit "$id" "$c/status" "$c/enabled" "$c/dpms"
        mode=$(head -n1 "$c/modes" 2>/dev/null)  # first = preferred
        [ -n "$mode" ] && printf '%s|mode|%s\n' "$id" "$mode"
        # the monitor's EDID (vendor, model, serial, size), only while connected; sysfs
        # reports size 0 for it, so read it instead of testing with -s
        edid=$(od -An -tx1 -v "$c/edid" 2>/dev/null | tr -d ' \n')
        [ -n "$edid" ] && printf '%s|edid|%s\n' "$id" "$edid"
    done
    for l in "$PROC"/acpi/button/lid/*/state; do
        [ -f "$l" ] || continue
        # shellcheck disable=SC2046  # word splitting wanted: "state:      open"
        set -- $(val "$l")
        printf 'lid|state|%s\n' "${2:-unknown}"
    done
}

busctl_prop() {  # logind property, e.g. 's "suspend"' -> suspend
    "$1" get-property org.freedesktop.login1 /org/freedesktop/login1 org.freedesktop.login1.Manager "$2" 2>/dev/null |
        sed -e 's/^[a-z]* //' -e 's/"//g'
}

section_sleep() {
    echo '<<<linux_hardware_sleep:sep(124)>>>'
    busctl=$(find_tool busctl)
    if [ -n "$busctl" ]; then
        for prop in HandleLidSwitch HandleLidSwitchExternalPower HandleLidSwitchDocked IdleAction; do
            printf 'logind|%s|%s\n' "$prop" "$(busctl_prop "$busctl" "$prop")"
        done
        # effective answer of logind: includes masked targets and sleep.conf AllowSuspend=
        for m in CanSuspend CanHibernate; do
            printf 'logind|%s|%s\n' "$m" "$("$busctl" call org.freedesktop.login1 /org/freedesktop/login1 \
                org.freedesktop.login1.Manager "$m" 2>/dev/null | sed -e 's/^[a-z]* //' -e 's/"//g')"
        done
    fi
    for l in "$PROC"/acpi/button/lid/*/state; do
        [ -f "$l" ] || continue
        # shellcheck disable=SC2046  # word splitting wanted: "state:      open"
        set -- $(val "$l")
        printf 'lid|state|%s\n' "${2:-unknown}"
    done
    systemctl=$(find_tool systemctl)
    if [ -n "$systemctl" ]; then
        printf 'systemd|default_target|%s\n' "$("$systemctl" get-default 2>/dev/null)"
        for t in sleep.target suspend.target hibernate.target; do
            printf 'systemd|%s|%s\n' "$t" "$("$systemctl" is-enabled "$t" 2>/dev/null)"
        done
    fi
}

section_aer() {
    echo '<<<linux_hardware_aer:sep(124)>>>'
    for d in "$SYS"/bus/pci/devices/*; do
        [ -f "$d/aer_dev_correctable" ] || continue
        id=${d##*/}
        drv=$(readlink -f "$d/driver" 2>/dev/null)
        printf '%s|driver|%s\n' "$id" "${drv##*/}"
        emit "$id" "$d/class"
        for kind in correctable nonfatal fatal; do
            while IFS= read -r line; do
                case $line in TOTAL_ERR_*) set -- $line; printf '%s|%s|%s\n' "$id" "$kind" "$2" ;; esac
            done <"$d/aer_dev_$kind"
        done
    done
}

section_rapl() {
    echo '<<<linux_hardware_rapl:sep(124)>>>'
    # top-level zones only (package-N, psys); sub-zones and intel-rapl-mmio repeat them
    for z in "$SYS"/class/powercap/intel-rapl:[0-9]*; do
        case ${z##*/} in *:*:*) continue ;; esac
        [ -f "$z/energy_uj" ] || continue
        emit "${z##*/}" "$z/name" "$z/energy_uj" "$z/max_energy_range_uj"
    done
}

section_profile() {
    echo '<<<linux_hardware_profile:sep(124)>>>'
    emit platform "$SYS/firmware/acpi/platform_profile" "$SYS/firmware/acpi/platform_profile_choices"
    c="$SYS/devices/system/cpu/cpu0/cpufreq"
    emit cpu "$c/scaling_governor" "$c/energy_performance_preference" "$c/scaling_driver"
    emit cpu "$SYS/devices/system/cpu/intel_pstate/status" "$SYS/devices/system/cpu/intel_pstate/no_turbo"
}

section_mmc() {
    echo '<<<linux_hardware_mmc:sep(124)>>>'
    _log=
    for b in "$SYS"/block/mmcblk*; do
        d=${b##*/}
        # mmcblk1boot0/1 and mmcblk1rpmb are parts of the same card
        case $d in *boot* | *rpmb* | "mmcblk*") continue ;; esac
        [ -d "$b/device" ] || continue
        if [ -z "$_log" ]; then
            # read once; "-" = not readable (no root)
            if [ -n "${LINUX_HARDWARE_DMESG:-}" ]; then
                _log=$(cat "$LINUX_HARDWARE_DMESG" 2>/dev/null)
            else
                _log=$(dmesg 2>/dev/null)
            fi
            _log=${_log:--}
        fi
        for k in type name manfid oemid date fwrev hwrev rev serial life_time pre_eol_info; do
            emit "$d" "$b/device/$k"
        done
        emit "$d" "$b/size" "$b/ro" "$b/removable" "$b/stat"
        # .../mmc_host/mmc1/mmc1:0001 -> mmc1
        host=$(readlink "$b/device" 2>/dev/null)
        host=${host##*/}
        host=${host%%:*}
        printf '%s|host|%s\n' "$d" "$host"
        if [ "$_log" = "-" ]; then
            printf '%s|log|unavailable\n' "$d"
        else
            printf '%s|io_errors|%s\n' "$d" "$(printf '%s\n' "$_log" \
                | grep -cE "I/O error, dev ${d}(p[0-9]+)?,|\] ${d}(p[0-9]+)?: .*(error|timed out|timeout)")"
            [ -n "$host" ] && printf '%s|host_errors|%s\n' "$d" "$(printf '%s\n' "$_log" \
                | grep -E "\] ${host}: " | grep -ciE 'error|timeout|timed out|fail|crc|recover')"
        fi
        # "/dev/mmcblk1p3 / ext4 rw,relatime,..." -> "/ rw"
        while read -r dev mp _ opts _; do
            case $dev in /dev/"$d" | /dev/"$d"p*) printf '%s|mount|%s %s\n' "$d" "$mp" "${opts%%,*}" ;; esac
        done <"$PROC/mounts"
        printf '%s|btime|%s\n' "$d" "$(awk '/^btime/ {print $2}' "$PROC/stat" 2>/dev/null)"
    done
}

section_hwmon
section_thermal
section_power
section_wifi
section_cpu
section_edac
section_drm
section_sleep
section_aer
section_rapl
section_profile
section_mmc
exit 0
