commit b6391869189a11be89614b5586a44d10a0aa8d5e Author: matt Date: Mon Oct 2 19:08:49 2023 +0200 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/badd.sh b/badd.sh new file mode 100644 index 0000000..ce9ba2b --- /dev/null +++ b/badd.sh @@ -0,0 +1,1212 @@ +#!/usr/bin/env sh +# shellcheck disable=SC1091 + +# Ignore warning about `local` being undefinded in POSIX +# shellcheck disable=SC3043 +# https://github.com/koalaman/shellcheck/wiki/SC3043#exceptions + +# PADD +# A more advanced version of the chronometer provided with Pihole + +# SETS LOCALE +export LC_ALL=C +export LC_NUMERIC=C + +############################################ VARIABLES ############################################# + +# VERSION +padd_version="v3.11.0" + +# LastChecks +LastCheckVersionInformation=$(date +%s) +LastCheckNetworkInformation=$(date +%s) +LastCheckSummaryInformation=$(date +%s) +LastCheckPiholeInformation=$(date +%s) +LastCheckSystemInformation=$(date +%s) +LastCheckPADDInformation=$(date +%s) + +# CORES +core_count=$(nproc --all 2> /dev/null) + +# COLORS +CSI="$(printf '\033')[" # Control Sequence Introducer +red_text="${CSI}91m" # Red +green_text="${CSI}92m" # Green +yellow_text="${CSI}93m" # Yellow +blue_text="${CSI}94m" # Blue +magenta_text="${CSI}95m" # Magenta +cyan_text="${CSI}96m" # Cyan +reset_text="${CSI}0m" # Reset to default +clear_line="${CSI}0K" # Clear the current line to the right to wipe any artifacts remaining from last print + +# STYLES +bold_text="${CSI}1m" +blinking_text="${CSI}5m" +dim_text="${CSI}2m" + +# CHECK BOXES +check_box_good="[${green_text}✓${reset_text}]" # Good +check_box_bad="[${red_text}✗${reset_text}]" # Bad +check_box_question="[${yellow_text}?${reset_text}]" # Question / ? +check_box_info="[${yellow_text}i${reset_text}]" # Info / i + +# PICO STATUSES +pico_status_ok="${check_box_good} Sys. OK" +pico_status_update="${check_box_info} Update" +pico_status_hot="${check_box_bad} Sys. Hot!" +pico_status_off="${check_box_info} No blck" +pico_status_ftl_down="${check_box_bad} FTL Down" +pico_status_dns_down="${check_box_bad} DNS Down" +pico_status_unknown="${check_box_question} Stat. Unk." + +# MINI STATUS +mini_status_ok="${check_box_good} System OK" +mini_status_update="${check_box_info} Update avail." +mini_status_hot="${check_box_bad} System is hot!" +mini_status_off="${check_box_info} No blocking!" +mini_status_ftl_down="${check_box_bad} FTL down!" +mini_status_dns_down="${check_box_bad} DNS off!" +mini_status_unknown="${check_box_question} Status unknown" + +# REGULAR STATUS +full_status_ok="${check_box_good} System is healthy" +full_status_update="${check_box_info} Updates are available" +full_status_hot="${check_box_bad} System is hot!" +full_status_off="${check_box_info} Blocking is disabled" +full_status_ftl_down="${check_box_bad} FTL is down!" +full_status_dns_down="${check_box_bad} DNS is off!" +full_status_unknown="${check_box_question} Status unknown!" + +# MEGA STATUS +mega_status_ok="${check_box_good} Your system is healthy" +mega_status_update="${check_box_info} Updates are available" +mega_status_hot="${check_box_bad} Your system is hot!" +mega_status_off="${check_box_info} Blocking is disabled!" +mega_status_ftl_down="${check_box_bad} FTLDNS service is not running!" +mega_status_dns_down="${check_box_bad} Pi-hole's DNS server is off!" +mega_status_unknown="${check_box_question} Unable to determine Pi-hole status!" + +# TINY STATUS +tiny_status_ok="${check_box_good} System is healthy" +tiny_status_update="${check_box_info} Updates are available" +tiny_status_hot="${check_box_bad} System is hot!" +tiny_status_off="${check_box_info} Blocking is disabled" +tiny_status_ftl_down="${check_box_bad} FTL is down!" +tiny_status_dns_down="${check_box_bad} DNS is off!" +tiny_status_unknown="${check_box_question} Status unknown!" + +# Text only "logos" +padd_text="${green_text}${bold_text}PADD${reset_text}" + +# PADD logos - regular and retro +padd_logo_1="${bold_text}${green_text} __ __ __ ${reset_text}" +padd_logo_2="${bold_text}${green_text}|__) /\\ | \\| \\ ${reset_text}" +padd_logo_3="${bold_text}${green_text}|__)/--\\|__/|__/ ${reset_text}" +padd_logo_retro_1="${bold_text} ${yellow_text}_${green_text}_ ${blue_text}_${magenta_text}_ ${yellow_text}_${green_text}_ ${reset_text}" +padd_logo_retro_2="${bold_text}${yellow_text}|${green_text}_${blue_text}_${cyan_text}) ${red_text}/${yellow_text}\\ ${blue_text}| ${red_text}\\${yellow_text}| ${cyan_text}\\ ${reset_text}" +padd_logo_retro_3="${bold_text}${green_text}| ${red_text}/${yellow_text}-${green_text}-${blue_text}\\${cyan_text}|${magenta_text}_${red_text}_${yellow_text}/${green_text}|${blue_text}_${cyan_text}_${magenta_text}/ ${reset_text}" + + +############################################# GETTERS ############################################## + +GetFTLData() { + local ftl_port data + ftl_port=$(getFTLAPIPort) + if [ -n "$ftl_port" ]; then + # Send command to FTL and ask to quit when finished + data="$(echo ">$1 >quit" | nc 127.0.0.1 "${ftl_port}")" + echo "${data}" + fi +} + +GetSummaryInformation() { + summary=$(GetFTLData "stats") + cache_info=$(GetFTLData "cacheinfo") + + clients=$(echo "${summary}" | grep "unique_clients" | grep -Eo "[0-9]+$") + + blocking_status=$(echo "${summary}" | grep "status" | grep -Eo "enabled|disabled|unknown" ) + + domains_being_blocked_raw=$(echo "${summary}" | grep "domains_being_blocked" | grep -Eo "[0-9]+$") + domains_being_blocked=$(printf "%.f" "${domains_being_blocked_raw}") + + dns_queries_today_raw=$(echo "$summary" | grep "dns_queries_today" | grep -Eo "[0-9]+$") + dns_queries_today=$(printf "%.f" "${dns_queries_today_raw}") + + ads_blocked_today_raw=$(echo "$summary" | grep "ads_blocked_today" | grep -Eo "[0-9]+$") + ads_blocked_today=$(printf "%.f" "${ads_blocked_today_raw}") + + ads_percentage_today_raw=$(echo "$summary" | grep "ads_percentage_today" | grep -Eo "[0-9.]+$") + ads_percentage_today=$(printf "%.1f" "${ads_percentage_today_raw}") + + cache_size=$(echo "$cache_info" | grep "cache-size" | grep -Eo "[0-9.]+$") + cache_deletes=$(echo "$cache_info" | grep "cache-live-freed" | grep -Eo "[0-9.]+$") + cache_inserts=$(echo "$cache_info"| grep "cache-inserted" | grep -Eo "[0-9.]+$") + + latest_blocked_raw=$(GetFTLData recentBlocked) + + top_blocked_raw=$(GetFTLData "top-ads (1)" | awk '{print $3}') + + top_domain_raw=$(GetFTLData "top-domains (1)" | awk '{print $3}') + + top_client_raw=$(GetFTLData "top-clients (1)" | awk '{print $4}') + if [ -z "${top_client_raw}" ]; then + # if no hostname was supplied, use IP + top_client_raw=$(GetFTLData "top-clients (1)" | awk '{print $3}') + fi +} + +GetSystemInformation() { + # System uptime + system_uptime_raw=$(uptime) + + # CPU temperature + if [ -d "/sys/devices/platform/coretemp.0/hwmon/" ]; then + cpu=$(cat "$(find /sys/devices/platform/coretemp.0/hwmon/ -maxdepth 2 -name "temp1_input" 2>/dev/null | head -1)" 2>/dev/null) + fi + if [ -z "${cpu}" ] && [ -f /sys/class/thermal/thermal_zone0/temp ]; then + cpu=$(cat /sys/class/thermal/thermal_zone0/temp) + fi + if [ -z "${cpu}" ] && [ -f /sys/class/hwmon/hwmon0/temp1_input ]; then + cpu=$(cat /sys/class/hwmon/hwmon0/temp1_input) + fi + if [ -z "${cpu}" ]; then + cpu=0 + fi + + # Convert CPU temperature to correct unit + if [ "${TEMPERATUREUNIT}" = "F" ]; then + temperature="$(printf %.1f "$(echo "${cpu}" | awk '{print $1 * 9 / 5000 + 32}')")°F" + elif [ "${TEMPERATUREUNIT}" = "K" ]; then + temperature="$(printf %.1f "$(echo "${cpu}" | awk '{print $1 / 1000 + 273.15}')")°K" + else + temperature="$(printf %.1f "$(echo "${cpu}" | awk '{print $1 / 1000}')")°C" + fi + + # CPU load, heatmap + cpu_load_1=$(awk '{print $1}' < /proc/loadavg) + cpu_load_5=$(awk '{print $2}' < /proc/loadavg) + cpu_load_15=$(awk '{print $3}' < /proc/loadavg) + cpu_load_1_heatmap=$(HeatmapGenerator "${cpu_load_1}" "${core_count}") + cpu_load_5_heatmap=$(HeatmapGenerator "${cpu_load_5}" "${core_count}") + cpu_load_15_heatmap=$(HeatmapGenerator "${cpu_load_15}" "${core_count}") + cpu_percent=$(printf %.1f "$(echo "${cpu_load_1} ${core_count}" | awk '{print ($1 / $2) * 100}')") + + # CPU temperature heatmap + hot_flag=false + # If we're getting close to 85°C... (https://www.raspberrypi.org/blog/introducing-turbo-mode-up-to-50-more-performance-for-free/) + if [ ${cpu} -gt 80000 ]; then + temp_heatmap=${blinking_text}${red_text} + # set flag to change the status message in SetStatusMessage() + hot_flag=true + elif [ ${cpu} -gt 70000 ]; then + temp_heatmap=${magenta_text} + elif [ ${cpu} -gt 60000 ]; then + temp_heatmap=${blue_text} + else + temp_heatmap=${cyan_text} + fi + + # Memory use, heatmap and bar + memory_percent=$(awk '/MemTotal:/{total=$2} /MemFree:/{free=$2} /Buffers:/{buffers=$2} /^Cached:/{cached=$2} END {printf "%.1f", (total-free-buffers-cached)*100/total}' '/proc/meminfo') + memory_heatmap=$(HeatmapGenerator "${memory_percent}") + + # Get product name and family + product_name= + product_family= + if [ -f /sys/devices/virtual/dmi/id/product_name ]; then + # Get product name, remove possible null byte + product_name=$(tr -d '\0' < /sys/devices/virtual/dmi/id/product_name) + fi + if [ -f /sys/devices/virtual/dmi/id/product_family ]; then + # Get product family, remove possible null byte + product_family=$(tr -d '\0' < /sys/devices/virtual/dmi/id/product_family) + fi + + board_vendor= + board_name= + if [ -f /sys/devices/virtual/dmi/id/board_vendor ]; then + board_vendor=$(tr -d '\0' < /sys/devices/virtual/dmi/id/board_vendor) + fi + if [ -f /sys/devices/virtual/dmi/id/board_name ]; then + board_name="$(tr -d '\0' < /sys/devices/virtual/dmi/id/board_name)" + fi + + + if [ -n "$product_name" ] || [ -n "$product_family" ]; then + if echo "$product_family" | grep -q "$product_name"; then + # If product_name is contained in product_family, only show product_family + sys_model="${product_family}" + else + # If product_name is not contained in product_family, both are shown + sys_model="${product_family} ${product_name}" + fi + elif [ -f /sys/firmware/devicetree/base/model ]; then + sys_model=$(tr -d '\0' < /sys/firmware/devicetree/base/model) + elif [ -n "$board_vendor" ] || [ -n "$board_name" ]; then + sys_model="${board_vendor} ${board_name}" + elif [ -f /tmp/sysinfo/model ]; then + sys_model=$(tr -d '\0' < /tmp/sysinfo/model) + elif [ -n "${DOCKER_VERSION}" ]; then + # Docker image. DOCKER_VERSION is read from /etc/pihole/versions + sys_model="Container" + fi + + # Cleaning device model from useless OEM information + sys_model=$(filterModel "${sys_model}") + + if [ -z "$sys_model" ]; then + sys_model="Unknown" + fi +} + +GetNetworkInformation() { + # Get pi IPv4 address + pi_ip4_addrs="$(ip addr | grep 'inet ' | grep -v '127.0.0.1/8' | awk '{print $2}' | cut -f1 -d'/' |wc -l)" + if [ "${pi_ip4_addrs}" -eq 0 ]; then + # No IPv4 address available + pi_ip4_addr="N/A" + elif [ "${pi_ip4_addrs}" -eq 1 ]; then + # One IPv4 address available + pi_ip4_addr="$(ip addr | grep 'inet ' | grep -v '127.0.0.1/8' | awk '{print $2}' | cut -f1 -d'/' | head -n 1)" + else + # More than one IPv4 address available + pi_ip4_addr="$(ip addr | grep 'inet ' | grep -v '127.0.0.1/8' | awk '{print $2}' | cut -f1 -d'/' | head -n 1)+" + fi + + # Get pi IPv6 address + pi_ip6_addrs="$(ip addr | grep 'inet6 ' | grep -v '::1/128' | awk '{print $2}' | cut -f1 -d'/' | wc -l)" + if [ "${pi_ip6_addrs}" -eq 0 ]; then + # No IPv6 address available + pi_ip6_addr="N/A" + ipv6_check_box=${check_box_bad} + elif [ "${pi_ip6_addrs}" -eq 1 ]; then + # One IPv6 address available + pi_ip6_addr="$(ip addr | grep 'inet6 ' | grep -v '::1/128' | awk '{print $2}' | cut -f1 -d'/' | head -n 1)" + ipv6_check_box=${check_box_good} + else + # More than one IPv6 address available + pi_ip6_addr="$(ip addr | grep 'inet6 ' | grep -v '::1/128' | awk '{print $2}' | cut -f1 -d'/' | head -n 1)+" + ipv6_check_box=${check_box_good} + fi + + # Get hostname and gateway + pi_hostname=$(hostname) + + full_hostname=${pi_hostname} + # does the Pi-hole have a domain set? + if [ -n "${PIHOLE_DOMAIN+x}" ]; then + # is Pi-hole acting as DHCP server? + if [ "${DHCP_ACTIVE}" = "true" ]; then + count=${pi_hostname}"."${PIHOLE_DOMAIN} + count=${#count} + if [ "${count}" -lt "18" ]; then + full_hostname=${pi_hostname}"."${PIHOLE_DOMAIN} + fi + fi + fi + + # Get the DNS count (from pihole -c) + dns_count="0" + [ -n "${PIHOLE_DNS_1}" ] && dns_count=$((dns_count+1)) + [ -n "${PIHOLE_DNS_2}" ] && dns_count=$((dns_count+1)) + [ -n "${PIHOLE_DNS_3}" ] && dns_count=$((dns_count+1)) + [ -n "${PIHOLE_DNS_4}" ] && dns_count=$((dns_count+1)) + [ -n "${PIHOLE_DNS_5}" ] && dns_count=$((dns_count+1)) + [ -n "${PIHOLE_DNS_6}" ] && dns_count=$((dns_count+1)) + [ -n "${PIHOLE_DNS_7}" ] && dns_count=$((dns_count+1)) + [ -n "${PIHOLE_DNS_8}" ] && dns_count=$((dns_count+1)) + [ -n "${PIHOLE_DNS_9}" ] && dns_count=$((dns_count+1)) + + # if there's only one DNS server + if [ ${dns_count} -eq 1 ]; then + dns_information="1 server" + else + dns_information="${dns_count} servers" + fi + + # Is Pi-Hole acting as the DHCP server? + if [ "${DHCP_ACTIVE}" = "true" ]; then + dhcp_status="Enabled" + dhcp_info=" Range: ${DHCP_START} - ${DHCP_END}" + dhcp_heatmap=${green_text} + dhcp_check_box=${check_box_good} + + # Is DHCP handling IPv6? + # DHCP_IPv6 is set in setupVars.conf + # shellcheck disable=SC2154 + if [ "${DHCP_IPv6}" = "true" ]; then + dhcp_ipv6_status="Enabled" + dhcp_ipv6_heatmap=${green_text} + dhcp_ipv6_check_box=${check_box_good} + else + dhcp_ipv6_status="Disabled" + dhcp_ipv6_heatmap=${red_text} + dhcp_ipv6_check_box=${check_box_bad} + fi + else + dhcp_status="Disabled" + dhcp_heatmap=${red_text} + dhcp_check_box=${check_box_bad} + + # if the DHCP Router variable isn't set + if [ -z ${DHCP_ROUTER+x} ]; then + DHCP_ROUTER=$(GetFTLData "gateway" | awk '{ printf $1 }') + fi + + dhcp_info=" Router: ${DHCP_ROUTER}" + dhcp_heatmap=${red_text} + dhcp_check_box=${check_box_bad} + + dhcp_ipv6_status="N/A" + dhcp_ipv6_heatmap=${yellow_text} + dhcp_ipv6_check_box=${check_box_question} + fi + + # DNSSEC + if [ "${DNSSEC}" = "true" ]; then + dnssec_status="Enabled" + dnssec_heatmap=${green_text} + else + dnssec_status="Disabled" + dnssec_heatmap=${red_text} + fi + + # Conditional forwarding + if [ "${CONDITIONAL_FORWARDING}" = "true" ] || [ "${REV_SERVER}" = "true" ]; then + conditional_forwarding_status="Enabled" + conditional_forwarding_heatmap=${green_text} + else + conditional_forwarding_status="Disabled" + conditional_forwarding_heatmap=${red_text} + fi + + #Default interface data + def_iface_data=$(GetFTLData "interfaces" | head -n1) + iface_name="$(echo "$def_iface_data" | awk '{print $1}')" + tx_bytes="$(echo "$def_iface_data" | awk '{print $4}')" + rx_bytes="$(echo "$def_iface_data" | awk '{print $5}')" +} + +GetPiholeInformation() { + # Get FTL status + + # Get FTL's current PID + ftlPID="$(getFTLPID)" + + # If FTL is not running (getFTLPID returns -1), set all variables to "not running" + ftl_down_flag=false + if [ "${ftlPID}" = "-1" ]; then + ftl_status="Not running" + ftl_heatmap=${red_text} + ftl_check_box=${check_box_bad} + # set flag to change the status message in SetStatusMessage() + ftl_down_flag=true + ftl_cpu="N/A" + ftl_mem_percentage="N/A" + else + ftl_status="Running" + ftl_heatmap=${green_text} + ftl_check_box=${check_box_good} + # Get FTL CPU and memory usage + ftl_cpu="$(ps h -p "${ftlPID}" -o %cpu | tr -d '[:space:]')%" + ftl_mem_percentage="$(ps h -p "${ftlPID}" -o %mem | tr -d '[:space:]')%" + # Get Pi-hole (blocking) status + ftl_dns_port=$(GetFTLData "dns-port") + fi + + # ${ftl_dns_port} == 0 DNS server part of dnsmasq disabled, ${ftl_status} == "Not running" no ftlPID found + dns_down_flag=false + if [ "${ftl_dns_port}" = 0 ] || [ "${ftl_status}" = "Not running" ]; then + dns_status="DNS offline" + dns_heatmap=${red_text} + dns_check_box=${check_box_bad} + # set flag to change the status message in SetStatusMessage() + dns_down_flag=true + else + dns_check_box=${check_box_good} + dns_status="Active" + dns_heatmap=${green_text} +fi +} + +GetVersionInformation() { + # Check if version status has been saved + # all info is sourced from /etc/pihole/versions + + out_of_date_flag=false + + # If PADD is running inside docker, immediately return without checking for updated component versions + if [ -n "${DOCKER_VERSION}" ]; then + docker_version_converted="$(VersionConverter "${DOCKER_VERSION}")" + docker_version_latest_converted="$(VersionConverter "${GITHUB_DOCKER_VERSION}")" + + if [ "${docker_version_converted}" -lt "${docker_version_latest_converted}" ]; then + out_of_date_flag="true" + docker_version_heatmap=${red_text} + else + docker_version_heatmap=${green_text} + fi + return + fi + + # Gather CORE version information... + # Extract vx.xx or vx.xx.xxx version + CORE_VERSION="$(echo "${CORE_VERSION}" | grep -oE '^v[0-9]+([.][0-9]+){1,2}')" + if [ "${CORE_BRANCH}" = "master" ]; then + core_version_converted="$(VersionConverter "${CORE_VERSION}")" + core_version_latest_converted=$(VersionConverter "${GITHUB_CORE_VERSION}") + + if [ "${core_version_converted}" -lt "${core_version_latest_converted}" ]; then + out_of_date_flag="true" + core_version_heatmap=${red_text} + else + core_version_heatmap=${green_text} + fi + + else + # Custom branch + if [ -z "${CORE_BRANCH}" ]; then + # Branch name is empty, something went wrong + core_version_heatmap=${red_text} + CORE_VERSION="?" + else + if [ "${CORE_HASH}" = "${GITHUB_CORE_HASH}" ]; then + # up-to-date + core_version_heatmap=${green_text} + else + # out-of-date + out_of_date_flag="true" + core_version_heatmap=${red_text} + fi + # shorten common branch names (fix/, tweak/, new/) + # use the first 7 characters of the branch name as version + CORE_VERSION="$(printf '%s' "$CORE_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)" + fi + fi + + # Gather web version information... + # Extract vx.xx or vx.xx.xxx version + if [ "$INSTALL_WEB_INTERFACE" = true ]; then + WEB_VERSION="$(echo "${WEB_VERSION}" | grep -oE '^v[0-9]+([.][0-9]+){1,2}')" + if [ "${WEB_BRANCH}" = "master" ]; then + web_version_converted="$(VersionConverter "${WEB_VERSION}")" + web_version_latest_converted=$(VersionConverter "${GITHUB_WEB_VERSION}") + + if [ "${web_version_converted}" -lt "${web_version_latest_converted}" ]; then + out_of_date_flag="true" + web_version_heatmap=${red_text} + else + web_version_heatmap=${green_text} + fi + + else + # Custom branch + if [ -z "${WEB_BRANCH}" ]; then + # Branch name is empty, something went wrong + web_version_heatmap=${red_text} + WEB_VERSION="?" + else + if [ "${WEB_HASH}" = "${GITHUB_WEB_HASH}" ]; then + # up-to-date + web_version_heatmap=${green_text} + else + # out-of-date + out_of_date_flag="true" + web_version_heatmap=${red_text} + fi + # shorten common branch names (fix/, tweak/, new/) + # use the first 7 characters of the branch name as version + WEB_VERSION="$(printf '%s' "$WEB_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)" + fi + fi + else + # Web interface not installed + WEB_VERSION="N/A" + web_version_heatmap=${yellow_text} + fi + + # Gather FTL version information... + # Extract vx.xx or vx.xx.xxx version + FTL_VERSION="$(echo "${FTL_VERSION}" | grep -oE '^v[0-9]+([.][0-9]+){1,2}')" + if [ "${FTL_BRANCH}" = "master" ]; then + ftl_version_converted="$(VersionConverter "${FTL_VERSION}")" + ftl_version_latest_converted=$(VersionConverter "${GITHUB_FTL_VERSION}") + + if [ "${ftl_version_converted}" -lt "${ftl_version_latest_converted}" ]; then + out_of_date_flag="true" + ftl_version_heatmap=${red_text} + else + ftl_version_heatmap=${green_text} + fi + else + # Custom branch + if [ -z "${FTL_BRANCH}" ]; then + # Branch name is empty, something went wrong + ftl_version_heatmap=${red_text} + FTL_VERSION="?" + else + if [ "${FTL_HASH}" = "${GITHUB_FTL_HASH}" ]; then + # up-to-date + ftl_version_heatmap=${green_text} + else + # out-of-date + out_of_date_flag="true" + ftl_version_heatmap=${red_text} + fi + # shorten common branch names (fix/, tweak/, new/) + # use the first 7 characters of the branch name as version + FTL_VERSION="$(printf '%s' "$FTL_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)" + fi + fi + +} + +GetPADDInformation() { + # If PADD is running inside docker, immediately return without checking for an update + if [ -n "${DOCKER_VERSION}" ]; then + return + fi + + # PADD version information... + padd_version_latest="$(curl --silent https://api.github.com/repos/pi-hole/PADD/releases/latest | grep '"tag_name":' | awk -F \" '{print $4}')" + # is PADD up-to-date? + padd_out_of_date_flag=false + if [ -z "${padd_version_latest}" ]; then + padd_version_heatmap=${yellow_text} + else + padd_version_latest_converted="$(VersionConverter "${padd_version_latest}")" + padd_version_converted=$(VersionConverter "${padd_version}") + + if [ "${padd_version_converted}" -lt "${padd_version_latest_converted}" ]; then + padd_out_of_date_flag="true" + padd_version_heatmap=${red_text} + else + # local and remote PADD version match or local is newer + padd_version_heatmap=${green_text} + fi + fi +} + +GenerateSizeDependendOutput() { + if [ "$1" = "pico" ] || [ "$1" = "nano" ]; then + ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 9 "color") + + elif [ "$1" = "micro" ]; then + ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 10 "color") + + elif [ "$1" = "mini" ]; then + ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 20 "color") + + latest_blocked=$(truncateString "$latest_blocked_raw" 29) + top_blocked=$(truncateString "$top_blocked_raw" 29) + + elif [ "$1" = "tiny" ]; then + ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 30 "color") + + latest_blocked=$(truncateString "$latest_blocked_raw" 41) + top_blocked=$(truncateString "$top_blocked_raw" 41) + top_domain=$(truncateString "$top_domain_raw" 41) + top_client=$(truncateString "$top_client_raw" 41) + + elif [ "$1" = "regular" ] || [ "$1" = "slim" ]; then + ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 40 "color") + + latest_blocked=$(truncateString "$latest_blocked_raw" 48) + top_blocked=$(truncateString "$top_blocked_raw" 48) + top_domain=$(truncateString "$top_domain_raw" 48) + top_client=$(truncateString "$top_client_raw" 48) + + + elif [ "$1" = "mega" ]; then + ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 30 "color") + + latest_blocked=$(truncateString "$latest_blocked_raw" 68) + top_blocked=$(truncateString "$top_blocked_raw" 68) + top_domain=$(truncateString "$top_domain_raw" 68) + top_client=$(truncateString "$top_client_raw" 68) + + fi + + # System uptime + if [ "$1" = "pico" ] || [ "$1" = "nano" ] || [ "$1" = "micro" ]; then + system_uptime=$(echo "${system_uptime_raw}" | awk -F'( |,|:)+' '{if ($7=="min") m=$6; else {if ($7~/^day/){if ($9=="min") {d=$6;m=$8} else {d=$6;h=$8;m=$9}} else {h=$6;m=$7}}} {print d+0,"days,",h+0,"hours"}') + else + system_uptime=$(echo "${system_uptime_raw}" | awk -F'( |,|:)+' '{if ($7=="min") m=$6; else {if ($7~/^day/){if ($9=="min") {d=$6;m=$8} else {d=$6;h=$8;m=$9}} else {h=$6;m=$7}}} {print d+0,"days,",h+0,"hours,",m+0,"minutes"}') + fi + + # Bar generations + if [ "$1" = "mini" ]; then + cpu_bar=$(BarGenerator "${cpu_percent}" 20) + memory_bar=$(BarGenerator "${memory_percent}" 20) + elif [ "$1" = "tiny" ]; then + cpu_bar=$(BarGenerator "${cpu_percent}" 7) + memory_bar=$(BarGenerator "${memory_percent}" 7) + else + cpu_bar=$(BarGenerator "${cpu_percent}" 10) + memory_bar=$(BarGenerator "${memory_percent}" 10) + fi +} + +SetStatusMessage() { + # depending on which flags are set, the "message field" shows a different output + # 7 messages are possible (from highest to lowest priority): + + # - System is hot + # - FTLDNS service is not running + # - Pi-hole's DNS server is off (FTL running, but not providing DNS) + # - Unable to determine Pi-hole blocking status + # - Pi-hole blocking disabled + # - Updates are available + # - Everything is fine + + + if [ "${hot_flag}" = true ]; then + # Check if CPU temperature is high + pico_status="${pico_status_hot}" + mini_status="${mini_status_hot} ${blinking_text}${red_text}${temperature}${reset_text}" + tiny_status="${tiny_status_hot} ${blinking_text}${red_text}${temperature}${reset_text}" + full_status="${full_status_hot} ${blinking_text}${red_text}${temperature}${reset_text}" + mega_status="${mega_status_hot} ${blinking_text}${red_text}${temperature}${reset_text}" + + elif [ "${ftl_down_flag}" = true ]; then + # Check if FTL is down + pico_status=${pico_status_ftl_down} + mini_status=${mini_status_ftl_down} + tiny_status=${tiny_status_ftl_down} + full_status=${full_status_ftl_down} + mega_status=${mega_status_ftl_down} + + elif [ "${dns_down_flag}" = true ]; then + # Check if DNS is down + pico_status=${pico_status_dns_down} + mini_status=${mini_status_dns_down} + tiny_status=${tiny_status_dns_down} + full_status=${full_status_dns_down} + mega_status=${mega_status_dns_down} + + elif [ "${blocking_status}" = "unknown" ]; then + # Check if blocking status is unknown + pico_status=${pico_status_unknown} + mini_status=${mini_status_unknown} + tiny_status=${tiny_status_unknown} + full_status=${full_status_unknown} + mega_status=${mega_status_unknown} + + elif [ "${blocking_status}" = "disabled" ]; then + # Check if blocking status is disabled + pico_status=${pico_status_off} + mini_status=${mini_status_off} + tiny_status=${tiny_status_off} + full_status=${full_status_off} + mega_status=${mega_status_off} + + elif [ "${out_of_date_flag}" = "true" ] || [ "${padd_out_of_date_flag}" = "true" ]; then + # Check if one of the components of Pi-hole (or PADD itself) is out of date + pico_status=${pico_status_update} + mini_status=${mini_status_update} + tiny_status=${tiny_status_update} + full_status=${full_status_update} + mega_status=${mega_status_update} + + elif [ "${blocking_status}" = "enabled" ]; then + # if we reach this point and blocking is enabled, everything is fine + pico_status=${pico_status_ok} + mini_status=${mini_status_ok} + tiny_status=${tiny_status_ok} + full_status=${full_status_ok} + mega_status=${mega_status_ok} + fi +} + +############################################# PRINTERS ############################################# + +PrintLogo() { + if [ -n "${DOCKER_VERSION}" ]; then + version_info="Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text}" + else + version_info="Pi-hole® ${core_version_heatmap}${CORE_VERSION}${reset_text}, Web ${web_version_heatmap}${WEB_VERSION}${reset_text}, FTL ${ftl_version_heatmap}${FTL_VERSION}${reset_text}" + fi + + # Screen size checks + if [ "$1" = "pico" ]; then + printf "%s${clear_line}\n" "p${padd_text} ${pico_status}" + elif [ "$1" = "nano" ]; then + printf "%s${clear_line}\n" "n${padd_text} ${mini_status}" + elif [ "$1" = "micro" ]; then + printf "%s${clear_line}\n${clear_line}\n" "µ${padd_text} ${mini_status}" + elif [ "$1" = "mini" ]; then + printf "%s${clear_line}\n${clear_line}\n" "${padd_text}${dim_text}mini${reset_text} ${mini_status}" + elif [ "$1" = "tiny" ]; then + printf "%s${clear_line}\n" "${padd_text}${dim_text}tiny${reset_text} ${version_info}${reset_text}" + printf "%s${clear_line}\n" " PADD ${padd_version_heatmap}${padd_version}${reset_text} ${tiny_status}${reset_text}" + elif [ "$1" = "slim" ]; then + printf "%s${clear_line}\n${clear_line}\n" "${padd_text}${dim_text}slim${reset_text} ${full_status}" + elif [ "$1" = "regular" ] || [ "$1" = "slim" ]; then + printf "%s${clear_line}\n" "${padd_logo_1}" + printf "%s${clear_line}\n" "${padd_logo_2}${version_info}${reset_text}" + printf "%s${clear_line}\n${clear_line}\n" "${padd_logo_3}PADD ${padd_version_heatmap}${padd_version}${reset_text} ${full_status}${reset_text}" + # normal or not defined + else + printf "%s${clear_line}\n" "${padd_logo_1}" + printf "%s${clear_line}\n" "${padd_logo_2} ${version_info}, PADD ${padd_version_heatmap}${padd_version}${reset_text}" + printf "%s${clear_line}\n${clear_line}\n" "${padd_logo_3} ${dns_check_box} DNS ${ftl_check_box} FTL ${mega_status}${reset_text}" + fi +} + +PrintDashboard() { + if [ -n "${DOCKER_VERSION}" ]; then + version_info="Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text}" + else + version_info="Pi-hole® ${core_version_heatmap}${CORE_VERSION}${reset_text}, Web ${web_version_heatmap}${WEB_VERSION}${reset_text}, FTL ${ftl_version_heatmap}${FTL_VERSION}${reset_text}" + fi + # Move cursor to (0,0). + printf '\e[H' + + # adds the y-offset + moveYOffset + + # mega is a screen with at least 80 columns and 26 lines + moveXOffset; printf "%s${clear_line}\n" "${padd_logo_1}" + moveXOffset; printf "%s${clear_line}\n" "${padd_logo_2} ${version_info}, PADD ${padd_version_heatmap}${padd_version}${reset_text}" + moveXOffset; printf "%s${clear_line}\n" "${padd_logo_3} ${dns_check_box} DNS ${ftl_check_box} FTL ${mega_status}${reset_text}" + moveXOffset; printf "%s${clear_line}\n" "" + moveXOffset; printf "%s${clear_line}\n" "${bold_text}STATS ==========================================================================${reset_text}" + moveXOffset; printf " %-10s%-19s %-10s[%-40s] %-5s${clear_line}\n" "Blocking:" "${domains_being_blocked} domains" "Piholed:" "${ads_blocked_bar}" "${ads_percentage_today}%" + moveXOffset; printf " %-10s%-30s%-29s${clear_line}\n" "Clients:" "${clients}" " ${ads_blocked_today} out of ${dns_queries_today} queries" + moveXOffset; printf " %-10s%-39s${clear_line}\n" "Top Ad:" "${top_blocked}" + moveXOffset; printf "%s${clear_line}\n" "${bold_text}FTL ============================================================================${reset_text}" + moveXOffset; printf " %-10s%-9s %-10s%-9s %-10s%-9s${clear_line}\n" "PID:" "${ftlPID}" "CPU Use:" "${ftl_cpu}" "Mem. Use:" "${ftl_mem_percentage}" + moveXOffset; printf " %-10s%-69s${clear_line}\n" "DNSCache:" "${cache_inserts} insertions, ${cache_deletes} deletions, ${cache_size} total entries" + moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK ========================================================================${reset_text}" + moveXOffset; printf " %-10s%-15s %-4s%-9s %-4s%-9s${clear_line}\n" "Interfce:" "${iface_name}" "TX:" "${tx_bytes}" "RX:" "${rx_bytes}" + moveXOffset; printf "%s${clear_line}\n" "${bold_text}SYSTEM =========================================================================${reset_text}" + moveXOffset; printf " %-10s%-39s${clear_line}\n" "Device:" "${sys_model}" + moveXOffset; printf " %-10s%-39s %-10s[${memory_heatmap}%-10s${reset_text}] %-6s${clear_line}\n" "Uptime:" "${system_uptime}" "Memory:" "${memory_bar}" "${memory_percent}%" + moveXOffset; printf " %-10s${temp_heatmap}%-10s${reset_text} %-10s${cpu_load_1_heatmap}%-4s${reset_text}, ${cpu_load_5_heatmap}%-4s${reset_text}, ${cpu_load_15_heatmap}%-7s${reset_text} %-10s[${memory_heatmap}%-10s${reset_text}] %-6s${clear_line}" "CPU Temp:" "${temperature}" "CPU Load:" "${cpu_load_1}" "${cpu_load_5}" "${cpu_load_15}" "CPU Load:" "${cpu_bar}" "${cpu_percent}%" + + # Clear to end of screen (below the drawn dashboard) + # https://vt100.net/docs/vt510-rm/ED.html + printf '\e[0J' +} + +############################################# HELPERS ############################################## + +# Provides a color based on a provided percentage +# takes in one or two parameters +HeatmapGenerator () { + # if one number is provided, just use that percentage to figure out the colors + if [ -z "$2" ]; then + load=$(printf "%.0f" "$1") + # if two numbers are provided, do some math to make a percentage to figure out the colors + else + load=$(printf "%.0f" "$(echo "$1 $2" | awk '{print ($1 / $2) * 100}')") + fi + + # Color logic + # |<- green ->| yellow | red -> + # 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 + if [ "${load}" -lt 75 ]; then + out=${green_text} + elif [ "${load}" -lt 90 ]; then + out=${yellow_text} + else + out=${red_text} + fi + + echo "$out" +} + +# Provides a "bar graph" +# takes in two or three parameters +# $1: percentage filled +# $2: max length of the bar +# $3: colored flag, if "color" backfill with color +BarGenerator() { + # number of filled in cells in the bar + barNumber=$(printf %.f "$(echo "$1 $2" | awk '{print ($1 / 100) * $2}')") + frontFill=$(for i in $(seq "$barNumber"); do printf "%b" "■"; done) + + # remaining "unfilled" cells in the bar + backfillNumber=$(($2-barNumber)) + + # if the filled in cells is less than the max length of the bar, fill it + if [ "$barNumber" -lt "$2" ]; then + # if the bar should be colored + if [ "$3" = "color" ]; then + # fill the rest in color + backFill=$(for i in $(seq $backfillNumber); do printf "%b" "■"; done) + out="${red_text}${frontFill}${green_text}${backFill}${reset_text}" + # else, it shouldn't be colored in + else + # fill the rest with "space" + backFill=$(for i in $(seq $backfillNumber); do printf "%b" "·"; done) + out="${frontFill}${reset_text}${backFill}" + fi + # else, fill it all the way + else + out=$(for i in $(seq "$2"); do printf "%b" "■"; done) + fi + + echo "$out" +} + +# Checks the size of the screen and sets the value of $padd_size +SizeChecker(){ + # adding a tiny delay here to to give the kernel a bit time to + # report new sizes correctly after a terminal resize + # this reduces "flickering" of GenerateSizeDependendOutput() items + # after a terminal re-size + sleep 0.1 + console_width=$(tput cols) + console_height=$(tput lines) + + # Mega + padd_size="mega" + width=80 + height=26 + + # Center the output (default position) + xOffset="$(( (console_width - width) / 2 ))" + yOffset="$(( (console_height - height) / 2 ))" + + # If the user sets an offset option, use it. + if [ -n "$xOffOrig" ]; then + xOffset=$xOffOrig + + # Limit the offset to avoid breaks + xMaxOffset=$((console_width - width)) + if [ "$xOffset" -gt "$xMaxOffset" ]; then + xOffset="$xMaxOffset" + fi + fi + if [ -n "$yOffOrig" ]; then + yOffset=$yOffOrig + + # Limit the offset to avoid breaks + yMaxOffset=$((console_height - height)) + if [ "$yOffset" -gt "$yMaxOffset" ]; then + yOffset="$yMaxOffset" + fi + fi +} + +# converts a given version string e.g. v3.7.1 to 3007001000 to allow for easier comparison of multi digit version numbers +# credits https://apple.stackexchange.com/a/123408 +VersionConverter() { + echo "$@" | tr -d '[:alpha:]' | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; +} + +# get the Telnet API Port FTL is using by parsing `pihole-FTL.conf` +# same implementation as https://github.com/pi-hole/pi-hole/pull/4945 +getFTLAPIPort(){ + local FTLCONFFILE="/etc/pihole/pihole-FTL.conf" + local DEFAULT_FTL_PORT=4711 + local ftl_api_port + + if [ -s "$FTLCONFFILE" ]; then + # if FTLPORT is not set in pihole-FTL.conf, use the default port + ftl_api_port="$({ grep '^FTLPORT=' "${FTLCONFFILE}" || echo "${DEFAULT_FTL_PORT}"; } | cut -d'=' -f2-)" + # Exploit prevention: set the port to the default port if there is malicious (non-numeric) + # content set in pihole-FTL.conf + expr "${ftl_api_port}" : "[^[:digit:]]" > /dev/null && ftl_api_port="${DEFAULT_FTL_PORT}" + else + # if there is no pihole-FTL.conf, use the default port + ftl_api_port="${DEFAULT_FTL_PORT}" + fi + + echo "${ftl_api_port}" + +} + +# returns FTL's PID based on the content of the pihole-FTL.pid file +# honor PIDFILE setting in `pihole-FTL.conf` +getFTLPID() { + local FTLCONFFILE="/etc/pihole/pihole-FTL.conf" + local DEFAULT_PID_FILE="/run/pihole-FTL.pid" + local FTL_PID_FILE + local FTL_PID + + if [ -s "${FTLCONFFILE}" ]; then + # if PIDFILE is not set in pihole-FTL.conf, use the default path + FTL_PID_FILE="$({ grep '^PIDFILE=' "${FTLCONFFILE}" || echo "${DEFAULT_PID_FILE}"; } | cut -d'=' -f2-)" + else + # if there is no pihole-FTL.conf, use the default path + FTL_PID_FILE="${DEFAULT_PID_FILE}" + fi + + if [ -s "${FTL_PID_FILE}" ]; then + # -s: FILE exists and has a size greater than zero + FTL_PID="$(cat "${FTL_PID_FILE}")" + # Exploit prevention: unset the variable if there is malicious content + # Verify that the value read from the file is numeric + expr "${FTL_PID}" : "[^[:digit:]]" > /dev/null && unset FTL_PID + fi + + # If FTL is not running, or the PID file contains malicious stuff, substitute + # negative PID to signal this + FTL_PID=${FTL_PID:=-1} + echo "${FTL_PID}" +} + + +moveYOffset(){ + # moves the cursor yOffset-times down + # https://vt100.net/docs/vt510-rm/CUD.html + # this needs to be guarded, because if the amount is 0, it is adjusted to 1 + # https://terminalguide.namepad.de/seq/csi_cb/ + + if [ "${yOffset}" -gt 0 ]; then + printf '\e[%sB' "${yOffset}" + fi +} + +moveXOffset(){ + # moves the cursor xOffset-times to the right + # https://vt100.net/docs/vt510-rm/CUF.html + # this needs to be guarded, because if the amount is 0, it is adjusted to 1 + # https://terminalguide.namepad.de/seq/csi_cb/ + + if [ "${xOffset}" -gt 0 ]; then + printf '\e[%sC' "${xOffset}" + fi +} + +# Remove undesired strings from sys_model variable - used in GetSystemInformation() function +filterModel() { + FILTERLIST="To be filled by O.E.M.|Not Applicable|System Product Name|System Version|Undefined|Default string|Not Specified|Type1ProductConfigId|INVALID|All Series|�" + + # Description: + # `-v` : set $FILTERLIST into a variable called `list` + # `gsub()` : replace all list items (ignoring case) with an empty string, deleting them + # `{$1=$1}1`: remove all extra spaces. The last "1" evaluates as true, printing the result + echo "$1" | awk -v list="$FILTERLIST" '{IGNORECASE=1; gsub(list,"")}; {$1=$1}1' +} + +# Truncates a given string and appends three '...' +# takes two parameters +# $1: string to truncate +# $2: max length of the string +truncateString() { + local truncatedString length shorted + + length=${#1} + shorted=$(($2-3)) # shorten max allowed length by 3 to make room for the dots + if [ "${length}" -gt "$2" ]; then + # if length of the string is larger then the specified max length + # cut every char from the string exceeding length $shorted and add three dots + truncatedString=$(echo "$1" | cut -c1-$shorted)"..." + echo "${truncatedString}" + else + echo "$1" + fi +} + + + +########################################## MAIN FUNCTIONS ########################################## + +OutputJSON() { + GetSummaryInformation + echo "{\"domains_being_blocked\":${domains_being_blocked_raw},\"dns_queries_today\":${dns_queries_today_raw},\"ads_blocked_today\":${ads_blocked_today_raw},\"ads_percentage_today\":${ads_percentage_today_raw},\"clients\": ${clients}}" +} + +ShowVersion() { + # source version file to check if $DOCKER_VERSION is set + . /etc/pihole/versions + GetPADDInformation + if [ -z "${padd_version_latest}" ]; then + padd_version_latest="N/A" + fi + if [ -n "${DOCKER_VERSION}" ]; then + # Check for latest Docker version + GetVersionInformation + printf "%s${clear_line}\n" " PADD version is ${padd_version} as part of Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text} (Latest Docker: ${GITHUB_DOCKER_VERSION})" + version_info="Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text}" + else + printf "%s${clear_line}\n" " PADD version is ${padd_version_heatmap}${padd_version}${reset_text} (Latest: ${padd_version_latest})" + fi +} + +StartupRoutine(){ + # Get config variables + . /etc/pihole/setupVars.conf + + # Clear the screen and move cursor to (0,0). + # This mimics the 'clear' command. + # https://vt100.net/docs/vt510-rm/ED.html + # https://vt100.net/docs/vt510-rm/CUP.html + # E3 extension `\e[3J` to clear the scrollback buffer see 'man clear' + printf '\e[H\e[2J\e[3J' + + # adds the y-offset + moveYOffset + + # Get versions information + . /etc/pihole/versions + + GetSystemInformation + GetSummaryInformation + GetPiholeInformation + GetNetworkInformation + GetVersionInformation + GetPADDInformation + for i in 3 2 1 + do + sleep 1 + done +} + +NormalPADD() { + + # Trap the window resize signal (handle window resize events) + trap 'TerminalResize' WINCH + + # Generate output that depends on the terminal size + # e.g. Heatmap and barchart + GenerateSizeDependendOutput ${padd_size} + + # Sets the message displayed in the "status field" depending on the set flags + SetStatusMessage + + # Output everything to the screen + PrintDashboard ${padd_size} + +} + +Update() { + # source version file to check if $DOCKER_VERSION is set + . /etc/pihole/versions + + if [ -n "${DOCKER_VERSION}" ]; then + echo "${check_box_info} Update is not supported for Docker" + exit 1 + fi + + GetPADDInformation + + if [ "${padd_out_of_date_flag}" = "true" ]; then + echo "${check_box_info} Updating PADD from ${padd_version} to ${padd_version_latest}" + + padd_script_path=$(realpath "$0") + + if which wget > /dev/null 2>&1; then + echo "${check_box_info} Downloading PADD update via wget ..." + if wget -qO "${padd_script_path}" https://install.padd.sh > /dev/null 2>&1; then + echo "${check_box_good} ... done. Restart PADD for the update to take effect" + else + echo "${check_box_bad} Cannot download PADD update via wget" + echo "${check_box_info} Go to https://install.padd.sh to download the update manually" + exit 1 + fi + elif which curl > /dev/null 2>&1; then + echo "${check_box_info} Downloading PADD update via curl ..." + if curl -sSL https://install.padd.sh -o "${padd_script_path}" > /dev/null 2>&1; then + echo "${check_box_good} ... done. Restart PADD for the update to take effect" + else + echo "${check_box_bad} Cannot download PADD update via curl" + echo "${check_box_info} Go to https://install.padd.sh to download the update manually" + exit 1 + fi + else + echo "${check_box_bad} Cannot download, neither wget nor curl are available" + echo "${check_box_info} Go to https://install.padd.sh to download the update manually" + exit 1 + fi + else + echo "${check_box_good} You are already using the latest PADD version ${padd_version}" + fi + + exit 0 +} + +DisplayHelp() { + cat << EOM + +::: PADD displays stats about your Pi-hole! +::: +::: Note: If no option is passed, then stats are displayed on screen, updated every 5 seconds +::: +::: Options: +::: -xoff [num] set the x-offset, reference is the upper left corner, disables auto-centering +::: -yoff [num] set the y-offset, reference is the upper left corner, disables auto-centering +::: -j, --json output stats as JSON formatted string and exit +::: -u, --update update to the latest version +::: -v, --version show PADD version info +::: -h, --help display this help text + +EOM +} + +CleanExit(){ + # save the return code of the script + err=$? + #clear the line + printf '\e[0K\n' + + # Show the cursor + # https://vt100.net/docs/vt510-rm/DECTCEM.html + printf '\e[?25h' + + # if background sleep is running, kill it + # http://mywiki.wooledge.org/SignalTrap#When_is_the_signal_handled.3F + kill $sleepPID > /dev/null 2>&1 + + exit $err # exit the script with saved $? +} + +TerminalResize(){ + # if a terminal resize is trapped, check the new terminal size and + # kill the sleep function within NormalPADD() to trigger redrawing + # of the Dashboard + SizeChecker + + # Clear the screen and move cursor to (0,0). + # This mimics the 'clear' command. + # https://vt100.net/docs/vt510-rm/ED.html + # https://vt100.net/docs/vt510-rm/CUP.html + # E3 extension `\e[3J` to clear the scrollback buffer (see 'man clear') + + printf '\e[H\e[2J\e[3J' + + kill $sleepPID > /dev/null 2>&1 +} + +main(){ + # Hiding the cursor. + # https://vt100.net/docs/vt510-rm/DECTCEM.html + printf '\e[?25l' + + # Trap on exit + trap 'CleanExit' INT TERM EXIT + + # If setupVars.conf is not present, then PADD is not running on a Pi-hole + # and we are not able to start as StartupRoutine() will fail below + if [ ! -f /etc/pihole/setupVars.conf ]; then + printf "%b" "${check_box_bad} Error!\n PADD only works in conjunction with Pi-hole!\n" + exit 1 + fi + + SizeChecker + + StartupRoutine ${padd_size} + + # Run PADD + NormalPADD +} + +# Process all options (if present) +while [ "$#" -gt 0 ]; do + case "$1" in + "-j" | "--json" ) OutputJSON; exit 0;; + "-u" | "--update" ) Update;; + "-h" | "--help" ) DisplayHelp; exit 0;; + "-v" | "--version" ) ShowVersion; exit 0;; + "-xoff" ) xOffset="$2"; xOffOrig="$2"; shift;; + "-yoff" ) yOffset="$2"; yOffOrig="$2"; shift;; + * ) DisplayHelp; exit 1;; + esac + shift +done + +main