From b21d883fb157d5edb8dbca2e45dee80dc93d54b2 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Wed, 12 Nov 2025 14:33:29 +0100 Subject: [PATCH] MOTD: deterministic spacing, tidy and compact output --- .../bsp/common/etc/update-motd.d/00-clear | 16 +- .../etc/update-motd.d/10-armbian-header | 313 +++++++----------- .../bsp/common/etc/update-motd.d/15-ap-info | 74 +++++ .../bsp/common/etc/update-motd.d/20-ip-info | 112 +++++++ .../etc/update-motd.d/25-containers-info | 53 +++ .../etc/update-motd.d/30-armbian-sysinfo | 308 +++++++++-------- .../common/etc/update-motd.d/35-armbian-tips | 90 +++-- .../bsp/common/etc/update-motd.d/41-commands | 22 +- .../bsp/common/etc/update-motd.d/99-blank | 3 + .../common/usr/lib/armbian/armbian-firstlogin | 1 + 10 files changed, 642 insertions(+), 350 deletions(-) create mode 100755 packages/bsp/common/etc/update-motd.d/15-ap-info create mode 100755 packages/bsp/common/etc/update-motd.d/20-ip-info create mode 100755 packages/bsp/common/etc/update-motd.d/25-containers-info create mode 100755 packages/bsp/common/etc/update-motd.d/99-blank diff --git a/packages/bsp/common/etc/update-motd.d/00-clear b/packages/bsp/common/etc/update-motd.d/00-clear index e20783b23e..9c978b5ce7 100755 --- a/packages/bsp/common/etc/update-motd.d/00-clear +++ b/packages/bsp/common/etc/update-motd.d/00-clear @@ -12,10 +12,20 @@ THIS_SCRIPT="clear" MOTD_DISABLE="" -[[ -f /etc/default/armbian-motd ]] && . /etc/default/armbian-motd +# Clear runtime MOTD state +MOTD_RUN_DIR="/run/armbian-motd" +if [[ -d "$MOTD_RUN_DIR" ]]; then + rm -f "${MOTD_RUN_DIR}/".*.printed 2>/dev/null +else + mkdir -p "$MOTD_RUN_DIR" 2>/dev/null +fi -for f in $MOTD_DISABLE; do - [[ $f == $THIS_SCRIPT ]] && exit 0 +safe_source() { [[ -f "$1" ]] && . "$1"; } + +# Optional overrides +safe_source /etc/default/armbian-motd +for f in ${MOTD_DISABLE}; do + [[ "${f}" == "${THIS_SCRIPT}" ]] && exit 0 done # Clear screen diff --git a/packages/bsp/common/etc/update-motd.d/10-armbian-header b/packages/bsp/common/etc/update-motd.d/10-armbian-header index 5dc1178f83..dd59d67cde 100755 --- a/packages/bsp/common/etc/update-motd.d/10-armbian-header +++ b/packages/bsp/common/etc/update-motd.d/10-armbian-header @@ -2,237 +2,166 @@ # # Copyright (c) Authors: https://www.armbian.com/authors # -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. +# Licensed under the GNU General Public License, version 2. +# Distributed “as is” without any warranty of any kind. -# DO NOT EDIT THIS FILE but add config options to /etc/default/armbian-motd -# any changes will be lost on board support package update +# DO NOT EDIT THIS FILE. +# To customize behavior, override options in /etc/default/armbian-motd. +# Any local edits here will be lost during board support package updates. + +set -o pipefail THIS_SCRIPT="header" MOTD_DISABLE="" -HIDE_IP_PATTERN="^dummy0|^lo|^docker|^hassio|^br-|^veth|^vnet|^virbr" -HIDE_LOCAL_IPV6="true" -# Read image configuration -[[ -f /etc/armbian-image-release ]] && . /etc/armbian-image-release -VENDORTEMP="${VENDOR}" +_motd_mark_after_header() { + local base="/run/armbian-motd" + mkdir -p "$base" 2>/dev/null || base="${XDG_RUNTIME_DIR:-/tmp}/armbian-motd" + mkdir -p "$base" 2>/dev/null || return 0 + local stamp="${base}/.after_header.printed" + [[ -e "$stamp" ]] || { echo ""; : >"$stamp"; } +} -# Read update configuration -[[ -f /etc/armbian-release ]] && . /etc/armbian-release +safe_source() { [[ -f "$1" ]] && . "$1"; } -# Keep the VENDOR from image if its defined there -[[ -n "${VENDORTEMP}" && "${VENDORTEMP}" != "${VENDOR}" ]] && VENDOR="${VENDORTEMP}" +# --- Load metadata from image and release files --- +safe_source /etc/armbian-image-release +VENDORTEMP="${VENDOR:-}" +safe_source /etc/armbian-release +[[ -n "${VENDORTEMP}" && "${VENDORTEMP}" != "${VENDOR:-}" ]] && VENDOR="${VENDORTEMP}" +[[ -n "${VENDORPRETTYNAME:-}" ]] && VENDOR="${VENDORPRETTYNAME}" -# If VENDORPRETTYNAME is defined, used that -[[ -n ${VENDORPRETTYNAME} ]] && VENDOR="${VENDORPRETTYNAME}" +# --- Distribution and upgrade information --- +DISTRIBUTION_CODENAME="" +DISTRIBUTION_ID="" +upgrade="" if [[ -f /etc/armbian-distribution-status ]]; then - . /etc/armbian-distribution-status - # Find a way that works - [[ -f /etc/lsb-release ]] && DISTRIBUTION_CODENAME=$(grep CODENAME /etc/lsb-release | cut -d"=" -f2) - [[ -f /etc/lsb-release ]] && DISTRIBUTION_ID=$(grep DISTRIB_ID /etc/lsb-release | cut -d"=" -f2) - [[ -z "${DISTRIBUTION_CODENAME}" && -f /etc/os-release ]] && DISTRIBUTION_CODENAME=$(grep VERSION_CODENAME /etc/os-release | cut -d"=" -f2) - [[ -z "${DISTRIBUTION_ID}" && -f /etc/os-release ]] && DISTRIBUTION_ID=$(grep "^ID" /etc/os-release | cut -d"=" -f2) - [[ -z "${DISTRIBUTION_CODENAME}" && -x /usr/bin/lsb_release ]] && DISTRIBUTION_CODENAME=$(/usr/bin/lsb_release -c | cut -d":" -f2 | tr -d "\t") - [[ -z "${DISTRIBUTION_ID}" && -x /usr/bin/lsb_release ]] && DISTRIBUTION_ID=$(/usr/bin/lsb_release -i | cut -d":" -f2 | tr -d "\t") - # Read Armbian distribution status - DISTRIBUTION_STATUS=$(grep "^${DISTRIBUTION_CODENAME}" /etc/armbian-distribution-status | cut -d"=" -f2 | cut -d";" -f1) + # Prefer lsb-release (safe to source; no VERSION conflict) + if [[ -f /etc/lsb-release ]]; then + # shellcheck disable=SC1091 + . /etc/lsb-release + DISTRIBUTION_CODENAME="${DISTRIB_CODENAME:-${DISTRIBUTION_CODENAME}}" + DISTRIBUTION_ID="${DISTRIB_ID:-${DISTRIBUTION_ID}}" + fi - # Read upgrade possibilities on stable channel - filter=$(grep "supported" /etc/armbian-distribution-status | cut -d"=" -f1) - upgrade=$(for j in ${filter}; do - for i in $(grep "^${DISTRIBUTION_CODENAME}" /etc/armbian-distribution-status | cut -d";" -f2 | cut -d"=" -f2 | sed "s/,/ /g"); do - if [[ "${i}" == "${j}" ]]; then - echo "${i}" - fi + # If still missing, read only the keys we need from os-release (do NOT source it) + if [[ -z "${DISTRIBUTION_CODENAME}" || -z "${DISTRIBUTION_ID}" ]]; then + if [[ -f /etc/os-release ]]; then + DISTRIBUTION_CODENAME="$(awk -F= '/^VERSION_CODENAME=/{sub(/^"/,"",$2); sub(/"$/,"",$2); print $2}' /etc/os-release)" + DISTRIBUTION_ID="$(awk -F= '/^ID=/{sub(/^"/,"",$2); sub(/"$/,"",$2); print $2}' /etc/os-release)" + fi + fi + + # If still missing, fallback to lsb_release command + if [[ -z "${DISTRIBUTION_CODENAME}" || -z "${DISTRIBUTION_ID}" ]]; then + if command -v lsb_release >/dev/null 2>&1; then + DISTRIBUTION_CODENAME="$(lsb_release -c | awk -F: '{gsub(/\t/,"",$2); print $2}')" + DISTRIBUTION_ID="$(lsb_release -i | awk -F: '{gsub(/\t/,"",$2); print $2}')" + fi + fi + + DISTRIBUTION_STATUS="$(awk -F'[=;]' -v c="${DISTRIBUTION_CODENAME}" '$1==c{print $2; exit}' /etc/armbian-distribution-status)" + filter=$(awk -F= '/supported/{print $1}' /etc/armbian-distribution-status) + current_line="$(awk -F';' -v c="${DISTRIBUTION_CODENAME}" '$1==c{print $2; exit}' /etc/armbian-distribution-status)" + candidates="$(printf '%s\n' "${current_line#*=}" | tr ',' ' ')" + for j in $filter; do + for i in $candidates; do + if [[ "$i" == "$j" ]]; then upgrade="$i"; fi done - done | tail -1) + done fi -[[ -f /etc/default/armbian-motd ]] && . /etc/default/armbian-motd +safe_source /etc/default/armbian-motd for f in ${MOTD_DISABLE}; do [[ "${f}" == "${THIS_SCRIPT}" ]] && exit 0 done -function get_wan_address() { - curl -4 --max-time 2 -s https://ipv4.whatismyip.akamai.com/ -} # get wan ip address +# --- Kernel and system info --- +KERNELID="$(uname -r)" -function get_wan6_address() { - curl -6 --max-time 2 -s https://ipv6.whatismyip.akamai.com/ -} # get wan ip6 address +_motd_mark_after_header -function get_ip_addresses() { - local ipv4s=() - local ipv4 - local address4 - local ipv6s=() - local ipv6 - local address6 - local intf - local f - - for f in /sys/class/net/*; do - intf=$(basename "${f}") - # match only interface names - if [[ ${intf} =~ ${HIDE_IP_PATTERN} ]]; then - continue - else - # List all IP addresses without reordering or duplicates - ipv4=$(ip -4 addr show dev "${intf}") - ipv4=$(echo "${ipv4}" | grep -v "${intf}:avahi") - ipv4=$(echo "${ipv4}" | awk '/inet/ {print $2}') - ipv4=$(echo "${ipv4}" | cut -d'/' -f1) - ipv4=$(echo "${ipv4}" | awk '!x[$0]++') - ipv6=$(ip -6 addr show dev "${intf}") - if [[ "${HIDE_LOCAL_IPV6}" == true ]]; then - ipv6=$(ip -6 addr show dev "${intf}" | grep -v fe80) - fi - ipv6=$(echo "${ipv6}" | grep -v "${intf}:avahi") - ipv6=$(echo "${ipv6}" | awk '/inet6/ {print $2}') - ipv6=$(echo "${ipv6}" | cut -d'/' -f1) - ipv6=$(echo "${ipv6}" | awk '!x[$0]++') - - for address4 in ${ipv4}; do - ipv4s+=("${address4}") - done - for address6 in ${ipv6}; do - ipv6s+=("${address6}") - done - fi - done - - echo "${ipv4s[*]}|${ipv6s[*]}" -} # get_ip_addresses - -# Read Armbian kernel version -KERNELID=$(uname -r) -# Get other variables -ip_address=$(get_ip_addresses &) -wan_ip_address=$(get_wan_address &) -wan_ip6_address=$(get_wan6_address &) - -# Get access point info -if systemctl is-active --quiet service hostapd && [[ -f /etc/hostapd/hostapd.conf ]]; then - . /etc/hostapd/hostapd.conf -fi - -# Display software vendor logo -if [[ -n "${VENDORCOLOR}" ]]; then - echo -e "\e[38;2;${VENDORCOLOR}m$(figlet -f small " ${VENDOR}")\e[0m" +# --- Vendor banner --- +if [[ -n "${VENDORCOLOR:-}" ]]; then + if command -v figlet >/dev/null 2>&1; then + printf '\e[38;2;%sm%s\e[0m\n' "${VENDORCOLOR}" "$(figlet -f small " ${VENDOR}")" + else + printf '\e[38;2;%sm### %s ###\e[0m\n' "${VENDORCOLOR}" "${VENDOR}" + fi else - echo -e "\e[1;91m$(figlet -f small " ${VENDOR}")\e[0m" + if command -v figlet >/dev/null 2>&1; then + printf '\e[1;91m%s\e[0m\n' "$(figlet -f small " ${VENDOR}")" + else + printf '\e[1;91m### %s ###\e[0m\n' "${VENDOR}" + fi fi -# Read RPI model from cpuinfo -if [[ ${BOARD:-} == rpi4b ]]; then - BOARD_NAME=$(tr '\0' '\n' < /proc/device-tree/model | sed -E 's/ Rev [0-9.]+$//') +# --- Board name and version line --- +# Keep BOARD_NAME from Armbian files; only special-case RPi4 model tweak. +if [[ "${BOARD:-}" == "rpi4b" ]]; then + BOARD_NAME="$(tr '\0' '\n' < /proc/device-tree/model | sed -E 's/ Rev [0-9.]+$//')" fi -# Display version, board, and kernel version -[[ ${VERSION} == *trunk* ]] && VERSION=$(echo -e ${VERSION} | cut -d"." -f1-2 | sed "s/\$/ rolling/") -echo -e " \e[0;92mv${VERSION}\x1B[0m for ${BOARD_NAME} running Armbian Linux \e[0;92m${KERNELID^}\x1B[0m" +# Preserve Armbian VERSION; if it ends with "trunk", show "rolling" +if [[ "${VERSION:-}" == *trunk* ]]; then + VERSION="$(printf '%s' "${VERSION}" | cut -d'.' -f1-2)" + VERSION="${VERSION} rolling" +fi -# render image and board type +printf ' \e[0;92mv%s\x1B[0m for %s running Armbian Linux \e[0;92m%s\x1B[0m\n' \ + "${VERSION}" "${BOARD_NAME}" "${KERNELID}" + +# --- Hardware support status --- +HARDWARE_STATUS="" if [[ "${IMAGE_TYPE:-}" != "stable" ]]; then - [[ "${IMAGE_TYPE}" == "user-built" ]] && HARDWARE_STATUS="\e[0;91mDIY\x1B[0m (custom image)\x1B[0m" - [[ "${IMAGE_TYPE}" == "nightly" ]] && HARDWARE_STATUS="\e[0;91mfor advanced users\x1B[0m (rolling release)\x1B[0m" + case "${IMAGE_TYPE:-}" in + "user-built") HARDWARE_STATUS=$'\e[0;91mDIY\x1B[0m (custom image)\x1B[0m' ;; + "nightly") HARDWARE_STATUS=$'\e[0;91mfor advanced users\x1B[0m (rolling release)\x1B[0m' ;; + esac else - [[ "${BOARD_TYPE:-}" == "csc" || "${BOARD_TYPE}" == "tvb" ]] && HARDWARE_STATUS="\e[0;91mDIY (community maintained)\x1B[0m" - [[ "${BOARD_TYPE}" == "wip" ]] && HARDWARE_STATUS="\e[0;91mfor advanced users\x1B[0m (work in progress)\x1B[0m" - [[ "${BOARD_TYPE}" == "eos" ]] && HARDWARE_STATUS="\e[0;91mend of life\x1B[0m" + case "${BOARD_TYPE:-}" in + "csc"|"tvb") HARDWARE_STATUS=$'\e[0;91mDIY (community maintained)\x1B[0m' ;; + "wip") HARDWARE_STATUS=$'\e[0;91mfor advanced users\x1B[0m (work in progress)\x1B[0m' ;; + "eos") HARDWARE_STATUS=$'\e[0;91mend of life\x1B[0m' ;; + esac fi -# render distribution status -if [[ ${DISTRIBUTION_STATUS} == supported ]]; then - DISTRO_STATUS="\e[0;92mstable\e[0m (${DISTRIBUTION_CODENAME})" -elif [[ ${DISTRIBUTION_STATUS} == eos ]]; then - DISTRO_STATUS="\e[0;91mend of life\e[0m (${DISTRIBUTION_CODENAME})" -else - DISTRO_STATUS="\e[0;93mrolling\e[0m (${DISTRIBUTION_CODENAME})" -fi +# --- Distribution status line --- +case "${DISTRIBUTION_STATUS:-}" in + supported) DISTRO_STATUS=$'\e[0;92mstable\e[0m' ;; + eos) DISTRO_STATUS=$'\e[0;91mend of life\e[0m' ;; + *) DISTRO_STATUS=$'\e[0;93mrolling\e[0m' ;; +esac +[[ -n "${DISTRIBUTION_CODENAME}" ]] && DISTRO_STATUS+=" (${DISTRIBUTION_CODENAME})" -# read packages update status -[[ -f /var/cache/apt/archives/updates.number ]] && . /var/cache/apt/archives/updates.number -if [[ ${NUM_UPDATES:-} -gt 0 ]]; then - if apt-mark showhold | grep -q linux-image 2> /dev/null; then +# --- Updates status --- +if [[ -f /var/cache/apt/archives/updates.number ]]; then + # shellcheck disable=SC1091 + . /var/cache/apt/archives/updates.number +fi +UPDATE_STATUS="" +if [[ "${NUM_UPDATES:-0}" -gt 0 ]]; then + if apt-mark showhold 2>/dev/null | grep -q '^linux-image'; then UPDATE_STATUS="Kernel upgrade \e[0;91mdisabled\e[0m" else UPDATE_STATUS="Kernel upgrade \e[0;92menabled\e[0m" fi UPDATE_STATUS+=" and \e[1;92m${NUM_UPDATES}\e[0m package" - # Cosmetic is important - [[ ${NUM_UPDATES} -gt 1 ]] && UPDATE_STATUS+="s" + [[ "${NUM_UPDATES}" -gt 1 ]] && UPDATE_STATUS+="s" UPDATE_STATUS+=" available for upgrade\e[0m " fi -# read running Docker containers if any -if systemctl is-active docker > /dev/null; then - CONTAINERS_STATUS=$(docker ps --format "{{.Names}}" | tr '\n' ',' | sed 's/,/, /g' | sed 's/, $//') +printf '\n' + +# --- Display summary --- +if [[ -n "${DISTRO_STATUS:-}" ]]; then + [[ -n "${upgrade}" ]] && DISTRO_STATUS+=", possible distro upgrade (${upgrade})" + printf ' Packages: %s %b\n' "${DISTRIBUTION_ID^}" "${DISTRO_STATUS}" fi -echo "" +[[ -n "${UPDATE_STATUS}" ]] && printf ' Updates: %b\n' "${UPDATE_STATUS}" +[[ -n "${HARDWARE_STATUS}" ]] && printf ' Support: %b\n' "${HARDWARE_STATUS}" -# Display packages status -if [[ -n ${DISTRO_STATUS} ]]; then - if [[ -n "${upgrade}" ]]; then - DISTRO_STATUS+=", possible distro upgrade (${upgrade})" - fi - echo -e " Packages: ${DISTRIBUTION_ID^} ${DISTRO_STATUS}" -fi - -# Display available updates -if [[ -n ${UPDATE_STATUS} ]]; then - echo -e " Updates: ${UPDATE_STATUS}" -fi - -# Display hardware support status -if [[ -n ${HARDWARE_STATUS} ]]; then - echo -e " Support: ${HARDWARE_STATUS}" -fi - -# Display IP addresses -IFS='|' read -r ipv4s ipv6s <<< "${ip_address}" -# remove WAN IPv4 from the list of all IPv4 -if [[ "${ipv4s}" == *${wan_ip_address}* ]]; then - ipv4s=$(echo $ipv4s | sed "s/"${wan_ip_address}"//g" | xargs ) -fi -if [[ -n ${ipv4s} || -n ${wan_ip_address} ]]; then - all_ip_address=" IPv4: " - if [[ -n ${ipv4s} ]]; then - all_ip_address+=" \x1B[93m(LAN)\x1B[0m " - fi - all_ip_address+="\x1B[92m${ipv4s// /, }\x1B[0m " - if [[ -n ${wan_ip_address} ]]; then - all_ip_address+="\x1B[93m(WAN)\x1B[0m \x1B[95m${wan_ip_address}\x1B[0m " - fi - echo -e "${all_ip_address}" -fi -# remove WAN IPv6 from the list of all IPv6 -if [[ -n "${wan_ip6_address}" && "${ipv6s}" == *${wan_ip6_address}* ]]; then - ipv6s=$(echo $ipv6s | sed "s/"${wan_ip6_address}"//g" | xargs ) -fi - -if [[ -n ${ipv6s} || -n ${wan_ip6_address} ]]; then - all_ip6_address=" IPv6: " - if [[ -n ${ipv6s} ]]; then - all_ip6_address+="\x1B[96m${ipv6s// /, }\x1B[0m " - fi - if [[ -n ${wan_ip6_address} ]]; then - all_ip6_address+="\x1B[93m(WAN)\x1B[0m \x1B[95m${wan_ip6_address}\x1B[0m " - fi - echo -e "${all_ip6_address}" -fi - -# Display running docker containers -if [[ -n "${CONTAINERS_STATUS}" ]]; then - echo -e " Containers: \x1B[92m${CONTAINERS_STATUS}\x1B[0m" -fi - -# Display hostapd -if [[ -n ${ssid} ]]; then - echo -e " WiFi AP: SSID: (\x1B[91m${ssid}\x1B[0m), $(iw dev "${interface:-}" info | grep channel | xargs)" -fi - -echo "" +# ensure zero exit when executed directly +(return 0 2>/dev/null) || exit 0 \ No newline at end of file diff --git a/packages/bsp/common/etc/update-motd.d/15-ap-info b/packages/bsp/common/etc/update-motd.d/15-ap-info new file mode 100755 index 0000000000..66e7328cf8 --- /dev/null +++ b/packages/bsp/common/etc/update-motd.d/15-ap-info @@ -0,0 +1,74 @@ +#!/bin/bash +# Print WiFi Access Point info for Armbian MOTD. +# Detects active hostapd instances, parses SSID + interface, shows channel via `iw`. + +set -o pipefail + +# one-time spacer after header (marks that something printed before Performance) +_motd_mark_after_header() { + # prefer /run, fallback to user runtime or /tmp (works when not root) + local base="/run/armbian-motd" + mkdir -p "$base" 2>/dev/null || base="${XDG_RUNTIME_DIR:-/tmp}/armbian-motd" + mkdir -p "$base" 2>/dev/null || return 0 + local stamp="${base}/.after_header.printed" + [[ -e "$stamp" ]] || { echo ""; : >"$stamp"; } +} + +safe_source() { [[ -f "$1" ]] && . "$1"; } + +THIS_SCRIPT="ap-info" +MOTD_DISABLE="" + +# Optional overrides +safe_source /etc/default/armbian-motd +for f in ${MOTD_DISABLE}; do + [[ "${f}" == "${THIS_SCRIPT}" ]] && exit 0 +done + +# helper: pretty-print one AP line +print_ap_line() { + local ssid="$1" iface="$2" + local chan + if command -v iw >/dev/null 2>&1; then + # Grab the whole "channel ..." line from iw and use it as-is + chan="$(iw dev "$iface" info 2>/dev/null \ + | sed -n 's/^[[:space:]]*channel[[:space:]]\+\(.*\)$/\1/p' \ + | head -1)" + fi + printf ' WiFi AP: SSID: (\x1B[91m%s\x1B[0m), channel %s\n' \ + "$ssid" "${chan:-n/a}" +} + +# detect active hostapd services +if systemctl is-active --quiet hostapd.service \ + || systemctl list-units 'hostapd@*.service' --state=active >/dev/null 2>&1; then + + # collect hostapd configs (classic and templated), skip non-existing + confs=() + for p in /etc/hostapd/hostapd.conf /etc/hostapd/hostapd-*.conf; do + [[ -f "$p" ]] && confs+=("$p") + done + + # print at most one line per iface+ssid combo + declare -A _ap_printed=() + first=1 + for cfg in "${confs[@]}"; do + ssid="$(grep -E '^[[:space:]]*ssid[[:space:]]*=' "$cfg" | sed -E 's/.*=//;s/^[[:space:]]+|[[:space:]]+$//')" + iface="$(grep -E '^[[:space:]]*interface[[:space:]]*=' "$cfg" | sed -E 's/.*=//;s/^[[:space:]]+|[[:space:]]+$//')" + + # fall back to iw to discover iface if config lacks it + [[ -z "$iface" ]] && iface="$(iw dev 2>/dev/null | awk '/Interface/ {print $2; exit}')" + + key="${iface}|${ssid}" + [[ -z "$ssid" || -z "$iface" || -n "${_ap_printed[$key]+x}" ]] && continue + + # print exactly one spacer before the first AP line + if (( first )); then _motd_mark_after_header; first=0; fi + + print_ap_line "$ssid" "$iface" + _ap_printed[$key]=1 + done +fi + +# ensure zero exit when executed directly +(return 0 2>/dev/null) || exit 0 diff --git a/packages/bsp/common/etc/update-motd.d/20-ip-info b/packages/bsp/common/etc/update-motd.d/20-ip-info new file mode 100755 index 0000000000..cc140b641c --- /dev/null +++ b/packages/bsp/common/etc/update-motd.d/20-ip-info @@ -0,0 +1,112 @@ +#!/bin/bash +# Print LAN/WAN IPv4 and IPv6 lines for Armbian MOTD. +# Standalone: discovers addresses itself and prints formatted output. + +set -o pipefail + +safe_source() { [[ -f "$1" ]] && . "$1"; } + +THIS_SCRIPT="ip-info" +MOTD_DISABLE="" + +# Defaults (can be overridden by /etc/default/armbian-motd if present) +HIDE_IP_PATTERN='^dummy0|^lo|^docker|^hassio|^br-|^veth|^vnet|^virbr' +HIDE_LOCAL_IPV6="true" + +# Optional overrides +safe_source /etc/default/armbian-motd +for f in ${MOTD_DISABLE}; do + [[ "${f}" == "${THIS_SCRIPT}" ]] && exit 0 +done + +# one-time spacer after header (marks that something printed before Performance) +_motd_mark_after_header() { + local base="/run/armbian-motd" + mkdir -p "$base" 2>/dev/null || base="${XDG_RUNTIME_DIR:-/tmp}/armbian-motd" + mkdir -p "$base" 2>/dev/null || return 0 + local stamp="${base}/.after_header.printed" + [[ -e "$stamp" ]] || { echo ""; : >"$stamp"; } +} + +get_wan_address() { curl -4 -fsS --max-time 2 https://ipv4.whatismyip.akamai.com/ 2>/dev/null || true; } +get_wan6_address() { curl -6 -fsS --max-time 2 https://ipv6.whatismyip.akamai.com/ 2>/dev/null || true; } + +get_lan_ips() { + # echo "|" + local ipv4s=() ipv6s=() intf + + for p in /sys/class/net/*; do + intf="${p##*/}" + [[ "$intf" =~ $HIDE_IP_PATTERN ]] && continue + + # IPv4 (order preserved, unique) + mapfile -t add4 < <(ip -4 -o addr show dev "$intf" 2>/dev/null \ + | awk '$3=="inet"{print $4}' | cut -d/ -f1 || true) + for a in "${add4[@]}"; do + [[ " ${ipv4s[*]} " == *" $a "* ]] || ipv4s+=("$a") + done + + # IPv6 (prefer scope global when hiding link-local) + if [[ "${HIDE_LOCAL_IPV6}" == "true" ]]; then + mapfile -t add6 < <(ip -6 -o addr show dev "$intf" scope global 2>/dev/null \ + | awk '$3=="inet6"{print $4}' | cut -d/ -f1 || true) + else + mapfile -t add6 < <(ip -6 -o addr show dev "$intf" 2>/dev/null \ + | awk '$3=="inet6"{print $4}' | cut -d/ -f1 || true) + fi + for a in "${add6[@]}"; do + [[ " ${ipv6s[*]} " == *" $a "* ]] || ipv6s+=("$a") + done + done + + printf '%s|%s\n' "${ipv4s[*]}" "${ipv6s[*]}" +} + +print_ip_lines() { + local ipv4s="$1" ipv6s="$2" wan4="$3" wan6="$4" + # print a single blank line before the first post-header section + _motd_spacer() { + local tag="${1:-after-header}" + local dir="/run/armbian-motd"; local stamp="$dir/.${tag}.printed" + mkdir -p "$dir" 2>/dev/null + [[ -e "$stamp" ]] || { echo ""; : > "$stamp"; } + } + + # Drop WAN duplicates from LAN lists + if [[ -n "$wan4" && " $ipv4s " == *" $wan4 "* ]]; then + ipv4s="$(printf '%s\n' $ipv4s | awk -v w="$wan4" '$0!=w' | paste -sd' ' -)" + fi + if [[ -n "$wan6" && " $ipv6s " == *" $wan6 "* ]]; then + ipv6s="$(printf '%s\n' $ipv6s | awk -v w="$wan6" '$0!=w' | paste -sd' ' -)" + fi + + # IPv4 line + if [[ -n "$ipv4s" || -n "$wan4" ]]; then + _motd_mark_after_header + local line=" IPv4: " + [[ -n "$ipv4s" ]] && line+=" \x1B[93m(LAN)\x1B[0m \x1B[92m${ipv4s// /, }\x1B[0m " + [[ -n "$wan4" ]] && line+="\x1B[93m(WAN)\x1B[0m \x1B[95m${wan4}\x1B[0m " + printf '%b\n' "$line" + fi + + # IPv6 line + if [[ -n "$ipv6s" || -n "$wan6" ]]; then + _motd_mark_after_header + local line=" IPv6: " + [[ -n "$ipv6s" ]] && line+="\x1B[96m${ipv6s// /, }\x1B[0m " + [[ -n "$wan6" ]] && line+="\x1B[93m(WAN)\x1B[0m \x1B[95m${wan6}\x1B[0m " + printf '%b\n' "$line" + fi +} + +main() { + local combo ipv4s ipv6s wan4 wan6 + combo="$(get_lan_ips)" + IFS='|' read -r ipv4s ipv6s <<< "$combo" + wan4="$(get_wan_address)" + wan6="$(get_wan6_address)" + print_ip_lines "$ipv4s" "$ipv6s" "$wan4" "$wan6" +} + +# If sourced, don't run main; if executed, run. +(return 0 2>/dev/null) || main diff --git a/packages/bsp/common/etc/update-motd.d/25-containers-info b/packages/bsp/common/etc/update-motd.d/25-containers-info new file mode 100755 index 0000000000..0de12993c7 --- /dev/null +++ b/packages/bsp/common/etc/update-motd.d/25-containers-info @@ -0,0 +1,53 @@ +#!/bin/bash +# Print running container names for Armbian MOTD (Docker). +# Standalone: discovers containers itself and prints a formatted line, +# identical to the original header behavior. + +set -o pipefail + +safe_source() { [[ -f "$1" ]] && . "$1"; } + +THIS_SCRIPT="containers-info" +MOTD_DISABLE="" + +# Optional overrides +safe_source /etc/default/armbian-motd +for f in ${MOTD_DISABLE}; do + [[ "${f}" == "${THIS_SCRIPT}" ]] && exit 0 +done + +_motd_spacer() { + local tag="${1:-after-header}" + local dir="/run/armbian-motd"; local stamp="$dir/.${tag}.printed" + mkdir -p "$dir" 2>/dev/null + [[ -e "$stamp" ]] || { echo ""; : > "$stamp"; } +} + +# one-time spacer after header (marks that something printed before Performance) +_motd_mark_after_header() { + local base="/run/armbian-motd" + mkdir -p "$base" 2>/dev/null || base="${XDG_RUNTIME_DIR:-/tmp}/armbian-motd" + mkdir -p "$base" 2>/dev/null || return 0 + local stamp="${base}/.after_header.printed" + [[ -e "$stamp" ]] || { echo ""; : >"$stamp"; } +} + + +list_docker_names() { + # Only show names if Docker is active and CLI is present + if systemctl is-active --quiet docker && command -v docker >/dev/null 2>&1; then + docker ps --format '{{.Names}}' 2>/dev/null | paste -sd', ' - + fi +} + +main() { + local names + names="$(list_docker_names)" + if [[ -n "$names" ]]; then + _motd_mark_after_header + printf ' Containers: \x1B[92m%s\x1B[0m\n' "$names" + fi +} + +# If sourced, don't run main; if executed, run. +(return 0 2>/dev/null) || { main; exit 0; } diff --git a/packages/bsp/common/etc/update-motd.d/30-armbian-sysinfo b/packages/bsp/common/etc/update-motd.d/30-armbian-sysinfo index 7707b18201..b513a3017e 100755 --- a/packages/bsp/common/etc/update-motd.d/30-armbian-sysinfo +++ b/packages/bsp/common/etc/update-motd.d/30-armbian-sysinfo @@ -7,186 +7,236 @@ # warranty of any kind, whether express or implied. # -# DO NOT EDIT THIS FILE but add config options to /etc/default/armbian-motd -# generate system information +# DO NOT EDIT THIS FILE — use /etc/default/armbian-motd for overrides. + +set -o pipefail export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin THIS_SCRIPT="sysinfo" -# don't change default values - they are lost on upgrades. Use /etc/default/armbian-motd for your values - +# Defaults (override in /etc/default/armbian-motd) MOTD_DISABLE="" PRIMARY_INTERFACE="eth0" -PRIMARY_DIRECTION="rx" -STORAGE=/dev/sda1 +PRIMARY_DIRECTION="rx" # rx | tx | both | (anything else to disable) +STORAGE="/dev/sda1" CPU_TEMP_LIMIT=60 -# Temperature offset in Celcius degrees -CPU_TEMP_OFFSET=0 +CPU_TEMP_OFFSET=0 # °C offset HDD_TEMP_LIMIT=60 AMB_TEMP_LIMIT=40 [[ -f /etc/default/armbian-motd ]] && . /etc/default/armbian-motd -for f in $MOTD_DISABLE; do - [[ $f == $THIS_SCRIPT ]] && exit 0 +for f in ${MOTD_DISABLE}; do + [[ "$f" == "$THIS_SCRIPT" ]] && exit 0 done -# don't edit below here +# Optional vendor helpers (ambienttemp, batteryinfo, getboardtemp) +if [[ -r /usr/lib/armbian/armbian-allwinner-battery ]]; then + # shellcheck disable=SC1091 + . /usr/lib/armbian/armbian-allwinner-battery +fi +# Provide safe no-op stubs if functions are missing +command -v ambienttemp >/dev/null 2>&1 || ambienttemp() { echo ""; } +command -v batteryinfo >/dev/null 2>&1 || batteryinfo() { :; } +command -v getboardtemp >/dev/null 2>&1 || getboardtemp() { board_temp=""; } -# Include functions: -# getboardtemp() -# batteryinfo() -# ambienttemp() -source /usr/lib/armbian/armbian-allwinner-battery - -function display() { - # $1=name $2=value $3=red_limit $4=minimal_show_limit $5=unit $6=after $7=acs/desc{ - # battery red color is opposite, lower number - if [[ "$1" == "Battery" ]]; then local great="<"; else local great=">"; fi - if [[ -n "$2" && "$2" -gt "0" && (( "${2%.*}" -ge "$4" )) ]]; then - printf "%-14s" "$1:" - if awk "BEGIN{exit ! ($2 $great $3)}"; then echo -ne "\e[0;91m $2"; else echo -ne "\e[0;92m $2"; fi - printf "%-1s\x1B[0m" "$5" - printf "%-11s\t" "$6" +display() { + # $1=name $2=value $3=red_limit $4=min_show_limit $5=unit $6=after + # Battery uses inverse threshold (lower is worse) + local name="$1" val="$2" red="$3" min="$4" unit="$5" after="$6" + [[ -z "$val" ]] && return 0 + if awk -v v="$val" -v m="$min" -v inv="$name" 'BEGIN{ + if (v+0 <= 0) exit 1; + if (inv=="Battery") exit !(v+0 >= m+0); else exit !(v+0 >= m+0) + }'; then + printf "%-14s" "${name}:" + if awk -v v="$val" -v r="$red" -v inv="$name" 'BEGIN{ + if (inv=="Battery") exit !(v+0 < r+0); else exit !(v+0 > r+0) + }'; then + printf "\e[0;91m %s" "$val" + else + printf "\e[0;92m %s" "$val" + fi + printf "%s\x1B[0m" "$unit" + printf "%-11s\t" "$after" return 1 fi -} # display + return 0 +} -function storage_info() { - # storage info - RootInfo=$(df -h /) - root_usage=$(awk '/\// {print $(NF-1)}' <<<${RootInfo} | sed 's/%//g') - root_total=$(awk '/\// {print $(NF-4)}' <<<${RootInfo}) - StorageInfo=$(df -h $STORAGE 2>/dev/null | grep $STORAGE) - if [[ -n "${StorageInfo}" && ${RootInfo} != *$STORAGE* ]]; then - storage_usage=$(awk '/\// {print $(NF-1)}' <<<${StorageInfo} | sed 's/%//g') - storage_total=$(awk '/\// {print $(NF-4)}' <<<${StorageInfo}) - if [[ -n "$(command -v smartctl)" ]]; then - DISK="${STORAGE::-1}" - storage_temp+=$(smartctl -l scttempsts $DISK 2> /dev/null | grep -i 'Current Temperature:' | awk '{print $(NF-1)}') +storage_info() { + # Root filesystem + local rootInfo storageInfo + rootInfo="$(df -hP /)" + root_usage="$(awk 'NR==2{gsub(/%/,"",$5); print $5}' <<<"$rootInfo")" + root_total="$(awk 'NR==2{print $2}' <<<"$rootInfo")" + + # Optional extra storage (only if it exists and is not the root FS) + if df -hP "$STORAGE" &>/dev/null; then + storageInfo="$(df -hP "$STORAGE" | awk 'NR==2')" + if [[ -n "$storageInfo" ]] && ! grep -q " $STORAGE " <<<"$rootInfo"; then + storage_usage="$(awk '{gsub(/%/,"",$5); print $5}' <<<"$storageInfo")" + storage_total="$(awk '{print $2}' <<<"$storageInfo")" + if command -v smartctl >/dev/null 2>&1; then + local disk="${STORAGE%[0-9]}" + storage_temp="$(smartctl -l scttempsts "$disk" 2>/dev/null \ + | awk -F': *' '/Current Temperature:/ {print $(NF-1)}')" + fi fi fi -} # storage_info +} -# query various systems and send some stuff to the background for overall faster execution. -# Works only with ambienttemp and batteryinfo since A20 is slow enough :) -amb_temp=$(ambienttemp &) +# Collect sensor and storage data +amb_temp="$(ambienttemp)" batteryinfo storage_info getboardtemp critical_load=80 -# get uptime, logged in users and load in one take -UPTIME=$(LC_ALL=C uptime) -UPT1=${UPTIME#*'up '} -UPT2=${UPT1%'user'*} -users=${UPT2//*','} -users=${users//' '} -time=${UPT2%','*} -time=${time//','} -time=$(echo $time | xargs) -load=${UPTIME#*'load average: '} -load=${load//','} -load=$(echo $load | cut -d" " -f1) -[[ $load == 0.0* ]] && load=0.10 -cpucount=$(grep -c processor /proc/cpuinfo) +# Uptime, users, and load +users="$(who 2>/dev/null | wc -l | tr -d ' ')" +time="$(uptime -p 2>/dev/null | sed -e 's/^up //')" +[[ -z "$time" ]] && time="$(LC_ALL=C uptime | sed -E 's/.*up ([^,]+), .*/\1/')" -load=$(awk '{printf("%.0f",($1/$2) * 100)}' <<< "$load $cpucount") +cpucount="$(grep -c ^processor /proc/cpuinfo 2>/dev/null || echo 1)" +read -r load1 _ < /proc/loadavg +[[ "$load1" == 0.0* ]] && load1="0.10" +load="$(awk -v l="$load1" -v c="$cpucount" 'BEGIN{printf("%.0f",(l/c)*100)}')" -# memory and swap -mem_info=$(LC_ALL=C free -w 2>/dev/null | grep "^Mem" || LC_ALL=C free | grep "^Mem") -memory_usage=$(awk '{printf("%.0f",(($2-($4+$6+$7))/$2) * 100)}' <<<${mem_info}) -mem_info=$(echo $mem_info | awk '{print $2}') -memory_total=$(( mem_info / 1024 )) -swap_info=$(LC_ALL=C free -m | grep "^Swap") -swap_usage=$( (awk '/Swap/ { printf("%3.0f", $3/$2*100) }' <<<${swap_info} 2>/dev/null || echo 0) | tr -c -d '[:digit:]') -swap_total=$(awk '{print $(2)}' <<<${swap_info}) +# Memory / swap +read -r mem_total_kb _ < <(awk '/MemTotal:/ {print $2" "$3; exit}' /proc/meminfo) +mem_total_mb="$(( mem_total_kb / 1024 ))" +mem_used_kb="$(awk ' + /MemTotal:/ {t=$2} + /MemFree:/ {f=$2} + /Buffers:/ {b=$2} + /^Cached:/ {c=$2} + END{printf("%d", t-f-b-c)} +' /proc/meminfo)" +memory_usage="$(awk -v u="$mem_used_kb" -v t="$mem_total_kb" 'BEGIN{printf("%.0f", (u/t)*100)}')" -# display info +swap_total_mb="$(awk '/SwapTotal:/ {print int($2/1024)}' /proc/meminfo)" +swap_used_mb="$(awk '/SwapFree:/ {sf=$2} /SwapTotal:/ {st=$2} END{print int((st-sf)/1024)}' /proc/meminfo)" +if [[ "${swap_total_mb:-0}" -gt 0 ]]; then + swap_usage="$(awk -v u="$swap_used_mb" -v t="$swap_total_mb" 'BEGIN{printf("%3.0f", (u/t)*100)}')" +else + swap_total_mb=0 + swap_usage=0 +fi -# draw a line to start this section +# --- spacing before Performance --- +# Add a blank line only if something printed before (e.g., IP, containers, AP) +if [[ -e /run/armbian-motd/.after_header.printed ]] \ + || [[ -e "${XDG_RUNTIME_DIR:-/tmp}/armbian-motd/.after_header.printed" ]]; then + echo "" +fi +printf "\e[0;90m Performance: \x1B[0m\n\n" -#echo "" -printf "\e[0;90m Performance: \x1B[0m" -echo "" # fixed newline +display " Load" "${load%% *}" "$critical_load" "0" "%" "" +printf " Uptime: \x1B[92m%s\x1B[0m\t" "${time:-unknown}" +display " Local users" "${users:-0}" "3" "2" "" "" echo "" -display " Load" "${load%% *}" "${critical_load}" "0" "%" "" -printf " Uptime: \x1B[92m%s\x1B[0m\t" "$time" -display " Local users" "${users##* }" "3" "2" "" -echo "" # fixed newline -if [[ ${memory_total} -gt 1000 ]]; then - memory_total=$(awk '{printf("%.2f",$1/1024)}' <<<${memory_total})"G" +# Humanize totals +if (( mem_total_mb > 1000 )); then + memory_total="$(awk -v m="$mem_total_mb" 'BEGIN{printf("%.2fG", m/1024)}')" else - memory_total+="M" + memory_total="${mem_total_mb}M" fi -if [[ ${swap_total} -gt 500 ]]; then - swap_total=$(awk '{printf("%.2f",$1/1024)}' <<<${swap_total})"G" +if (( swap_total_mb > 500 )); then + swap_total_disp="$(awk -v s="$swap_total_mb" 'BEGIN{printf("%.2fG", s/1024)}')" else - swap_total+="M" + swap_total_disp="${swap_total_mb}M" fi -display " Memory usage" "$memory_usage" "70" "0" "%" " of ${memory_total}" -display " Zram usage" "$swap_usage" "75" "0" "%" " of ${swap_total}" -echo "" # fixed newline -display " CPU temp" "$board_temp" $CPU_TEMP_LIMIT "0" "°C" "" -display " Ambient temp" "$amb_temp" $AMB_TEMP_LIMIT "0" "°C" "" -display " Usage of /" "$root_usage" "90" "1" "%" " of $root_total $overlay_root" -if command -v overlayroot-chroot > /dev/null 2>&1 && findmnt -k /media/root-ro | tail -1 | grep -w /media/root-ro > /dev/null 2>&1; then -echo -ne "\x1B[91m(read only rootfs)\x1B[0m" +display " Memory usage" "$memory_usage" "70" "0" "%" " of ${memory_total}" +display " Zram usage" "$swap_usage" "75" "0" "%" " of ${swap_total_disp}" +echo "" + +# Temperatures & root usage +# Apply optional CPU temp offset if both numeric +if [[ -n "${board_temp:-}" ]] && awk -v t="$board_temp" -v o="$CPU_TEMP_OFFSET" 'BEGIN{exit !(t ~ /^-?[0-9.]+$/ && o ~ /^-?[0-9.]+$/)}'; then + board_temp="$(awk -v t="$board_temp" -v o="$CPU_TEMP_OFFSET" 'BEGIN{printf("%.1f", t+o)}')" fi -echo "" # fixed newline + +# Truncate temperature decimals (e.g. 37.6 → 37) +cpu_temp_int=$(printf '%.0f\n' "${board_temp%%.*}") +amb_temp_int=$(printf '%.0f\n' "${amb_temp%%.*}") + +display " CPU temp" "$cpu_temp_int" "$CPU_TEMP_LIMIT" "0" "°C" "" +display " Ambient temp" "$amb_temp_int" "$AMB_TEMP_LIMIT" "0" "°C" "" +display " Usage of /" "${root_usage:-0}" "90" "1" "%" " of ${root_total} ${overlay_root}" + +if command -v overlayroot-chroot >/dev/null 2>&1 && findmnt -k /media/root-ro | tail -1 | grep -qw /media/root-ro; then + echo -ne "\x1B[91m(read only rootfs)\x1B[0m" +fi +echo "" + +# Optional secondary storage / disk temp / battery a=0 -display " storage /" "$storage_usage" "90" "1" "%" " of $storage_total" ; a=$((a+$?)) -display " storage temp" "$storage_temp" $HDD_TEMP_LIMIT "0" "°C" "" ; a=$((a+$?)) -display " Battery" "$battery_percent" "20" "1" "%" "$status_battery_text" ; a=$((a+$?)) -(( $a > 0 )) && echo "" # new line only if some value is displayed +display " storage /" "${storage_usage:-0}" "90" "1" "%" " of ${storage_total:-}" ; a=$((a+$?)) +display " storage temp" "${storage_temp:-0}" "$HDD_TEMP_LIMIT" "0" "°C" "" ; a=$((a+$?)) +display " Battery" "${battery_percent:-0}" "20" "1" "%" "${status_battery_text:-}" ; a=$((a+$?)) +(( a > 0 )) && echo "" -line=0 -if [[ -n "$PRIMARY_INTERFACE" ]] && vnstat -i "$PRIMARY_INTERFACE" &> /dev/null; then - traffic=$(LC_ALL=C vnstat -i $PRIMARY_INTERFACE --oneline | cut -d";" -f4,5) - traffic_rx=$(echo $traffic | cut -d";" -f1,1 | sed -r 's/([0-9]+\.[0-9]{1})[0-9]*/\1/' | cut -d" " -f1 | cut -d"." -f1) - traffic_rx_unit=$(echo $traffic | cut -d";" -f1,1 | sed -r 's/([0-9]+\.[0-9]{1})[0-9]*/\1/' | cut -d" " -f2) - traffic_tx=$(echo $traffic | cut -d";" -f2,2 | sed -r 's/([0-9]+\.[0-9]{1})[0-9]*/\1/' | cut -d" " -f1 | cut -d"." -f1) - traffic_tx_unit=$(echo $traffic | cut -d";" -f2,2 | sed -r 's/([0-9]+\.[0-9]{1})[0-9]*/\1/' | cut -d" " -f2) - [[ "$traffic" == *"Not enough"* ]] && { traffic_tx="n/a "; traffic_rx="n/a "; } - case $PRIMARY_DIRECTION in - tx) - display " TX today" "$traffic_tx" 500 0 "" "$traffic_tx_unit" "" - line=$((line+1)) - ;; - rx) - display " RX today" "$traffic_rx" 500 0 "" "$traffic_rx_unit" "" - line=$((line+1)) - ;; - both) - display " TX today" "$traffic_tx" 500 0 "" "$traffic_tx_unit" "" - display " RX today" "$traffic_rx" 500 0 "" "$traffic_rx_unit" "" - line=$((line+1)) - ;; - *) #off or whatever - ;; - esac +# vnStat daily traffic (if available) +if [[ -n "$PRIMARY_INTERFACE" ]] && command -v vnstat >/dev/null 2>&1; then + if vnstat -i "$PRIMARY_INTERFACE" &>/dev/null; then + # Use C locale so decimals are '.' and units are ASCII. + line="$(LC_ALL=C vnstat -i "$PRIMARY_INTERFACE" --oneline 2>/dev/null)" + # Fields 4 and 5 are today's RX;TX totals. They may be like "205.88KiB" or "205.88 KiB". + rx_raw="$(cut -d';' -f4 <<<"$line")" + tx_raw="$(cut -d';' -f5 <<<"$line")" + + # Normalize: extract integer value (truncate decimals) and unit; ensure exactly one space. + norm_val() { sed -E 's/^([0-9]+)(\.[0-9]+)?([[:space:]]*)([A-Za-z]+).*/\1/'; } + norm_unit() { sed -E 's/^([0-9]+)(\.[0-9]+)?([[:space:]]*)([A-Za-z]+).*/\4/'; } + + traffic_rx_value="$(printf '%s' "$rx_raw" | norm_val)" + traffic_rx_unit="$( printf '%s' "$rx_raw" | norm_unit)" + traffic_tx_value="$(printf '%s' "$tx_raw" | norm_val)" + traffic_tx_unit="$( printf '%s' "$tx_raw" | norm_unit)" + + # Handle "Not enough" (no data yet) + if [[ "$rx_raw$tx_raw" == *"Not enough"* ]]; then + traffic_rx_value="n/a"; traffic_rx_unit="" + traffic_tx_value="n/a"; traffic_tx_unit="" + fi + + case "$PRIMARY_DIRECTION" in + tx) + display " TX today" "$traffic_tx_value" 500 0 " ${traffic_tx_unit}" "" ;; + rx) + display " RX today" "$traffic_rx_value" 500 0 " ${traffic_rx_unit}" "" ;; + both) + display " TX today" "$traffic_tx_value" 500 0 " ${traffic_tx_unit}" "" + display " RX today" "$traffic_rx_value" 500 0 " ${traffic_rx_unit}" "" ;; + *) : ;; + esac + echo "" # force newline after vnStat output + fi fi -if [[ $(command -v zpool) ]]; then - zpoolstatus="$(zpool status &1 )" - if [[ -n $(echo $zpoolstatus | grep 'not loaded') ]]; then - : - elif [[ -n $(echo $zpoolstatus | grep 'degraded\|OFFLINE') ]]; then +# ZFS pool summary +if command -v zpool >/dev/null 2>&1; then + zpoolstatus="$(zpool status 2>&1 || true)" + if grep -q 'not loaded' <<<"$zpoolstatus"; then + : # no line + elif grep -Eq 'degraded|OFFLINE' <<<"$zpoolstatus"; then printf " ZFS pool: " echo -ne "\e[0;91mDegraded\x1B[0m" - line=$((line+1)) - elif [[ -n $(echo $zpoolstatus | grep 'no pools available') ]]; then + echo "" + elif grep -q 'no pools available' <<<"$zpoolstatus"; then printf " ZFS pool: " echo -ne "n/a" - line=$((line+1)) + echo "" else printf " ZFS pool: " echo -ne "\e[0;92mOnline\x1B[0m" - line=$((line+1)) + echo "" fi fi -echo "" + +exit 0 diff --git a/packages/bsp/common/etc/update-motd.d/35-armbian-tips b/packages/bsp/common/etc/update-motd.d/35-armbian-tips index c1b6067abc..643b65d153 100755 --- a/packages/bsp/common/etc/update-motd.d/35-armbian-tips +++ b/packages/bsp/common/etc/update-motd.d/35-armbian-tips @@ -1,32 +1,82 @@ #!/bin/bash # -# Copyright (c) Authors: https://www.armbian.com/authors +# Armbian MOTD: Tips (compact + robust) # -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. - -# DO NOT EDIT THIS FILE but add config options to /etc/default/armbian-motd -# any changes will be lost on board support package update THIS_SCRIPT="tips" MOTD_DISABLE="" -[[ -f /etc/default/armbian-motd ]] && . /etc/default/armbian-motd +safe_source() { [[ -f "$1" ]] && . "$1"; } -for f in $MOTD_DISABLE; do - [[ $f == $THIS_SCRIPT ]] && exit 0 +safe_source /etc/default/armbian-motd +for f in ${MOTD_DISABLE}; do + [[ "${f}" == "${THIS_SCRIPT}" ]] && exit 0 done +# --- helpers --- +_has_prev_output() { + [[ -e /run/armbian-motd/.after_header.printed ]] \ + || [[ -e "${XDG_RUNTIME_DIR:-/tmp}/armbian-motd/.after_header.printed" ]] +} + +# extract field from a tiny JSON object; prefer jq, fallback to sed +_json_get() { + local key="$1" json="$2" + if command -v jq >/dev/null 2>&1; then + jq -r --arg k "$key" '.[$k] // empty' <<<"$json" 2>/dev/null + else + printf '%s\n' "$json" | sed -n -E "s/.*\"$key\":\"([^\"]*)\".*/\1/p" + fi +} + +# --- collect content --- + +lines=() + +# 1) Optional quote: /etc/update-motd.d/quotes.txt lines like: YYYY-MM-DD|Some text... quotes="/etc/update-motd.d/quotes.txt" -if [[ -f $quotes && $(( $RANDOM % 1 )) == 0 ]]; then - random_line=$(shuf -i 1-$(wc -l < $quotes) -n 1) - quote=$(sed -n -e "$random_line"p $quotes) - DUE_DATE=$(echo $quote | cut -d"|" -f1) - SELECTED_QUOTE=$(echo $quote | cut -d"|" -f2) - if [[ -n $SELECTED_QUOTE && $(date +'%Y-%m-%d') < $(date -d $DUE_DATE +"%Y-%m-%d") ]]; then - printf "\e[0;90m Tips:\x1B[0m\n " #; printf '%.s─' $(seq 1 5); echo -e " \x1B[0m" - echo -e "\n$SELECTED_QUOTE" | fold -w 79 -s | sed 's/^/ /' - echo "" - fi +if [[ -f "$quotes" ]]; then + total=$(wc -l < "$quotes") + if [[ "$total" -gt 0 ]]; then + rline=$(( RANDOM % total + 1 )) + raw=$(sed -n "${rline}p" "$quotes") + due="${raw%%|*}" + qtext="${raw#*|}" + # show only if not expired or due is empty + today=$(date +%Y-%m-%d) + if [[ -z "$due" || "$today" < "$(date -d "$due" +%Y-%m-%d 2>/dev/null || echo 9999-12-31)" ]]; then + [[ -n "$qtext" ]] && lines+=("$qtext") + fi + fi fi + +# 2) One non-expired recommend_message (if defined) +if [[ "$(declare -p recommend_messages 2>/dev/null)" == "declare -a"* ]]; then + today=$(date +%Y-%m-%d) + valid=() + for item in "${recommend_messages[@]}"; do + exp=$(_json_get expiration "$item") + [[ -z "$exp" || "$today" < "$exp" ]] && valid+=("$item") + done + if ((${#valid[@]})); then + pick=${valid[$RANDOM % ${#valid[@]}]} + msg=$(_json_get message "$pick") + url=$(_json_get url "$pick") + [[ -n "$msg" ]] && lines+=("$msg") + [[ -n "$url" ]] && lines+=("$url") + fi +fi + +# --- print (only if we actually have something to say) --- + +if ((${#lines[@]})); then + # One blank line ONLY if something printed earlier + _has_prev_output && echo "" + + printf "\e[0;90m Tips:\x1B[0m\n\n" + for l in "${lines[@]}"; do + printf "%s\n" " $l" + done +fi + +exit 0 diff --git a/packages/bsp/common/etc/update-motd.d/41-commands b/packages/bsp/common/etc/update-motd.d/41-commands index 70db186675..86e17bfa6e 100755 --- a/packages/bsp/common/etc/update-motd.d/41-commands +++ b/packages/bsp/common/etc/update-motd.d/41-commands @@ -12,15 +12,24 @@ THIS_SCRIPT="commands" MOTD_DISABLE="" -[[ -f /etc/default/armbian-motd ]] && . /etc/default/armbian-motd +safe_source() { [[ -f "$1" ]] && . "$1"; } + +# Optional overrides +safe_source /etc/default/armbian-motd +for f in ${MOTD_DISABLE}; do + [[ "${f}" == "${THIS_SCRIPT}" ]] && exit 0 +done + +_motd_spacer() { + local tag="${1:-after-header}" + local dir="/run/armbian-motd"; local stamp="$dir/.${tag}.printed" + mkdir -p "$dir" 2>/dev/null + [[ -e "$stamp" ]] || { echo ""; : > "$stamp"; } +} # read upgrade count to show upgrade command [[ -f /var/cache/apt/archives/updates.number ]] && . /var/cache/apt/archives/updates.number -for f in $MOTD_DISABLE; do - [[ $f == $THIS_SCRIPT ]] && exit 0 -done - # text, sudo / without, command, condition # condition can be fairly complex list=( @@ -52,6 +61,7 @@ done # show list for existing command only if [[ "${cmd_count}" -gt 0 ]]; then + echo "" # ensure we start on a fresh line printf "\e[0;90m Commands: \x1B[0m\n" #; printf '%.s─' $(seq 1 39); echo -e "\x1B[0m" echo "" for l in "${output[@]}" @@ -61,7 +71,7 @@ if [[ "${cmd_count}" -gt 0 ]]; then command=$(echo $l | cut -d"," -f3) printf " \e[1;33m%-${name_len}s\e[0m %-0s: $sudo$command\n" "$name" done - echo -e "\033[0m" + echo -en "\033[0m" fi exit 0 diff --git a/packages/bsp/common/etc/update-motd.d/99-blank b/packages/bsp/common/etc/update-motd.d/99-blank new file mode 100755 index 0000000000..e94a68c02d --- /dev/null +++ b/packages/bsp/common/etc/update-motd.d/99-blank @@ -0,0 +1,3 @@ +#!/bin/bash +echo "" +exit 0 diff --git a/packages/bsp/common/usr/lib/armbian/armbian-firstlogin b/packages/bsp/common/usr/lib/armbian/armbian-firstlogin index f262d6fb13..1613cc462a 100755 --- a/packages/bsp/common/usr/lib/armbian/armbian-firstlogin +++ b/packages/bsp/common/usr/lib/armbian/armbian-firstlogin @@ -992,6 +992,7 @@ if [[ -f /root/.not_logged_in_yet && -n $(tty) ]]; then else # no display manager detected -> clear screen and show motd clear + run-parts --lsbsysinit /etc/cron.daily run-parts --lsbsysinit /etc/update-motd.d # Display reboot recommendation if necessary