🌈 Sync 2026-09-24 15:19:50

This commit is contained in:
github-actions[bot] committed 2026-09-24 15:19:50 +08:00
1 parent 765ac2b267
commit 87101cc7f2
126 files changed
+4403 -2879

No files matched your search

@@ -64,6 +64,8 @@ function json_unhex4(h, n, i, c, v) {
return n
}
function json_get_msg(obj, s, i, c, esc, out, hex, n) {
# Summary decoder is ASCII/Latin-1 oriented. \uXXXX outside 1..255 is
# collapsed (not UTF-8). Invalid \u appends u. NUL is dropped.
if (!match(obj, /"msg"[[:space:]]*:[[:space:]]*"/)) return ""
s = substr(obj, RSTART + RLENGTH)
out = ""
@@ -79,6 +81,8 @@ function json_get_msg(obj, s, i, c, esc, out, hex, n) {
else if (c == "u") {
hex = substr(s, i + 1, 4)
n = (length(hex) == 4) ? json_unhex4(hex) : -1
# 1..255: Latin-1 %c. n>255: collapse. n<0: append u.
# n==0 (NUL): BusyBox awk strings cannot hold NUL; drop it.
if (n >= 1 && n <= 255) out = out sprintf("%c", n)
else if (n < 0) out = out "u"
if (n >= 0) i += 4
@@ -28,12 +28,15 @@ NF_LOG_IPV6_READY=false
# firewall.<zone>.log bit, computes a target, then uci set + uci commit. Two
# concurrent callers could otherwise interleave and last-commit-wins.
#
# BusyBox flock constraint: it has NO -w timeout. A stuck lock holder blocks
# any waiter until the holder exits or the device reboots. The critical
# section MUST stay SHORT (a few uci commands). Do NOT hold the lock across
# the /etc/init.d/firewall reload (can take seconds); the lock is released
# before reload, and reload-failure rollback is a best-effort UCI write
# outside the lock.
# BusyBox flock constraint: it has NO -w timeout. The lock is opened with
# `exec 9>>` + `flock 9`. In ash/dash, `>>` clears close-on-exec, so a live
# child keeps fd 9 after this holder dies. A stuck lock therefore blocks any
# waiter until the last fd-9 inheritor exits or the device reboots (killing
# the holder alone may not release it). The critical section MUST stay SHORT
# (a few uci commands). Do NOT hold the lock across the /etc/init.d/firewall
# reload (can take seconds); the lock is released before reload.
# Reload-failure rollback re-acquires the lock so the read-compare-restore
# is atomic against concurrent writers; do not drop that re-acquire.
# Overridable for tests/containers (default is root-only /etc/fwlive).
WAN_LOG_LOCK_FILE="${FWLIVE_WAN_LOG_LOCK_FILE:-/etc/fwlive/logging.lock}"
WAN_LOG_BASELINE_FILE="${FWLIVE_WAN_LOG_BASELINE_FILE:-/etc/fwlive/wan-log-baseline}"
@@ -435,7 +438,17 @@ restore_wan_log_baseline() {
logger -t fwlive "WAN log baseline restore skipped: firewall changes pending" 2>/dev/null || true
return 1
fi
if ! commit_wan_log_change "$zone" "$zone_json" "$baseline"; then
_commit_rc=0
commit_wan_log_change "$zone" "$zone_json" "$baseline" || _commit_rc=$?
if [ "$_commit_rc" -eq 2 ]; then
# Commit landed; live fw4 still needs a reload. Baseline file
# stays so a later restore can retry — read-back was not ours.
release_wan_log_lock
reload_firewall || logger -t fwlive "WAN log baseline restore: firewall reload failed after raced commit" 2>/dev/null || true
logger -t fwlive "WAN log baseline restore: post-commit verify raced" 2>/dev/null || true
return 1
fi
if [ "$_commit_rc" -ne 0 ]; then
release_wan_log_lock
logger -t fwlive "WAN log baseline restore: commit gate failed" 2>/dev/null || true
return 1
@@ -701,7 +714,19 @@ restore_wan_zone_log() {
logger -t fwlive "WAN log rollback skipped after stage: firewall changes staged by another writer" 2>/dev/null || true
return 1
fi
uci commit firewall 2>/dev/null || true
if ! uci commit firewall 2>/dev/null; then
# Drop our staged rollback delta so a later toggle is not stuck
# on firewall_changes_pending from this package's own orphaned
# write. Mirror commit_wan_log_change: revert only when the
# remaining staging is entirely our log option.
_staged=$(uci -q changes firewall 2>/dev/null || true)
_total=$(printf '%s\n' "$_staged" | grep -c . 2>/dev/null || true)
_ours=$(wan_log_count_our_staged_lines "$zone" "$_staged")
if [ "${_total:-0}" -gt 0 ] && [ "${_total:-0}" -eq "${_ours:-0}" ]; then
uci -q revert firewall 2>/dev/null || true
fi
return 1
fi
}
# Stage + commit the WAN log bit. Caller MUST hold the logging lock; this
@@ -721,10 +746,11 @@ restore_wan_zone_log() {
# for a failed reload.
# Residual window: a writer that stages between the post-stage check and
# `uci commit` can still ride along; post-commit verification detects a
# mismatched log bit. Same-option races (another writer also staging
# firewall.<wan>.log) are not distinguishable in the changes list.
# The caller-reported error is firewall_changes_pending: the LuCI view already
# maps it to the accurate "another change is staged" notice.
# mismatched log bit and reports firewall_commit_raced (no blind-revert).
# Same-option races (another writer also staging firewall.<wan>.log) are
# not distinguishable in the changes list. Pre-stage races report
# firewall_changes_pending: the LuCI view already maps it to the accurate
# "another change is staged" notice.
#
# target: value to stage; EMPTY means delete the option (bit fully cleared).
# Prints the failure JSON and returns 1 on abort or failure.
@@ -807,14 +833,44 @@ commit_wan_log_change() {
# Post-commit verification: confirm the committed config really
# carries what we wrote (empty target = option must now be gone/empty).
# A mismatch means another writer overtook the commit: warn loudly but do
# NOT blind-revert (that would destroy unrelated committed data); the
# reload-failure rollback path still guards the ordinary failure case.
# NOT blind-revert (that would destroy unrelated committed data). Return 2
# so the caller still reloads (live fw4 must track committed UCI) and then
# reports firewall_commit_raced instead of ok:true. Do not print JSON here:
# reload may still fail. Return 1 is reserved for pre-commit aborts that
# already printed an error body.
if ! verify_wan_log_commit "$zone" "$target"; then
logger -t fwlive "WAN log post-commit verify FAILED: wrote '${target:-<deleted>}', read back differs" 2>/dev/null || true
return 2
fi
return 0
}
# Caller MUST have released the logging lock. rc from commit_wan_log_change:
# 0 — our target is committed; reload and report success.
# 1 — abort JSON already printed; no commit, skip reload.
# 2 — commit succeeded but verify raced; still reload, then report
# firewall_commit_raced (or firewall_reload_failed). Do not use
# reload_and_report_wan_log: a reload failure must not restore
# `previous` over a foreign committed value.
report_wan_log_after_commit() {
_rc="$1"
zone_json="$2"
if [ "$_rc" -eq 1 ]; then
return 0
fi
if [ "$_rc" -eq 2 ]; then
if ! reload_firewall; then
logger -t fwlive "Firewall reload failed after raced WAN log commit" 2>/dev/null || true
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"firewall_reload_failed"}' "$zone_json"
return 0
fi
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"firewall_commit_raced"}' "$zone_json"
return 0
fi
shift 2
reload_and_report_wan_log "$@"
}
# Firewall reload + best-effort UCI rollback on reload failure. The reload
# itself runs WITHOUT the logging lock (it can take seconds and a held lock
# would block a concurrent toggle until the holder exits — BusyBox flock has
@@ -918,13 +974,11 @@ enable_wan_logging() {
fi
target=$(wan_filter_log_target_value "$current")
if ! commit_wan_log_change "$zone" "$zone_json" "$target"; then
release_wan_log_lock
return 0
fi
_commit_rc=0
commit_wan_log_change "$zone" "$zone_json" "$target" || _commit_rc=$?
release_wan_log_lock
reload_and_report_wan_log "$zone" "$current" "$target" \
report_wan_log_after_commit "$_commit_rc" "$zone_json" \
"$zone" "$current" "$target" \
'Firewall reload failed after enable; reverted UCI WAN log' \
'WAN zone logging enabled' \
"$zone_json"
@@ -971,13 +1025,11 @@ disable_wan_logging() {
# not restore that bit.
target=$(wan_filter_log_clear_value "$current")
if ! commit_wan_log_change "$zone" "$zone_json" "$target"; then
release_wan_log_lock
return 0
fi
_commit_rc=0
commit_wan_log_change "$zone" "$zone_json" "$target" || _commit_rc=$?
release_wan_log_lock
reload_and_report_wan_log "$zone" "$current" "$target" \
report_wan_log_after_commit "$_commit_rc" "$zone_json" \
"$zone" "$current" "$target" \
'Firewall reload failed after disable; reverted UCI WAN log' \
'WAN zone logging disabled' \
"$zone_json"
+248 -55
View File
@@ -39,6 +39,11 @@ RESOLVE_BUDGET=5
POLL_LINES_MAX=2000
# Bound nft ruleset dumps (corrupted/hung nft must not block LuCI rules load).
NFT_TIMEOUT=5
# Bound poll log.read (wedged logd must not hold an rpcd worker past the UI cadence).
# Integer seconds — BusyBox timeout truncates fractions to 0 (see RESOLVE_TIMEOUT).
POLL_TIMEOUT=5
# Host tests override this to a pinned libubox helper; devices keep the package path.
: "${FWLIVE_JSHN_SH:=/usr/share/libubox/jshn.sh}"
# json_escape lives in fwlive-logging.sh so prerm can source that file alone.
# shellcheck disable=SC1090,SC1091 # LOGGING_SH resolves to sibling ../fwlive-logging.sh
@@ -170,15 +175,70 @@ $(uci_rule_names)
EOF
}
# One awk pass: prefix<TAB>comment. Last quoted log prefix / comment on
# the line, including \" units (same capture as the former sed).
nft_dump_fields() {
LC_ALL=C awk '
function last_at(s, needle, p, i, found) {
found = 0
p = 1
while ((i = index(substr(s, p), needle)) > 0) {
found = p + i - 1
p = found + 1
}
return found
}
function quoted_at(s, needle, i, rest, out, c, n, esc, j) {
i = last_at(s, needle)
if (i == 0)
return ""
rest = substr(s, i + length(needle))
n = length(rest)
out = ""
esc = 0
for (j = 1; j <= n; j++) {
c = substr(rest, j, 1)
if (esc) {
out = out c
esc = 0
continue
}
if (c == "\\") {
out = out c
esc = 1
continue
}
if (c == "\"")
return out
out = out c
}
return ""
}
{
prefix = quoted_at($0, "log prefix \"")
comment = quoted_at($0, "comment \"")
if (prefix == "" && comment == "")
next
print prefix "\t" comment
}
'
}
# pass=labeled|unlabeled|all — labeled before unlabeled so !fw4: beats
# an earlier cosmetic for the same prefix under first-wins.
# Reads TSV from nft_dump_fields; stops once the map cap trips.
map_from_nft_stream() {
pass="${1:-all}"
while IFS= read -r line; do
# Capture [^"]* plus escaped-quote units (\"...) so a log prefix or
# comment containing an escaped quote is not truncated at the quote.
prefix=$(echo "$line" | sed -n 's/.*log prefix "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
comment=$(echo "$line" | sed -n 's/.*comment "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
_tab=$(printf '\t')
# IFS= keeps a leading tab (empty prefix + comment-only !fw4: line).
while IFS= read -r line || [ -n "$line" ]; do
[ -n "$_map_overflow" ] && break
[ -n "$line" ] || continue
prefix="${line%%${_tab}*}"
case "$line" in
*"$_tab"*) comment="${line#*"$_tab"}" ;;
*) comment="" ;;
esac
label=''
case "$comment" in
@@ -209,14 +269,6 @@ nft_list_ruleset() {
run_with_timeout "$NFT_TIMEOUT" nft list ruleset
}
detect_firewall_backend() {
if command -v nft >/dev/null 2>&1 && nft_list_ruleset >/dev/null; then
printf '%s' nft
return 0
fi
printf '%s' unknown
}
_fwlive_tmp_dir_ok() {
# Fail closed unless the dump dir is a real directory with the sticky
# bit. Without sticky, another uid can unlink a just-created mktemp
@@ -251,15 +303,29 @@ enrich_rules_dump() {
_rules_dump=$(_fwlive_mktemp "$_dump_prefix") || _rules_dump=''
if [ -n "$_rules_dump" ]; then
# One dump serves detect + parse. Empty/failed dump is not nft
# (unknown/no_backend, same as a failed detect probe).
if nft_list_ruleset >"$_rules_dump" 2>/dev/null; then
map_from_nft_stream labeled <"$_rules_dump"
map_from_nft_stream unlabeled <"$_rules_dump"
elif [ -z "$_enrich_error" ]; then
_enrich_error='nft_failed'
backend=nft
# Stream awk output to a second temp so a huge dump is
# not materialized in the rpcd worker's shell heap.
_nft_tsv=$(_fwlive_mktemp "fwlive-nft-tsv")
if [ -z "$_nft_tsv" ]; then
_enrich_error='mktemp_failed'
else
nft_dump_fields <"$_rules_dump" >"$_nft_tsv"
map_from_nft_stream labeled <"$_nft_tsv"
if [ -z "$_map_overflow" ]; then
map_from_nft_stream unlabeled <"$_nft_tsv"
fi
rm -f "$_nft_tsv"
fi
fi
rm -f "$_rules_dump"
else
# nft is on PATH (caller checked); skip enrichment without a dump.
_enrich_error='mktemp_failed'
backend=nft
fi
}
@@ -268,13 +334,14 @@ build_rules_map() {
_map_keys=0
_map_overflow=''
_enrich_error=''
backend=$(detect_firewall_backend)
backend=unknown
# UCI first, then labeled stream prefixes, then unlabeled cosmetics
# First-wins keeps UCI over nft and !fw4: over cosmetic.
map_uci_rule_names
if [ "$backend" = nft ]; then
# Temp file first, then a single nft_list_ruleset; backend from success.
if command -v nft >/dev/null 2>&1; then
enrich_rules_dump fwlive-nft
fi
@@ -301,7 +368,8 @@ poll_clamp_lines() {
return 0
;;
esac
_digits=$(printf '%s' "$_val" | sed 's/^0*//')
# Strip leading zeros in-shell so default-50 poll does not pay for sed.
_digits=${_val#${_val%%[!0]*}}
if [ -z "$_digits" ]; then
printf '%s' "50"
return 0
@@ -317,33 +385,52 @@ poll_clamp_lines() {
fi
}
# Failed jshn reads still default to 50 (silent numeric contract; syslog named cause only).
poll_lines_from_input() {
input="$1"
lines=50
_cause=
if ! command -v jshn >/dev/null 2>&1; then
printf '%s' "$lines"
return 0
_cause=jshn_missing
else
_rc=0
# jshn uses intentionally unset state variables. Keep nounset disabled in
# this subshell so strict mode is restored automatically on every exit.
# exit 2: library missing or source failed; exit 1: JSON parse failed.
# Missing addresses[0] is an intentional silent default, not a parse error.
_first=$( (
set +u
# shellcheck disable=SC1090,SC1091
. "${FWLIVE_JSHN_SH:-/usr/share/libubox/jshn.sh}" 2>/dev/null || exit 2
json_init >/dev/null 2>&1 || exit 2
# json_load masks a failing jshn exit through eval. Check the real
# parser once, then evaluate only its generated shell assignments.
_json_no_warning=1
_json_code=$(jshn -r "$input" 2>/dev/null) || exit 1
eval "$_json_code" || exit 1
first=
if json_select addresses 2>/dev/null; then
json_get_var first 1 || first=
fi
printf '%s' "${first:-}"
) ) || _rc=$?
if [ "$_rc" -eq 2 ]; then
_cause=jshn_lib_missing
elif [ "$_rc" -ne 0 ]; then
_cause=invalid_input
else
case "${_first:-}" in
''|*[!0-9]*) ;;
*) lines="$_first" ;;
esac
fi
fi
# jshn uses intentionally unset state variables. Keep nounset disabled in
# this subshell so strict mode is restored automatically on every exit.
_first=$( (
set +u
. /usr/share/libubox/jshn.sh
# json_load masks a failing jshn exit through eval. Check the real
# parser once, then evaluate only its generated shell assignments.
_json_no_warning=1
_json_code=$(jshn -r "$input" 2>/dev/null) || exit 1
eval "$_json_code" || exit 1
if ! json_select addresses 2>/dev/null; then exit 1; fi
json_get_var first 1 || exit 1
printf '%s' "${first:-}"
) ) || _first=
case "${_first:-}" in
''|*[!0-9]*) ;;
*) lines="$_first" ;;
esac
# Named cause goes to syslog only: poll JSON "error" is connection-lost in the view.
if [ -n "$_cause" ]; then
logger -t fwlive "$_cause" 2>/dev/null || true
fi
lines=$(poll_clamp_lines "$lines")
@@ -360,7 +447,7 @@ fetch_firewall_logs() {
# Adaptive plan prints: lines shed bucket
start_cs=$(fwlive_adaptive_clock_cs)
# Drop dead oneshot flag — only consumed on logd's stream branch.
if ! raw=$(ubus call log read "{\"lines\":$lines,\"stream\":false}" 2>/dev/null); then
if ! raw=$(run_with_timeout "$POLL_TIMEOUT" ubus call log read "{\"lines\":$lines,\"stream\":false}"); then
# Do not record ~0 ms as cold: failure is not healthy fast processing
# and must not clear an existing hot/shed cap.
out='{"log":[],"error":"log_read_failed"}'
@@ -383,7 +470,7 @@ fetch_firewall_logs() {
# Empty success is not valid poll JSON — the shipped filter always
# prints {"log":[...]}, but a stub/regression must not emit a blank body.
_filter_ok=1
if ! out=$(printf '%s' "$raw" | "$FILTER_SH" /tmp); then
if ! out=$(printf '%s' "$raw" | run_with_timeout "$POLL_TIMEOUT" "$FILTER_SH" /tmp); then
_filter_ok=0
if [ -z "$out" ]; then
out='{"log":[],"error":"filter_failed"}'
@@ -423,16 +510,29 @@ poll_logs() {
# "<ptr> name = <host>"; fall back to "Address N: <ip> <host>".
# Trailing dots are stripped. Empty / server-only output is not a name.
parse_nslookup_name() {
# Prefer "<ptr> name = <host>". The "Address N: ip host" fallback
# must ignore the Server block (BusyBox mini nslookup prints the
# resolver via the same Address N: format first). Only accept
# Address N: after a blank line (the answer section).
# Prefer "<ptr> name = <host>". "name =" must be a whole token
# (not a suffix of hostname, not the phrase "domain name =").
# The "Address N: ip host" fallback must ignore the Server
# block (BusyBox mini nslookup prints the resolver via the
# same Address N: format first). Only accept Address N: after
# a blank line (the answer section).
printf '%s\n' "$1" | awk '
tolower($0) ~ /name[ \t]*=/ {
sub(/.*[Nn][Aa][Mm][Ee][ \t]*=[ \t]*/, "")
gsub(/[ \t].*$/, "")
gsub(/\.$/, "")
if ($0 != "") { print; exit }
{
low = tolower($0)
host = ""
if (match(low, /^name[ \t]*=[ \t]*/)) {
host = substr($0, RLENGTH + 1)
} else if (match(low, /[^a-z0-9_]name[ \t]*=[ \t]*/)) {
pre = substr(low, 1, RSTART - 1)
sub(/[ \t]+$/, "", pre)
if (pre != "domain" && pre !~ /[^a-z0-9_]domain$/)
host = substr($0, RSTART + RLENGTH)
}
if (host != "") {
gsub(/[ \t].*$/, "", host)
gsub(/\.$/, "", host)
if (host != "") { print host; exit }
}
}
NF == 0 { in_answer = 1; next }
in_answer && $1 == "Address" && $2 ~ /^[0-9]+:/ && NF >= 4 {
@@ -575,7 +675,8 @@ resolve_addresses() {
_addresses=$( (
set +u
. /usr/share/libubox/jshn.sh
# shellcheck disable=SC1090,SC1091
. "${FWLIVE_JSHN_SH:-/usr/share/libubox/jshn.sh}"
# json_load masks a failing jshn exit through eval. Check the real
# parser once, then evaluate only its generated shell assignments.
_json_no_warning=1
@@ -752,19 +853,23 @@ run_selftest() {
# Escaped-quote capture: a log prefix containing \" must not be truncated
# at the escaped quote (nft dumps strings with backslash escapes).
got=$(printf '%s\n' 'log prefix "My \" Rule"' | sed -n 's/.*log prefix "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
_tab=$(printf '\t')
got=$(printf '%s\n' 'log prefix "My \" Rule"' | nft_dump_fields)
got="${got%%${_tab}*}"
if [ "$got" != 'My \" Rule' ]; then
echo "escaped-quote prefix capture: expected 'My \\\" Rule' got '$got'" >&2
return 1
fi
# Plain prefix still captures unchanged.
got=$(printf '%s\n' 'log prefix "Plain Rule"' | sed -n 's/.*log prefix "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
got=$(printf '%s\n' 'log prefix "Plain Rule"' | nft_dump_fields)
got="${got%%${_tab}*}"
if [ "$got" != 'Plain Rule' ]; then
echo "plain prefix capture: expected 'Plain Rule' got '$got'" >&2
return 1
fi
# Empty prefix edge.
got=$(printf '%s\n' 'log prefix ""' | sed -n 's/.*log prefix "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
got=$(printf '%s\n' 'log prefix ""' | nft_dump_fields)
got="${got%%${_tab}*}"
if [ "$got" != '' ]; then
echo "empty prefix capture: expected '' got '$got'" >&2
return 1
@@ -774,6 +879,10 @@ run_selftest() {
check_eq poll_clamp_lines "$got" "$POLL_LINES_MAX" "over-long expected $POLL_LINES_MAX got $got" || return 1
got=$(poll_clamp_lines "0")
check_eq poll_clamp_lines "$got" "50" "zero expected 50 got $got" || return 1
got=$(poll_clamp_lines "000")
check_eq poll_clamp_lines "$got" "50" "all-zeros expected 50 got $got" || return 1
got=$(poll_clamp_lines "050")
check_eq poll_clamp_lines "$got" "50" "leading-zero expected 50 got $got" || return 1
got=$(poll_clamp_lines "2001")
check_eq poll_clamp_lines "$got" "$POLL_LINES_MAX" "2001 expected $POLL_LINES_MAX got $got" || return 1
got=$(poll_clamp_lines "500")
@@ -816,6 +925,16 @@ Address 1: 8.8.8.8 dns.google")
got=$(parse_nslookup_name "Server: 127.0.0.1
Address: 127.0.0.1:53")
check_eq parse_nslookup_name "$got" '' "server-only expected empty got '$got'" || return 1
got=$(parse_nslookup_name "hostname = evil.example.")
check_eq parse_nslookup_name "$got" '' "hostname = must not parse as name got '$got'" || return 1
got=$(parse_nslookup_name "domain name = other.example.")
check_eq parse_nslookup_name "$got" '' "domain name = must not parse as name got '$got'" || return 1
got=$(parse_nslookup_name "domain name = two.example.")
check_eq parse_nslookup_name "$got" '' "domain name = must not parse as name got '$got'" || return 1
got=$(parse_nslookup_name "domain name = tab.example.")
check_eq parse_nslookup_name "$got" '' "domain tab name = must not parse as name got '$got'" || return 1
got=$(parse_nslookup_name "name = dns.google. hostname = evil.example.")
check_eq parse_nslookup_name "$got" "dns.google" "standalone name = must beat hostname = got '$got'" || return 1
run_logging_selftest || return 1
@@ -834,6 +953,18 @@ Address: 127.0.0.1:53")
echo "skip: jshn not available (poll cap via jshn not tested)" >&2
return 0
fi
# Dash rejects libubox `export -n` in json_init. Skip numeric poll-cap
# checks unless this interpreter can actually use the helper; poll still
# defaults to 50 and logs a named cause in that case.
if ! (
set +u
# shellcheck disable=SC1090,SC1091
. "${FWLIVE_JSHN_SH:-/usr/share/libubox/jshn.sh}" 2>/dev/null || exit 1
json_init >/dev/null 2>&1
); then
echo "skip: jshn library not usable (poll cap via jshn not tested)" >&2
return 0
fi
got=$(poll_lines_from_input '{"addresses":["999999"]}')
check_eq poll_lines_from_input "$got" "$POLL_LINES_MAX" "expected $POLL_LINES_MAX got $got" || return 1
@@ -851,6 +982,64 @@ Address: 127.0.0.1:53")
got=$(poll_lines_from_input '{"addresses":["100"]}')
check_eq poll_lines_from_input "$got" "100" "expected 100 got $got" || return 1
# jshn failures must log a named cause; stdout stays the numeric default.
# logger() override is inherited by the $() subshell; command -v still finds
# the binary, so the unqualified logger call is what this intercepts.
_poll_log="/tmp/fwlive-poll-cause.$$"
: > "$_poll_log"
logger() { printf '%s\n' "$*" >> "$_poll_log"; }
got=$(poll_lines_from_input '{"addresses":["500"]}')
check_eq poll_lines_from_input "$got" "500" "override path expected 500 got $got" || return 1
case "$(cat "$_poll_log")" in
*jshn_missing*|*jshn_lib_missing*|*invalid_input*)
echo "poll_lines_from_input: successful cap must stay silent, got $(cat "$_poll_log")" >&2
rm -f "$_poll_log"
unset -f logger
return 1
;;
esac
: > "$_poll_log"
got=$(poll_lines_from_input '{}')
check_eq poll_lines_from_input "$got" "50" "missing addresses expected 50 got $got" || return 1
case "$(cat "$_poll_log")" in
*jshn_missing*|*jshn_lib_missing*|*invalid_input*)
echo "poll_lines_from_input: missing addresses[0] must stay silent, got $(cat "$_poll_log")" >&2
rm -f "$_poll_log"
unset -f logger
return 1
;;
esac
: > "$_poll_log"
got=$(poll_lines_from_input 'not-json{{{')
check_eq poll_lines_from_input "$got" "50" "invalid JSON expected 50 got $got" || return 1
case "$(cat "$_poll_log")" in
*invalid_input*) ;;
*) echo "poll_lines_from_input: expected invalid_input log, got $(cat "$_poll_log")" >&2
rm -f "$_poll_log"
unset -f logger
return 1 ;;
esac
: > "$_poll_log"
_saved_jshn_sh="$FWLIVE_JSHN_SH"
FWLIVE_JSHN_SH="/no/such/fwlive-jshn.sh"
got=$(poll_lines_from_input '{"addresses":["500"]}')
FWLIVE_JSHN_SH="$_saved_jshn_sh"
check_eq poll_lines_from_input "$got" "50" "missing library expected 50 got $got" || return 1
case "$(cat "$_poll_log")" in
*jshn_lib_missing*) ;;
*) echo "poll_lines_from_input: expected jshn_lib_missing log, got $(cat "$_poll_log")" >&2
rm -f "$_poll_log"
unset -f logger
return 1 ;;
esac
rm -f "$_poll_log"
unset -f logger
return 0
}
@@ -863,6 +1052,10 @@ case "${1:-}" in
poll_clamp_lines "${2:-}"
exit $?
;;
__poll_lines)
poll_lines_from_input "${2:-}"
exit $?
;;
__tmp_dir_ok)
_fwlive_tmp_dir_ok "${2:-}"
exit $?