#!/bin/sh # connstats.sh # Log-derived Wi-Fi connection counters per run (intended for cron every 10 minutes). # Writes one summary line per run to /root/connstats.log # Optional: --debug for chatty troubleshooting output in the same log. # Optional: --always-find-offset to calibrate syslog header time via a logger marker # and (on fresh start / rotation / truncation) build a true last-10-min slice. LOG_SRC="/var/log/messages" OUT_LOG="/root/connstats.log" STATE="/tmp/connstats.state" TMPDIR="/tmp" # How many tail lines to scan when we have no state (first run / after reboot / rotation) BOOTSTRAP_BACKFILL_LINES=2000 # Used only with --always-find-offset during bootstrap-like runs BOOTSTRAP_TIME_WINDOW_SEC=600 BOOTSTRAP_TIME_TAIL_LINES=10000 DEBUG=0 ALWAYS_FIND_OFFSET=0 for arg in "$@"; do case "$arg" in --debug) DEBUG=1 ;; --always-find-offset) ALWAYS_FIND_OFFSET=1 ;; -h|--help) echo "Usage: $0 [--debug] [--always-find-offset]" exit 0 ;; *) echo "Unknown arg: $arg" >&2 echo "Usage: $0 [--debug] [--always-find-offset]" >&2 exit 2 ;; esac done umask 077 ts_iso() { # ISO-ish timestamp; works on BusyBox and GNU date date "+%Y-%m-%dT%H:%M:%S%z" } log() { # Always append to OUT_LOG echo "[$(ts_iso)] $*" >> "$OUT_LOG" } # Generate a short random token (8 chars) for marker grep rand_token() { if [ -r /dev/urandom ]; then tr -dc 'a-z0-9' < /dev/urandom 2>/dev/null | head -c 8 else echo "$(date -u +%s 2>/dev/null)$$" | tr -dc 'a-z0-9' | tail -c 8 fi } # Parse syslog header timestamp (Mon DD HH:MM:SS) into epoch seconds (UTC) using awk mktime() # Expects typical line like: "<13> Jan 31 12:31:00 root[-] [notice]: message" syslog_header_to_epoch_utc() { echo "$1" | TZ=UTC awk ' function mon2num(m) { if (m=="Jan") return 1 if (m=="Feb") return 2 if (m=="Mar") return 3 if (m=="Apr") return 4 if (m=="May") return 5 if (m=="Jun") return 6 if (m=="Jul") return 7 if (m=="Aug") return 8 if (m=="Sep") return 9 if (m=="Oct") return 10 if (m=="Nov") return 11 if (m=="Dec") return 12 return 0 } { mon=$2; day=$3; tod=$4 year=strftime("%Y") m=mon2num(mon) if (m==0) { print ""; exit 1 } split(tod, t, ":") if (length(t) != 3) { print ""; exit 1 } hh=t[1]+0; mm=t[2]+0; ss=t[3]+0 d=day+0 print mktime(sprintf("%d %02d %02d %02d %02d %02d", year, m, d, hh, mm, ss)) } ' } # Compute offset_sec = payload_epoch_utc - syslog_header_epoch_utc # Returns offset seconds on stdout (blank on failure) find_time_offset_sec() { token="$(rand_token)" now_epoch="$(date -u +%s 2>/dev/null | tr -d ' ')" [ -z "$now_epoch" ] && now_epoch=0 logger "connstat calculation started. issueing time marker $token epoch=$now_epoch" 2>/dev/null found_line="" i=0 while [ $i -lt 3 ]; do found_line="$(tail -n 300 "$LOG_SRC" 2>/dev/null | grep "$token" | tail -n 1)" [ -n "$found_line" ] && break sleep 1 i=$((i + 1)) done [ -z "$found_line" ] && { echo ""; return; } hdr_epoch="$(syslog_header_to_epoch_utc "$found_line" | tr -d ' ')" [ -z "$hdr_epoch" ] && { echo ""; return; } payload_epoch="$(echo "$found_line" | awk '{ for (i=1; i<=NF; i++) { if ($i ~ /^epoch=[0-9]+$/) { sub(/^epoch=/,"",$i); print $i; exit } } }' | tr -d ' ')" [ -z "$payload_epoch" ] && payload_epoch="$now_epoch" echo $((payload_epoch - hdr_epoch)) } # Basic sanity checks if [ ! -r "$LOG_SRC" ]; then log "ERROR log_src_unreadable path=$LOG_SRC" exit 1 fi # Get current inode + line count (line-based incremental read is most portable) cur_inode="$(ls -i "$LOG_SRC" 2>/dev/null | awk '{print $1}')" cur_lines="$(wc -l < "$LOG_SRC" 2>/dev/null | tr -d ' ')" if [ -z "$cur_inode" ] || [ -z "$cur_lines" ]; then log "ERROR cannot_stat_log inode='$cur_inode' lines='$cur_lines'" exit 1 fi last_inode="" last_line="" if [ -f "$STATE" ]; then # STATE format: # inode= # line= last_inode="$(grep '^inode=' "$STATE" 2>/dev/null | head -n1 | cut -d= -f2)" last_line="$(grep '^line=' "$STATE" 2>/dev/null | head -n1 | cut -d= -f2)" fi # Decide where to start reading start_line="" backfill_start() { start_line=$((cur_lines - BOOTSTRAP_BACKFILL_LINES + 1)) [ "$start_line" -lt 1 ] && start_line=1 } # Extract the slice tmp_slice="$TMPDIR/connstats.slice.$$" # Grep patterns (hostapd transition events) re_auth='hostapd.*IEEE 802\.11: authenticated' re_assoc='hostapd.*IEEE 802\.11: associated' re_disassoc='hostapd.*IEEE 802\.11: disassociated' # Kernel extras for "seen_any" breadth (optional but useful) re_kernel_seen='kernel.*(station kicked out|station timed out)' # Are we in a bootstrap-like condition (no state OR rotation/truncation)? bootstrap_like=0 if [ -z "$last_inode" ] || [ -z "$last_line" ]; then bootstrap_like=1 else if [ "$cur_inode" != "$last_inode" ] || [ "$cur_lines" -lt "$last_line" ]; then bootstrap_like=1 fi fi # Bootstrap handling if [ "$ALWAYS_FIND_OFFSET" -eq 1 ] && [ "$bootstrap_like" -eq 1 ]; then offset_sec="$(find_time_offset_sec)" if [ -n "$offset_sec" ]; then now_epoch="$(date -u +%s 2>/dev/null | tr -d ' ')" [ -z "$now_epoch" ] && now_epoch=0 cutoff_epoch=$((now_epoch - BOOTSTRAP_TIME_WINDOW_SEC)) if [ $DEBUG -eq 1 ]; then log "DEBUG bootstrap_time_window_last_10m offset_sec=$offset_sec cutoff_epoch=$cutoff_epoch tail_lines=$BOOTSTRAP_TIME_TAIL_LINES" fi # Build slice = last-10-min relevant lines (hostapd transitions + kernel station kicked/timed out) # using (syslog_header_epoch + offset_sec) >= cutoff_epoch tail -n "$BOOTSTRAP_TIME_TAIL_LINES" "$LOG_SRC" 2>/dev/null \ | TZ=UTC awk -v cutoff="$cutoff_epoch" -v off="$offset_sec" ' function mon2num(m) { if (m=="Jan") return 1 if (m=="Feb") return 2 if (m=="Mar") return 3 if (m=="Apr") return 4 if (m=="May") return 5 if (m=="Jun") return 6 if (m=="Jul") return 7 if (m=="Aug") return 8 if (m=="Sep") return 9 if (m=="Oct") return 10 if (m=="Nov") return 11 if (m=="Dec") return 12 return 0 } function header_epoch( mon,day,tod,year,m,hh,mm,ss,t) { mon=$2; day=$3; tod=$4 year=strftime("%Y") m=mon2num(mon) if (m==0) return -1 split(tod, t, ":") if (length(t) != 3) return -1 hh=t[1]+0; mm=t[2]+0; ss=t[3]+0 return mktime(sprintf("%d %02d %02d %02d %02d %02d", year, m, (day+0), hh, mm, ss)) } { line=$0 if (line ~ /hostapd.*IEEE 802\.11: (authenticated|associated|disassociated)/ || line ~ /kernel.*(station kicked out|station timed out)/) { he=header_epoch() if (he < 0) next corr=he + off if (corr >= cutoff) print line } } ' > "$tmp_slice" 2>/dev/null start_line="(time_window_last_10m)" else # Offset calc failed -> fall back to old bootstrap behavior backfill_start if [ $DEBUG -eq 1 ]; then log "DEBUG time_offset_failed_fallback_to_backfill cur_inode=$cur_inode cur_lines=$cur_lines start_line=$start_line backfill_lines=$BOOTSTRAP_BACKFILL_LINES" fi sed -n "${start_line},\$p" "$LOG_SRC" > "$tmp_slice" 2>/dev/null fi else # Original behavior (unchanged) if [ -z "$last_inode" ] || [ -z "$last_line" ]; then backfill_start if [ $DEBUG -eq 1 ]; then log "DEBUG bootstrap_no_state_backfill cur_inode=$cur_inode cur_lines=$cur_lines start_line=$start_line backfill_lines=$BOOTSTRAP_BACKFILL_LINES" fi else # Rotation / truncation detection if [ "$cur_inode" != "$last_inode" ] || [ "$cur_lines" -lt "$last_line" ]; then backfill_start if [ $DEBUG -eq 1 ]; then log "DEBUG bootstrap_rotation_or_truncate_backfill last_inode=$last_inode cur_inode=$cur_inode last_line=$last_line cur_lines=$cur_lines start_line=$start_line backfill_lines=$BOOTSTRAP_BACKFILL_LINES" fi else start_line=$((last_line + 1)) if [ $DEBUG -eq 1 ]; then log "DEBUG incremental last_line=$last_line cur_lines=$cur_lines start_line=$start_line" fi fi fi sed -n "${start_line},\$p" "$LOG_SRC" > "$tmp_slice" 2>/dev/null fi slice_lines="$(wc -l < "$tmp_slice" 2>/dev/null | tr -d ' ')" [ -z "$slice_lines" ] && slice_lines=0 if [ $DEBUG -eq 1 ]; then log "DEBUG slice_path=$tmp_slice slice_lines=$slice_lines" fi # Totals (MAC repeats allowed) auth_events_total="$(grep -E "$re_auth" "$tmp_slice" | wc -l | tr -d ' ')" assoc_events_total="$(grep -E "$re_assoc" "$tmp_slice" | wc -l | tr -d ' ')" disassoc_events_total="$(grep -E "$re_disassoc" "$tmp_slice" | wc -l | tr -d ' ')" # Extract MAC right after token "STA" (from hostapd lines) extract_sta_macs() { awk ' { for (i=1; i<=NF; i++) { if ($i=="STA") { print $(i+1); break; } } } ' } # Extract MACs in brackets: [aa:bb:cc:dd:ee:ff] extract_bracket_macs() { grep -oE '\[[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}\]' | tr -d '[]' } # Count unique valid MACs from stdin uniq_count() { grep -E '^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$' \ | sort -u \ | wc -l | tr -d ' ' } # Unique MACs per event type (hostapd) unique_macs_authenticated="$( grep -E "$re_auth" "$tmp_slice" \ | extract_sta_macs \ | uniq_count )" unique_macs_associated="$( grep -E "$re_assoc" "$tmp_slice" \ | extract_sta_macs \ | uniq_count )" unique_macs_disassociated="$( grep -E "$re_disassoc" "$tmp_slice" \ | extract_sta_macs \ | uniq_count )" # Unique MACs seen in "any relevant activity" (union: auth/assoc/disassoc + selected kernel) unique_macs_seen_any="$( ( grep -E 'hostapd.*IEEE 802\.11: (authenticated|associated|disassociated)' "$tmp_slice" \ | extract_sta_macs grep -E "$re_kernel_seen" "$tmp_slice" \ | extract_bracket_macs ) | uniq_count )" ######################################################################## # B-class metrics (ephemeral state in /tmp; reboot loss accepted) ######################################################################## SESSION_STATE="/tmp/connstats.sessions.state" SESSION_STATE_TMP="$TMPDIR/connstats.sessions.state.$$" SESSION_STATE_TTL_SEC=21600 AUTH_ASSOC_MAX_SAMPLES=50 now_epoch_b="$(date -u +%s 2>/dev/null | tr -d ' ')" [ -z "$now_epoch_b" ] && now_epoch_b=0 # Output file (temporary, removed each run) B_OUT_TMP="$TMPDIR/connstats.b.out.$$" TZ=UTC awk -v state_in="$SESSION_STATE" \ -v state_out="$SESSION_STATE_TMP" \ -v now="$now_epoch_b" \ -v ttl="$SESSION_STATE_TTL_SEC" \ -v maxs="$AUTH_ASSOC_MAX_SAMPLES" ' function mon2num(m) { if (m=="Jan") return 1 if (m=="Feb") return 2 if (m=="Mar") return 3 if (m=="Apr") return 4 if (m=="May") return 5 if (m=="Jun") return 6 if (m=="Jul") return 7 if (m=="Aug") return 8 if (m=="Sep") return 9 if (m=="Oct") return 10 if (m=="Nov") return 11 if (m=="Dec") return 12 return 0 } function header_epoch( mon,day,tod,year,m,hh,mm,ss,t) { mon=$2; day=$3; tod=$4 year=strftime("%Y") m=mon2num(mon) if (m==0) return -1 split(tod, t, ":") if (length(t) != 3) return -1 hh=t[1]+0; mm=t[2]+0; ss=t[3]+0 return mktime(sprintf("%d %02d %02d %02d %02d %02d", year, m, (day+0), hh, mm, ss)) } function extract_sta_mac( i) { for (i=1; i<=NF; i++) { if ($i=="STA") return $(i+1) } return "" } function mac_valid(m) { return (m ~ /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/) } BEGIN { if (state_in != "") { while ((getline line < state_in) > 0) { n = split(line, a, " ") if (n >= 2) { m=a[1]; e=a[2]+0 if (mac_valid(m) && e > 0) last_auth[m]=e } } close(state_in) } } { line=$0 if (line !~ /hostapd.*IEEE 802\.11: (authenticated|associated|disassociated)/) next e = header_epoch() if (e < 0) next mac = extract_sta_mac() if (!mac_valid(mac)) next if (line ~ /IEEE 802\.11: authenticated/) { last_auth[mac]=e next } if (line ~ /IEEE 802\.11: associated/) { if (mac in last_auth) { d = e - last_auth[mac] if (d >= 0) { ms = d * 1000 if (sample_count < maxs) { if (samples == "") samples = ms else samples = samples "," ms sample_count++ } } } next } if (line ~ /IEEE 802\.11: disassociated/) { if (mac in last_auth) { d = e - last_auth[mac] if (d >= 0) { full_sessions_total++ sum_len += d if (d < 60) early_drop_sessions_total++ delete last_auth[mac] } } next } } END { if (full_sessions_total > 0) avg_session_length = int((sum_len / full_sessions_total) + 0.5) else avg_session_length = 0 if (state_out != "") { for (m in last_auth) { e = last_auth[m] + 0 if (e > 0 && (now <= 0 || (e >= (now - ttl)))) { print m " " e > state_out } } close(state_out) } print full_sessions_total "\t" avg_session_length "\t" early_drop_sessions_total "\t" samples } ' "$tmp_slice" > "$B_OUT_TMP" 2>/dev/null # Default B fields full_sessions_total=0 avg_session_length=0 early_drop_sessions_total=0 auth_to_assoc_time_ms_samples="" # Parse awk output if [ -s "$B_OUT_TMP" ]; then b_out_line="$(head -n 1 "$B_OUT_TMP" 2>/dev/null)" full_sessions_total="$(echo "$b_out_line" | awk -F'\t' '{print $1}' | tr -d ' ')" avg_session_length="$(echo "$b_out_line" | awk -F'\t' '{print $2}' | tr -d ' ')" early_drop_sessions_total="$(echo "$b_out_line" | awk -F'\t' '{print $3}' | tr -d ' ')" auth_to_assoc_time_ms_samples="$(echo "$b_out_line" | awk -F'\t' '{print $4}')" fi rm -f "$B_OUT_TMP" 2>/dev/null [ -z "$full_sessions_total" ] && full_sessions_total=0 [ -z "$avg_session_length" ] && avg_session_length=0 [ -z "$early_drop_sessions_total" ] && early_drop_sessions_total=0 # Move state into place (ephemeral but persistent across runs until reboot) if [ -f "$SESSION_STATE_TMP" ]; then mv -f "$SESSION_STATE_TMP" "$SESSION_STATE" 2>/dev/null else rm -f "$SESSION_STATE_TMP" 2>/dev/null fi ######################################################################## # C-class metrics (kernel kick reason codes 1..8) ######################################################################## # Only count kernel "station kicked out ..." (avoids PRS DRIVER_LOG "reason" lines) re_kick_reason_base='kernel.*station kicked out.*reason[[:space:]]+' reason_code_events_1="$(grep -E "${re_kick_reason_base}1([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')" reason_code_events_2="$(grep -E "${re_kick_reason_base}2([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')" reason_code_events_3="$(grep -E "${re_kick_reason_base}3([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')" reason_code_events_4="$(grep -E "${re_kick_reason_base}4([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')" reason_code_events_5="$(grep -E "${re_kick_reason_base}5([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')" reason_code_events_6="$(grep -E "${re_kick_reason_base}6([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')" reason_code_events_7="$(grep -E "${re_kick_reason_base}7([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')" reason_code_events_8="$(grep -E "${re_kick_reason_base}8([^0-9]|$)" "$tmp_slice" 2>/dev/null | wc -l | tr -d ' ')" [ -z "$reason_code_events_1" ] && reason_code_events_1=0 [ -z "$reason_code_events_2" ] && reason_code_events_2=0 [ -z "$reason_code_events_3" ] && reason_code_events_3=0 [ -z "$reason_code_events_4" ] && reason_code_events_4=0 [ -z "$reason_code_events_5" ] && reason_code_events_5=0 [ -z "$reason_code_events_6" ] && reason_code_events_6=0 [ -z "$reason_code_events_7" ] && reason_code_events_7=0 [ -z "$reason_code_events_8" ] && reason_code_events_8=0 ######################################################################## # D-class metrics (flaps between ath0 and ath1 within the bucket) ######################################################################## D_OUT_TMP="$TMPDIR/connstats.d.out.$$" awk ' function extract_sta_mac( i) { for (i=1; i<=NF; i++) if ($i=="STA") return $(i+1) return "" } function mac_valid(m) { return (m ~ /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/) } function extract_iface( i, v) { for (i=1; i<=NF; i++) { v = $i if (v ~ /^ath[0-9]+:$/) { sub(/:$/,"",v); return v } } return "" } { line=$0 if (line !~ /hostapd.*IEEE 802\.11: associated/) next iface = extract_iface() if (iface != "ath0" && iface != "ath1") next mac = extract_sta_mac() if (!mac_valid(mac)) next if (mac in last_iface) { prev = last_iface[mac] if (prev != iface) { if (prev=="ath0" && iface=="ath1") { flap_events_0_to_1++ uniq_0_to_1[mac]=1 } else if (prev=="ath1" && iface=="ath0") { flap_events_1_to_0++ uniq_1_to_0[mac]=1 } } } last_iface[mac]=iface } END { for (m in uniq_0_to_1) unique_0_to_1++ for (m in uniq_1_to_0) unique_1_to_0++ if (unique_0_to_1+0 < 0) unique_0_to_1=0 if (flap_events_0_to_1+0 < 0) flap_events_0_to_1=0 if (unique_1_to_0+0 < 0) unique_1_to_0=0 if (flap_events_1_to_0+0 < 0) flap_events_1_to_0=0 print unique_0_to_1 "\t" flap_events_0_to_1 "\t" unique_1_to_0 "\t" flap_events_1_to_0 } ' "$tmp_slice" > "$D_OUT_TMP" 2>/dev/null unique_macs_flap_ath0_to_ath1=0 flap_events_total_ath0_to_ath1=0 unique_macs_flap_ath1_to_ath0=0 flap_events_total_ath1_to_ath0=0 if [ -s "$D_OUT_TMP" ]; then d_out_line="$(head -n 1 "$D_OUT_TMP" 2>/dev/null)" unique_macs_flap_ath0_to_ath1="$(echo "$d_out_line" | awk -F'\t' '{print $1}' | tr -d ' ')" flap_events_total_ath0_to_ath1="$(echo "$d_out_line" | awk -F'\t' '{print $2}' | tr -d ' ')" unique_macs_flap_ath1_to_ath0="$(echo "$d_out_line" | awk -F'\t' '{print $3}' | tr -d ' ')" flap_events_total_ath1_to_ath0="$(echo "$d_out_line" | awk -F'\t' '{print $4}' | tr -d ' ')" fi rm -f "$D_OUT_TMP" 2>/dev/null [ -z "$unique_macs_flap_ath0_to_ath1" ] && unique_macs_flap_ath0_to_ath1=0 [ -z "$flap_events_total_ath0_to_ath1" ] && flap_events_total_ath0_to_ath1=0 [ -z "$unique_macs_flap_ath1_to_ath0" ] && unique_macs_flap_ath1_to_ath0=0 [ -z "$flap_events_total_ath1_to_ath0" ] && flap_events_total_ath1_to_ath0=0 ######################################################################## # Emit one summary line ######################################################################## log "connstats bucket_run slice_lines=$slice_lines unique_macs_seen_any=$unique_macs_seen_any unique_macs_authenticated=$unique_macs_authenticated auth_events_total=$auth_events_total unique_macs_associated=$unique_macs_associated assoc_events_total=$assoc_events_total unique_macs_disassociated=$unique_macs_disassociated disassoc_events_total=$disassoc_events_total full_sessions_total=$full_sessions_total avg_session_length=$avg_session_length early_drop_sessions_total=$early_drop_sessions_total auth_to_assoc_time_ms_samples=$auth_to_assoc_time_ms_samples reason_code_events_1=$reason_code_events_1 reason_code_events_2=$reason_code_events_2 reason_code_events_3=$reason_code_events_3 reason_code_events_4=$reason_code_events_4 reason_code_events_5=$reason_code_events_5 reason_code_events_6=$reason_code_events_6 reason_code_events_7=$reason_code_events_7 reason_code_events_8=$reason_code_events_8 unique_macs_flap_ath0_to_ath1=$unique_macs_flap_ath0_to_ath1 flap_events_total_ath0_to_ath1=$flap_events_total_ath0_to_ath1 unique_macs_flap_ath1_to_ath0=$unique_macs_flap_ath1_to_ath0 flap_events_total_ath1_to_ath0=$flap_events_total_ath1_to_ath0" if [ $DEBUG -eq 1 ]; then log "DEBUG dump_auth_lines_begin" grep -E "$re_auth" "$tmp_slice" | sed 's/^/DEBUG AUTH: /' >> "$OUT_LOG" log "DEBUG dump_assoc_lines_begin" grep -E "$re_assoc" "$tmp_slice" | sed 's/^/DEBUG ASSOC: /' >> "$OUT_LOG" log "DEBUG dump_disassoc_lines_begin" grep -E "$re_disassoc" "$tmp_slice" | sed 's/^/DEBUG DISASSOC: /' >> "$OUT_LOG" log "DEBUG dump_kick_reason_lines_begin" grep -E "${re_kick_reason_base}[1-8]([^0-9]|$)" "$tmp_slice" | sed 's/^/DEBUG KICK: /' >> "$OUT_LOG" log "DEBUG dump_flap_assoc_lines_begin" grep -E 'hostapd.*IEEE 802\.11: associated' "$tmp_slice" | sed 's/^/DEBUG FLAP_ASSOC: /' >> "$OUT_LOG" log "DEBUG dump_flap_events_begin" awk ' function extract_sta_mac( i) { for (i=1; i<=NF; i++) if ($i=="STA") return $(i+1); return "" } function mac_valid(m) { return (m ~ /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/) } function extract_iface( i, v) { for (i=1; i<=NF; i++) { v=$i; if (v ~ /^ath[0-9]+:$/) { sub(/:$/,"",v); return v } } return "" } { if ($0 !~ /hostapd.*IEEE 802\.11: associated/) next iface=extract_iface() if (iface!="ath0" && iface!="ath1") next mac=extract_sta_mac() if (!mac_valid(mac)) next if (mac in last_iface) { prev=last_iface[mac] if (prev!=iface) { if ((prev=="ath0" && iface=="ath1") || (prev=="ath1" && iface=="ath0")) { print "mac=" mac " from=" prev " to=" iface } } } last_iface[mac]=iface } ' "$tmp_slice" | sed 's/^/DEBUG FLAP: /' >> "$OUT_LOG" fi # Update state to current end-of-file line count and inode { echo "inode=$cur_inode" echo "line=$cur_lines" } > "$STATE" rm -f "$tmp_slice" 2>/dev/null exit 0