From 8149d295f9e001c34ca2d9783125d6579e86399d Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Fri, 25 Sep 2026 06:50:57 -0400 Subject: [PATCH 01/10] Update MerlinAU.sh Log out of an AiMesh node immediately when merlinau_fw_update=1 is detected. Skip all further node actions / status while that node is flashing. Report actively flashing nodes as busy instead of failed/unreachable in mn screen. Clean up per-run AiMesh state properly. Preserve failed WebUI login diagnostics in JFFS. Protect concurrent diagnostic writes. Reuse existing log-retention mechanism for the new diagnostic logs.. Clear the firmware-update guard on failed or aborted updates. --- MerlinAU.sh | 190 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 173 insertions(+), 17 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 1b567ab1..74bd4ab3 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -132,6 +132,9 @@ readonly curlHTTPstatusStr="HTTP_Status_Code" readonly curlTmpLogFPath="${TEMP_DIR}/tmpCurl_${ScriptFNameTag}_$$.TMP.LOG" readonly curlErrLogFPath="${TEMP_DIR}/tmpCurl_${ScriptFNameTag}_$$.ERR.LOG" readonly curlTmpRespFile="${TEMP_DIR}/tmpCurl_${ScriptFNameTag}_$$.RESP.TXT" +readonly webUILoginDiagDir="${SETTINGS_DIR}/WebUILoginFailures" +readonly webUILoginDiagLockFile="${TEMP_DIR}/${ScriptFNameTag}_WebUILoginFailures.lock" +readonly webUILoginDiagLockFD=385 # Temporary NVRAM key to indicate when a F/W Update is in progress # readonly nvramTempFWupdateKey="merlinau_fw_update" @@ -2288,15 +2291,17 @@ readonly POST_UPDATE_EMAIL_SCRIPT_HOOK="[ -x $ScriptFilePath ] && $POST_UPDATE_E ##----------------------------------------## _CleanUpOldLogFiles_() { - [ ! -d "$FW_LOG_DIR" ] && return 0 + local logDir="$FW_LOG_DIR" + [ $# -gt 0 ] && [ -n "$1" ] && logDir="$1" + [ ! -d "$logDir" ] && return 0 local retCode numLogFiles topLogFile savedTopLogFile="" - numLogFiles="$(ls -1lt "$FW_LOG_DIR"/*.log 2>/dev/null | wc -l)" + numLogFiles="$(ls -1lt "$logDir"/*.log 2>/dev/null | wc -l)" # Leave one log file (if any available) # [ "$numLogFiles" -lt 2 ] && return 0 # Save the most recent log file # - topLogFile="$(ls -1t "$FW_LOG_DIR"/*.log 2>/dev/null | head -n1)" + topLogFile="$(ls -1t "$logDir"/*.log 2>/dev/null | head -n1)" if [ -n "$topLogFile" ] && [ -s "$topLogFile" ] then @@ -2308,7 +2313,7 @@ _CleanUpOldLogFiles_() fi # Delete logs older than 30 days # - /usr/bin/find -L "$FW_LOG_DIR" -name '*.log' -mtime +30 -exec rm {} \; + /usr/bin/find -L "$logDir" -name '*.log' -mtime +30 -exec rm {} \; retCode="$?" # Restore the most recent log file # @@ -4392,7 +4397,36 @@ _ReEnableAsusTrendMicroProcesses_() } ##------------------------------------------## -## Modified by ExtremeFiretop [2025-Sep-16] ## +## Added by ExtremeFiretop [2026-Sep-24] ## +##------------------------------------------## +_ClearFWUpdateGuard_() +{ + local guardValue + + guardValue="$(nvram get "$nvramTempFWupdateKey" 2>/dev/null)" + [ -z "$guardValue" ] && return 0 + + if ! nvram unset "$nvramTempFWupdateKey" 2>/dev/null + then + Say "${YLWct}*WARNING*${NOct}: Unable to clear the AiMesh F/W-update NVRAM guard [$nvramTempFWupdateKey]." + return 1 + fi + + # The guard may have been persisted by another NVRAM commit while the + # update was in progress. Commit its removal so it cannot return after + # the next reboot. This commit is done only when the guard actually exists. + if ! nvram commit >/dev/null 2>&1 + then + Say "${YLWct}*WARNING*${NOct}: Unable to commit removal of the AiMesh F/W-update NVRAM guard [$nvramTempFWupdateKey]." + return 1 + fi + + Say "Cleared AiMesh F/W-update NVRAM guard [$nvramTempFWupdateKey]." + return 0 +} + +##------------------------------------------## +## Modified by ExtremeFiretop [2026-Sep-24] ## ##------------------------------------------## _DoCleanUp_() { @@ -4410,9 +4444,9 @@ _DoCleanUp_() [ $# -gt 1 ] && [ "$2" -eq 1 ] && keepZIPfile=true [ $# -gt 2 ] && [ "$3" -eq 1 ] && keepWfile=true - # Clear the NVRAM F/W-update guard used by AiMesh nodes # - # This value is intentionally never committed to NVRAM # - nvram unset "$nvramTempFWupdateKey" 2>/dev/null + # Clear the AiMesh F/W-update guard and commit its removal in case + # another firmware component persisted the temporary value. + _ClearFWUpdateGuard_ # Stop the LEDs blinking # _Reset_LEDs_ 1 @@ -4590,8 +4624,79 @@ _CheckForMinimumModelSupport_() "$routerModelCheckFailed" && return 1 || return 0 } +##------------------------------------------## +## Added [2026-Sep-24] ## +##------------------------------------------## +_SaveWebUILoginFailure_() +{ + if [ $# -lt 5 ] || [ -z "$1" ] || [ -z "$2" ] || \ + [ -z "$3" ] || [ -z "$4" ] || [ -z "$5" ] + then return 1 + fi + + local loginTarget="$1" statusSTRx="$2" + local responseFPath="$3" curlTmpLogFile="$4" curlErrLogFile="$5" + local diagTimeStamp diagFile diagIndex=0 + + # Keep WebUI login diagnostics on JFFS regardless of the selected F/W log path. # + [ -d "$webUILoginDiagDir" ] || \ + mkdir -p -m 700 "$webUILoginDiagDir" 2>/dev/null || return 1 + + # Serialize writes because AiMesh node login checks can run in parallel. # + eval exec "$webUILoginDiagLockFD>$webUILoginDiagLockFile" + flock -x "$webUILoginDiagLockFD" 2>/dev/null || \ + { eval exec "${webUILoginDiagLockFD}>&-" ; return 1 ; } + + # Use one timestamped .log file per failure so the existing 30-day log # + # cleanup logic can rotate these diagnostics just like F/W update logs. # + diagTimeStamp="$(date '+%Y-%m-%d_%H_%M_%S')" + diagFile="${webUILoginDiagDir}/${ScriptFNameTag}_WebUILoginFailure_${diagTimeStamp}.log" + while [ -e "$diagFile" ] + do + diagIndex="$((diagIndex + 1))" + diagFile="${webUILoginDiagDir}/${ScriptFNameTag}_WebUILoginFailure_${diagTimeStamp}_$(printf '%02d' "$diagIndex").log" + done + + { + printf '%s\n' "============================================================" + printf '%s - WebUI Login Failure [%s]\n' "$(date +"$LOGdateFormat")" "$loginTarget" + printf 'Result: %s\n' "$statusSTRx" + + printf '%s\n' "-------------------- CURL STATUS ---------------------------" + if [ -s "$curlTmpLogFile" ] + then cat "$curlTmpLogFile" + else printf '%s\n' "" + fi + + printf '%s\n' "-------------------- CURL STDERR ---------------------------" + if [ -s "$curlErrLogFile" ] + then cat "$curlErrLogFile" + else printf '%s\n' "" + fi + + printf '%s\n' "-------------------- ROUTER RESPONSE -----------------------" + if [ -s "$responseFPath" ] + then + # Preserve the full HTTP response but never persist session cookies. # + awk '{ + if (tolower($0) ~ /^set-cookie:/) + print "Set-Cookie: " + else + print + }' "$responseFPath" + else printf '%s\n' "" + fi + printf '\n' + } > "$diagFile" 2>/dev/null + chmod 600 "$diagFile" 2>/dev/null + + flock -u "$webUILoginDiagLockFD" 2>/dev/null + eval exec "${webUILoginDiagLockFD}>&-" + return 0 +} + ##----------------------------------------## -## Modified by Martinski W. [2026-Sep-20] ## +## Modified [2026-Sep-24] ## ##----------------------------------------## _DoMainRouterLogin_() { @@ -4641,6 +4746,12 @@ _DoMainRouterLogin_() fi fi + if [ "$statusCODE" -ne 0 ] + then + _SaveWebUILoginFailure_ "Local Router: $routerURL" "$statusSTRx" \ + "$responseFPath" "$curlTmpLogFPath" "$curlErrLogFPath" + fi + rm -f "$curlErrLogFPath" "$curlTmpLogFPath" "$responseFPath" echo "$statusSTRx" return "$statusCODE" @@ -5492,7 +5603,7 @@ _GetNodeURL_() } ##----------------------------------------## -## Modified by Martinski W. [2026-Sep-21] ## +## Modified [2026-Sep-24] ## ##----------------------------------------## _DoMeshNodeLogin_() { @@ -5546,6 +5657,12 @@ _DoMeshNodeLogin_() fi fi + if [ "$statusCODE" -ne 0 ] + then + _SaveWebUILoginFailure_ "AiMesh Node: $nodeURL" "$statusSTRx" \ + "$responseFPath" "$curlTmpLogFile" "$curlErrLogFile" + fi + rm -f "$curlErrLogFile" "$curlTmpLogFile" "$responseFPath" echo "$statusSTRx" return "$statusCODE" @@ -5612,8 +5729,8 @@ _GetNVRAM_FromWebUI_() } ##----------------------------------------## -## Modified by Martinski W. [2026-Sep-21] ## -##----------------------------------------## +## Modified [2026-Sep-24] ## +##-------------------------------------------## # Trigger the node "Check for updates" (no waiting here) # _MeshNodeTriggerFWCheck_() { @@ -5625,6 +5742,7 @@ _MeshNodeTriggerFWCheck_() local safeID="$(_MeshSafeID_ "$nodeIPv4addr")" local nodeURL="$(_GetNodeURL_ "$nodeIPv4addr")" local cookieFile="/tmp/${runID}.${safeID}.cookie" + local busyFile="/tmp/${runID}.${safeID}.busy" local curlStatus nvramKeyPair # Check for Login Credentials # @@ -5654,7 +5772,14 @@ _MeshNodeTriggerFWCheck_() then if echo "$nvramKeyPair" | grep -qE "\"$nvramTempFWupdateKey\"[[:blank:]]*:[[:blank:]]*\"1\"" then - Say "AiMesh Node [$nodeIPv4addr] entered an active MerlinAU F/W update before start_webs_update. Skipping firmware check." + # Tell the parent process not to query this node again during this run. # + : > "$busyFile" + Say "AiMesh Node [$nodeIPv4addr] entered an active MerlinAU F/W update before start_webs_update. Skipping firmware check and releasing WebUI session." + + # Release the node's single WebUI administration session immediately. # + curl -s -k "${nodeURL}/Logout.asp" \ + --cookie "$cookieFile" --max-time 2 >/dev/null 2>&1 + rm -f "$cookieFile" return 0 fi fi @@ -10424,7 +10549,8 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or # NVRAM key guard set BEFORE restarting/logging into the WebGUI. # Primary routers running MerlinAU can query this NVRAM value and # avoid triggering 'start_webs_update' on AiMesh nodes mid-flash. - # Do *NOT* commit this key value since a reboot must clear it. + # Do *NOT* commit this key here. Cleanup and startup explicitly + # clear and commit its removal so a stale guard cannot survive reboot. #-------------------------------------------------------------------# nvram set "$nvramTempFWupdateKey"=1 rm -f "$fwUploadResponseFile" "$fwUploadDiagFile" @@ -10916,6 +11042,12 @@ _CheckForMinimumRequirements_() _DoStartupInit_() { Say "$SCRIPT_NAME $SCRIPT_VERSION starting up" + + # A successful firmware flash reboots before _DoCleanUp_ can run. + # Any update guard still present during services-start is therefore + # stale and must be removed persistently before normal operation resumes. + _ClearFWUpdateGuard_ + _CreateDirPaths_ _InitCustomDefaultsConfig_ _InitCustomUserSettings_ @@ -11241,8 +11373,8 @@ _ValidatePrivateIPv4Address_() } ##----------------------------------------## -## Modified by Martinski W. [2026-Jan-01] ## -##----------------------------------------## +## Modified [2026-Sep-24] ## +##-------------------------------------------## _ProcessMeshNodes_() { if [ $# -eq 0 ] || [ -z "$1" ] @@ -11293,6 +11425,13 @@ _ProcessMeshNodes_() for nodeIPv4addr in $node_list do _ValidatePrivateIPv4Address_ "$nodeIPv4addr" || continue + local safeID="$(_MeshSafeID_ "$nodeIPv4addr")" + local busyFile="/tmp/${runID}.${safeID}.busy" + + # An actively flashing node was already logged out by the guard check. # + # Do not log back into it just to retrieve status information. # + [ -f "$busyFile" ] && continue + _GetNodeInfo_ "$nodeIPv4addr" "$runID" >/dev/null 2>&1 & done wait @@ -11304,6 +11443,17 @@ _ProcessMeshNodes_() local safeID="$(_MeshSafeID_ "$nodeIPv4addr")" local varsFile="/tmp/${runID}.${safeID}.vars" + local busyFile="/tmp/${runID}.${safeID}.busy" + + if [ -f "$busyFile" ] + then + if "$includeExtraLogic" + then + printf "\n AiMesh Node [${GRNct}%s${NOct}]: MerlinAU F/W update in progress; status query skipped.\n" "$nodeIPv4addr" + uid="$((uid + 1))" + fi + continue + fi # Load per-node globals (node_*, Node_combinedVer, NodeGNUtonFW) # if [ -s "$varsFile" ] @@ -11338,7 +11488,7 @@ _ProcessMeshNodes_() _SendEMailNotification_ AGGREGATED_UPDATE_NOTIFICATION fi - rm -f "/tmp/${runID}."*.vars 2>/dev/null + rm -f "/tmp/${runID}."*.vars "/tmp/${runID}."*.busy "/tmp/${runID}."*.cookie 2>/dev/null else if "$includeExtraLogic" then @@ -12629,6 +12779,12 @@ _RunLockedInitializationChecks_() retCode=1 fi + if ! _CleanUpOldLogFiles_ "$webUILoginDiagDir" + then + Say "${YLWct}*WARNING*${NOct}: Unable to clean up old WebUI login-failure log files." + retCode=1 + fi + # Set variable to 'false' to stop the check # checkWebsUpdateScriptForGnuton="$isGNUtonFW" if ! _Gnuton_Check_Webs_Update_Script_ From cd06e442397fe5b2253465c870f6b8be68c7550d Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Fri, 25 Sep 2026 07:01:40 -0400 Subject: [PATCH 02/10] Fix Logout Request... Standardizing node logouts Fix Logout Request... Standardizing node logouts --- MerlinAU.sh | 95 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 80 insertions(+), 15 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 74bd4ab3..6ef8ffec 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -4636,6 +4636,7 @@ _SaveWebUILoginFailure_() local loginTarget="$1" statusSTRx="$2" local responseFPath="$3" curlTmpLogFile="$4" curlErrLogFile="$5" + local loginOwner="${6:-}" local diagTimeStamp diagFile diagIndex=0 # Keep WebUI login diagnostics on JFFS regardless of the selected F/W log path. # @@ -4661,6 +4662,7 @@ _SaveWebUILoginFailure_() printf '%s\n' "============================================================" printf '%s - WebUI Login Failure [%s]\n' "$(date +"$LOGdateFormat")" "$loginTarget" printf 'Result: %s\n' "$statusSTRx" + [ -n "$loginOwner" ] && printf 'Current WebUI Login Owner: %s\n' "$loginOwner" printf '%s\n' "-------------------- CURL STATUS ---------------------------" if [ -s "$curlTmpLogFile" ] @@ -4749,7 +4751,7 @@ _DoMainRouterLogin_() if [ "$statusCODE" -ne 0 ] then _SaveWebUILoginFailure_ "Local Router: $routerURL" "$statusSTRx" \ - "$responseFPath" "$curlTmpLogFPath" "$curlErrLogFPath" + "$responseFPath" "$curlTmpLogFPath" "$curlErrLogFPath" "$(nvram get login_ip_str 2>/dev/null)" fi rm -f "$curlErrLogFPath" "$curlTmpLogFPath" "$responseFPath" @@ -5728,6 +5730,29 @@ _GetNVRAM_FromWebUI_() return "$statusCODE" } +##---------------------------------------## +## Added [2026-Sep-24] ## +##---------------------------------------## +_DoMeshNodeLogout_() +{ + if [ $# -lt 2 ] || [ -z "$1" ] || [ -z "$2" ] + then return 1 + fi + + local nodeURL="$1" cookieFile="$2" + + curl -s -k "${nodeURL}/Logout.asp" \ + --referer "${nodeURL}/Main_Login.asp" \ + --user-agent 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0' \ + -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' \ + -H 'Accept-Language: en-US,en;q=0.5' \ + -H 'Accept-Encoding: gzip, deflate' \ + -H 'Connection: keep-alive' \ + -H 'Upgrade-Insecure-Requests: 0' \ + --cookie "$cookieFile" \ + --max-time 3 >/dev/null 2>&1 +} + ##----------------------------------------## ## Modified [2026-Sep-24] ## ##-------------------------------------------## @@ -5777,8 +5802,7 @@ _MeshNodeTriggerFWCheck_() Say "AiMesh Node [$nodeIPv4addr] entered an active MerlinAU F/W update before start_webs_update. Skipping firmware check and releasing WebUI session." # Release the node's single WebUI administration session immediately. # - curl -s -k "${nodeURL}/Logout.asp" \ - --cookie "$cookieFile" --max-time 2 >/dev/null 2>&1 + _DoMeshNodeLogout_ "$nodeURL" "$cookieFile" rm -f "$cookieFile" return 0 fi @@ -5876,7 +5900,7 @@ _GetNodeInfo_() if [ "$curlCode" -ne 0 ] || [ -z "$htmlContent" ] then # Logout best-effort # - curl -s -k "${nodeURL}/Logout.asp" --cookie "$cookieFile" --max-time 2 >/dev/null 2>&1 + _DoMeshNodeLogout_ "$nodeURL" "$cookieFile" printf "\n${REDct}Failed to get information for AiMesh Node [$nodeIPv4addr].${NOct}\n" rm -f "$cookieFile" return 1 @@ -5899,16 +5923,7 @@ _GetNodeInfo_() Node_combinedVer="${node_firmver}.${node_buildno}.$node_extendno" # Logout request # - curl -s -k "${nodeURL}/Logout.asp" \ - -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0' \ - -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' \ - -H 'Accept-Language: en-US,en;q=0.5' \ - -H 'Accept-Encoding: gzip, deflate' \ - -H 'Connection: keep-alive' \ - -H "Referer: ${nodeURL}/Main_Login.asp" \ - -H 'Upgrade-Insecure-Requests: 0' \ - --cookie "$cookieFile" \ - --max-time 2 >/dev/null 2>&1 + _DoMeshNodeLogout_ "$nodeURL" "$cookieFile" curlCode="$?" # Write a vars file the parent shell can source safely # @@ -10667,6 +10682,23 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or if ! nvramKeyPair="$(_GetNVRAM_FromWebUI_ "$routerURL" "$cookieFile" "$nvramTempFWupdateKey" "$$")" then rm -f "$cookieFile" + + # If another WebUI session still owns httpd, give its logout a short + # grace period to complete before attempting to acquire a new session. + loginOwner="$(nvram get login_ip_str 2>/dev/null)" + loginWaitSecs=0 + while [ -n "$loginOwner" ] && [ "$loginOwner" != "0.0.0.0" ] && \ + [ "$loginWaitSecs" -lt 5 ] + do + sleep 1 + loginWaitSecs="$((loginWaitSecs + 1))" + loginOwner="$(nvram get login_ip_str 2>/dev/null)" + done + if [ -n "$loginOwner" ] && [ "$loginOwner" != "0.0.0.0" ] + then + _MsgToSysLog_ "*WARNING*: WebUI session owner [$loginOwner] is still active before Router Login 2nd Attempt." + fi + if ! curlStatus="$(_DoMainRouterLogin_ "$routerURL" "$credsENC" "$cookieFile")" then rm -f "$cookieFile" @@ -11449,7 +11481,7 @@ _ProcessMeshNodes_() then if "$includeExtraLogic" then - printf "\n AiMesh Node [${GRNct}%s${NOct}]: MerlinAU F/W update in progress; status query skipped.\n" "$nodeIPv4addr" + _PrintBusyNodeInfo_ "$nodeIPv4addr" "$uid" uid="$((uid + 1))" fi continue @@ -11903,6 +11935,39 @@ _SimpleNotificationDate_() echo "$(date -d @$notifyTimeSecs +"%Y-%b-%d %I:%M %p")" } +##---------------------------------------## +## Added [2026-Sep-24] ## +##---------------------------------------## +_PrintBusyNodeInfo_() +{ + local node_info="$1" uid="$2" + local line1="Node ID: ${uid}" + local line2="AiMesh Node: ${node_info}" + local line3="MerlinAU F/W Update: IN PROGRESS" + local line4="Status Query: SKIPPED" + local max_length=0 line length h_line='' + + for line in "$line1" "$line2" "$line3" "$line4" + do + length="$(printf "%s" "$line" | awk '{print length}')" + [ "$length" -gt "$max_length" ] && max_length="$length" + done + + for i in $(awk "BEGIN{for(i=1;i<=$max_length;i++) print i}") + do h_line="${h_line}─" ; done + + printf "\n ┌─%s─┐" "$h_line" + length="$(printf "%s" "$line1" | awk '{print length}')" + printf "\n │ %s%*s │" "$line1" "$((max_length - length))" "" + length="$(printf "%s" "$line2" | awk '{print length}')" + printf "\n │ AiMesh Node: ${GRNct}%s${NOct}%*s │" "$node_info" "$((max_length - length))" "" + length="$(printf "%s" "$line3" | awk '{print length}')" + printf "\n │ MerlinAU F/W Update: ${YLWct}IN PROGRESS${NOct}%*s │" "$((max_length - length))" "" + length="$(printf "%s" "$line4" | awk '{print length}')" + printf "\n │ Status Query: ${YLWct}SKIPPED${NOct}%*s │" "$((max_length - length))" "" + printf "\n └─%s─┘" "$h_line" +} + ##---------------------------------------## ## Added by ExtremeFiretop [2024-Mar-27] ## ##---------------------------------------## From 4d3b2ffd19f55cc6bc8112bf8fa1b27b78e44ebe Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Fri, 25 Sep 2026 09:39:54 -0400 Subject: [PATCH 03/10] Add httpd restart to allow login removes the ineffective 5-second wait for login_ip_str to clear... if the existing cookie has failed and login_ip_str is still populated, restarts httpd --- MerlinAU.sh | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 6ef8ffec..957f2b7f 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -20,7 +20,7 @@ set -u ## Set version for each Production Release ## readonly SCRIPT_VERSION=1.6.9 -readonly SCRIPT_VERSTAG="26092216" +readonly SCRIPT_VERSTAG="26092509" readonly SCRIPT_NAME="MerlinAU" ## Set to "master" for Production Releases ## SCRIPT_BRANCH="dev" @@ -5731,7 +5731,7 @@ _GetNVRAM_FromWebUI_() } ##---------------------------------------## -## Added [2026-Sep-24] ## +## Added by ExtremeFiretop [2026-Sep-24] ## ##---------------------------------------## _DoMeshNodeLogout_() { @@ -10177,7 +10177,7 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or local retCode credsENC="" local currentVersionNum="" releaseVersionNum="" - local current_version="" + local current_version="" loginOwner="" curlStatus="" # Create directory for downloading & extracting firmware # if ! _CreateDirectory_ "$FW_ZIP_DIR" ; then return 1 ; fi @@ -10683,20 +10683,18 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or then rm -f "$cookieFile" - # If another WebUI session still owns httpd, give its logout a short - # grace period to complete before attempting to acquire a new session. + # AiMesh nodes can reject Logout.asp before http_logout() runs, + # leaving the primary router recorded as the WebUI session owner. + # If any stale owner remains after our Cookie fails validation, + # reset httpd locally to release that server-side session before + # attempting to acquire a new login Cookie. + curlStatus="" loginOwner="$(nvram get login_ip_str 2>/dev/null)" - loginWaitSecs=0 - while [ -n "$loginOwner" ] && [ "$loginOwner" != "0.0.0.0" ] && \ - [ "$loginWaitSecs" -lt 5 ] - do - sleep 1 - loginWaitSecs="$((loginWaitSecs + 1))" - loginOwner="$(nvram get login_ip_str 2>/dev/null)" - done if [ -n "$loginOwner" ] && [ "$loginOwner" != "0.0.0.0" ] then - _MsgToSysLog_ "*WARNING*: WebUI session owner [$loginOwner] is still active before Router Login 2nd Attempt." + _MsgToSysLog_ "*WARNING*: WebUI session owner [$loginOwner] is still active before Router Login 2nd Attempt. Restarting web server." + /sbin/service restart_httpd >/dev/null 2>&1 + sleep 3 fi if ! curlStatus="$(_DoMainRouterLogin_ "$routerURL" "$credsENC" "$cookieFile")" @@ -11936,7 +11934,7 @@ _SimpleNotificationDate_() } ##---------------------------------------## -## Added [2026-Sep-24] ## +## Added by ExtremeFiretop [2026-Sep-24] ## ##---------------------------------------## _PrintBusyNodeInfo_() { From b98caf8181cb95716d7d6f3a460c645564511385 Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Fri, 25 Sep 2026 10:05:12 -0400 Subject: [PATCH 04/10] Cleanup Cleanup --- MerlinAU.sh | 129 +++++++--------------------------------------------- 1 file changed, 16 insertions(+), 113 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 957f2b7f..d60c9abe 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -4,7 +4,7 @@ # # Project Created: 2023-Oct-01 by @ExtremeFiretop # Official Co-Author: @Martinski W. since 2023-Nov-01 -# Last Modified: 2026-Sep-22 +# Last Modified: 2026-Sep-25 # # MerlinAU™ / MerlinAutoUpdate™ # Official project: https://github.com/ExtremeFiretop/MerlinAutoUpdate-Router @@ -132,9 +132,6 @@ readonly curlHTTPstatusStr="HTTP_Status_Code" readonly curlTmpLogFPath="${TEMP_DIR}/tmpCurl_${ScriptFNameTag}_$$.TMP.LOG" readonly curlErrLogFPath="${TEMP_DIR}/tmpCurl_${ScriptFNameTag}_$$.ERR.LOG" readonly curlTmpRespFile="${TEMP_DIR}/tmpCurl_${ScriptFNameTag}_$$.RESP.TXT" -readonly webUILoginDiagDir="${SETTINGS_DIR}/WebUILoginFailures" -readonly webUILoginDiagLockFile="${TEMP_DIR}/${ScriptFNameTag}_WebUILoginFailures.lock" -readonly webUILoginDiagLockFD=385 # Temporary NVRAM key to indicate when a F/W Update is in progress # readonly nvramTempFWupdateKey="merlinau_fw_update" @@ -2291,17 +2288,14 @@ readonly POST_UPDATE_EMAIL_SCRIPT_HOOK="[ -x $ScriptFilePath ] && $POST_UPDATE_E ##----------------------------------------## _CleanUpOldLogFiles_() { - local logDir="$FW_LOG_DIR" - [ $# -gt 0 ] && [ -n "$1" ] && logDir="$1" - [ ! -d "$logDir" ] && return 0 local retCode numLogFiles topLogFile savedTopLogFile="" - numLogFiles="$(ls -1lt "$logDir"/*.log 2>/dev/null | wc -l)" + numLogFiles="$(ls -1lt "$FW_LOG_DIR"/*.log 2>/dev/null | wc -l)" # Leave one log file (if any available) # [ "$numLogFiles" -lt 2 ] && return 0 # Save the most recent log file # - topLogFile="$(ls -1t "$logDir"/*.log 2>/dev/null | head -n1)" + topLogFile="$(ls -1t "$FW_LOG_DIR"/*.log 2>/dev/null | head -n1)" if [ -n "$topLogFile" ] && [ -s "$topLogFile" ] then @@ -2313,7 +2307,7 @@ _CleanUpOldLogFiles_() fi # Delete logs older than 30 days # - /usr/bin/find -L "$logDir" -name '*.log' -mtime +30 -exec rm {} \; + /usr/bin/find -L "$FW_LOG_DIR" -name '*.log' -mtime +30 -exec rm {} \; retCode="$?" # Restore the most recent log file # @@ -4625,81 +4619,8 @@ _CheckForMinimumModelSupport_() } ##------------------------------------------## -## Added [2026-Sep-24] ## +## Modified by ExtremeFiretop [2026-Sep-24] ## ##------------------------------------------## -_SaveWebUILoginFailure_() -{ - if [ $# -lt 5 ] || [ -z "$1" ] || [ -z "$2" ] || \ - [ -z "$3" ] || [ -z "$4" ] || [ -z "$5" ] - then return 1 - fi - - local loginTarget="$1" statusSTRx="$2" - local responseFPath="$3" curlTmpLogFile="$4" curlErrLogFile="$5" - local loginOwner="${6:-}" - local diagTimeStamp diagFile diagIndex=0 - - # Keep WebUI login diagnostics on JFFS regardless of the selected F/W log path. # - [ -d "$webUILoginDiagDir" ] || \ - mkdir -p -m 700 "$webUILoginDiagDir" 2>/dev/null || return 1 - - # Serialize writes because AiMesh node login checks can run in parallel. # - eval exec "$webUILoginDiagLockFD>$webUILoginDiagLockFile" - flock -x "$webUILoginDiagLockFD" 2>/dev/null || \ - { eval exec "${webUILoginDiagLockFD}>&-" ; return 1 ; } - - # Use one timestamped .log file per failure so the existing 30-day log # - # cleanup logic can rotate these diagnostics just like F/W update logs. # - diagTimeStamp="$(date '+%Y-%m-%d_%H_%M_%S')" - diagFile="${webUILoginDiagDir}/${ScriptFNameTag}_WebUILoginFailure_${diagTimeStamp}.log" - while [ -e "$diagFile" ] - do - diagIndex="$((diagIndex + 1))" - diagFile="${webUILoginDiagDir}/${ScriptFNameTag}_WebUILoginFailure_${diagTimeStamp}_$(printf '%02d' "$diagIndex").log" - done - - { - printf '%s\n' "============================================================" - printf '%s - WebUI Login Failure [%s]\n' "$(date +"$LOGdateFormat")" "$loginTarget" - printf 'Result: %s\n' "$statusSTRx" - [ -n "$loginOwner" ] && printf 'Current WebUI Login Owner: %s\n' "$loginOwner" - - printf '%s\n' "-------------------- CURL STATUS ---------------------------" - if [ -s "$curlTmpLogFile" ] - then cat "$curlTmpLogFile" - else printf '%s\n' "" - fi - - printf '%s\n' "-------------------- CURL STDERR ---------------------------" - if [ -s "$curlErrLogFile" ] - then cat "$curlErrLogFile" - else printf '%s\n' "" - fi - - printf '%s\n' "-------------------- ROUTER RESPONSE -----------------------" - if [ -s "$responseFPath" ] - then - # Preserve the full HTTP response but never persist session cookies. # - awk '{ - if (tolower($0) ~ /^set-cookie:/) - print "Set-Cookie: " - else - print - }' "$responseFPath" - else printf '%s\n' "" - fi - printf '\n' - } > "$diagFile" 2>/dev/null - chmod 600 "$diagFile" 2>/dev/null - - flock -u "$webUILoginDiagLockFD" 2>/dev/null - eval exec "${webUILoginDiagLockFD}>&-" - return 0 -} - -##----------------------------------------## -## Modified [2026-Sep-24] ## -##----------------------------------------## _DoMainRouterLogin_() { if [ $# -lt 3 ] || [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] @@ -4748,12 +4669,6 @@ _DoMainRouterLogin_() fi fi - if [ "$statusCODE" -ne 0 ] - then - _SaveWebUILoginFailure_ "Local Router: $routerURL" "$statusSTRx" \ - "$responseFPath" "$curlTmpLogFPath" "$curlErrLogFPath" "$(nvram get login_ip_str 2>/dev/null)" - fi - rm -f "$curlErrLogFPath" "$curlTmpLogFPath" "$responseFPath" echo "$statusSTRx" return "$statusCODE" @@ -5604,9 +5519,9 @@ _GetNodeURL_() echo "${urlProto}://${nodeIPv4addr}${urlPort}" } -##----------------------------------------## -## Modified [2026-Sep-24] ## -##----------------------------------------## +##------------------------------------------## +## Modified by ExtremeFiretop [2026-Sep-24] ## +##------------------------------------------## _DoMeshNodeLogin_() { if [ $# -lt 4 ] || [ -z "$1" ] || \ @@ -5659,12 +5574,6 @@ _DoMeshNodeLogin_() fi fi - if [ "$statusCODE" -ne 0 ] - then - _SaveWebUILoginFailure_ "AiMesh Node: $nodeURL" "$statusSTRx" \ - "$responseFPath" "$curlTmpLogFile" "$curlErrLogFile" - fi - rm -f "$curlErrLogFile" "$curlTmpLogFile" "$responseFPath" echo "$statusSTRx" return "$statusCODE" @@ -5753,9 +5662,9 @@ _DoMeshNodeLogout_() --max-time 3 >/dev/null 2>&1 } -##----------------------------------------## -## Modified [2026-Sep-24] ## -##-------------------------------------------## +##------------------------------------------## +## Modified by ExtremeFiretop [2026-Sep-24] ## +##------------------------------------------## # Trigger the node "Check for updates" (no waiting here) # _MeshNodeTriggerFWCheck_() { @@ -5798,7 +5707,7 @@ _MeshNodeTriggerFWCheck_() if echo "$nvramKeyPair" | grep -qE "\"$nvramTempFWupdateKey\"[[:blank:]]*:[[:blank:]]*\"1\"" then # Tell the parent process not to query this node again during this run. # - : > "$busyFile" + touch "$busyFile" Say "AiMesh Node [$nodeIPv4addr] entered an active MerlinAU F/W update before start_webs_update. Skipping firmware check and releasing WebUI session." # Release the node's single WebUI administration session immediately. # @@ -10677,7 +10586,7 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or # Double-check IF the existing Cookie is still valid. If it's not, # attempt to get a NEW login session Cookie by logging in again. # If this login fails now then we have to abort here and reboot. - # Added by Martinski W. [2026-Sep-20] + # Modified by ExtremeFiretop [2026-Sep-25] #-------------------------------------------------------------------# if ! nvramKeyPair="$(_GetNVRAM_FromWebUI_ "$routerURL" "$cookieFile" "$nvramTempFWupdateKey" "$$")" then @@ -11402,9 +11311,9 @@ _ValidatePrivateIPv4Address_() fi } -##----------------------------------------## -## Modified [2026-Sep-24] ## -##-------------------------------------------## +##------------------------------------------## +## Modified by ExtremeFiretop [2026-Sep-24] ## +##------------------------------------------## _ProcessMeshNodes_() { if [ $# -eq 0 ] || [ -z "$1" ] @@ -12842,12 +12751,6 @@ _RunLockedInitializationChecks_() retCode=1 fi - if ! _CleanUpOldLogFiles_ "$webUILoginDiagDir" - then - Say "${YLWct}*WARNING*${NOct}: Unable to clean up old WebUI login-failure log files." - retCode=1 - fi - # Set variable to 'false' to stop the check # checkWebsUpdateScriptForGnuton="$isGNUtonFW" if ! _Gnuton_Check_Webs_Update_Script_ From 41a203438b03d743032c5d952f97e5d057e97038 Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Fri, 25 Sep 2026 10:15:32 -0400 Subject: [PATCH 05/10] improve wording improve wording --- MerlinAU.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index d60c9abe..8c2685d4 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -5708,9 +5708,9 @@ _MeshNodeTriggerFWCheck_() then # Tell the parent process not to query this node again during this run. # touch "$busyFile" - Say "AiMesh Node [$nodeIPv4addr] entered an active MerlinAU F/W update before start_webs_update. Skipping firmware check and releasing WebUI session." + Say "AiMesh Node [$nodeIPv4addr] entered an active MerlinAU F/W update before start_webs_update. Skipping firmware check and attempting to release WebUI session." - # Release the node's single WebUI administration session immediately. # + # Best-effort logout. Some AiMesh firmware blocks Logout.asp while re_mode=1. # _DoMeshNodeLogout_ "$nodeURL" "$cookieFile" rm -f "$cookieFile" return 0 @@ -5831,7 +5831,7 @@ _GetNodeInfo_() # Combine extracted information into one string # Node_combinedVer="${node_firmver}.${node_buildno}.$node_extendno" - # Logout request # + # Logout best-effort. Some AiMesh firmware blocks Logout.asp while re_mode=1. # _DoMeshNodeLogout_ "$nodeURL" "$cookieFile" curlCode="$?" From bd1a76171199b26776ba536681ee5779eb748b62 Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Fri, 25 Sep 2026 14:21:24 -0400 Subject: [PATCH 06/10] improve notes improve notes --- MerlinAU.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index e703a3f0..781108fe 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -5658,6 +5658,11 @@ _DoMeshNodeLogout_() local nodeURL="$1" cookieFile="$2" + # Best-effort only: AiMesh nodes normally run with re_mode=1, which intercepts # + # Logout.asp and returns message.htm instead. In that normal state this request # + # does NOT clear the server-side session owner, even when curl itself succeeds. # + # It is only useful when the node UI restriction has been manually disabled # + # (for example re_mode=0). Callers should not depend on this request releasing the WebUI. # curl -s -k "${nodeURL}/Logout.asp" \ --referer "${nodeURL}/Main_Login.asp" \ --user-agent 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0' \ @@ -5718,7 +5723,7 @@ _MeshNodeTriggerFWCheck_() touch "$busyFile" Say "AiMesh Node [$nodeIPv4addr] entered an active MerlinAU F/W update before start_webs_update. Skipping firmware check and attempting to release WebUI session." - # Best-effort logout. Some AiMesh firmware blocks Logout.asp while re_mode=1. # + # Best-effort only; usually a no-op while normal AiMesh re_mode=1 is active. # _DoMeshNodeLogout_ "$nodeURL" "$cookieFile" rm -f "$cookieFile" return 0 @@ -5816,7 +5821,7 @@ _GetNodeInfo_() if [ "$curlCode" -ne 0 ] || [ -z "$htmlContent" ] then - # Logout best-effort # + # Logout best-effort; usually a no-op while normal AiMesh re_mode=1 is active. # _DoMeshNodeLogout_ "$nodeURL" "$cookieFile" printf "\n${REDct}Failed to get information for AiMesh Node [$nodeIPv4addr].${NOct}\n" rm -f "$cookieFile" @@ -5839,7 +5844,7 @@ _GetNodeInfo_() # Combine extracted information into one string # Node_combinedVer="${node_firmver}.${node_buildno}.$node_extendno" - # Logout best-effort. Some AiMesh firmware blocks Logout.asp while re_mode=1. # + # Logout best-effort; usually a no-op while normal AiMesh re_mode=1 is active. # _DoMeshNodeLogout_ "$nodeURL" "$cookieFile" curlCode="$?" From 1563e45d7b182a657cf791fe6e86bb08cd04226b Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Fri, 25 Sep 2026 20:40:21 -0400 Subject: [PATCH 07/10] Moving the flashing wording Moving the flashing wording --- MerlinAU.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 781108fe..5c41e8ac 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -10591,10 +10591,6 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or #------------------------------------------------------------------# _Unmount_Eject_USB_Drives_ - echo - Say "Flashing ${GRNct}${firmware_file}${NOct}...\n${REDct}Please wait for reboot in about 4 minutes or less.${NOct}" - echo - #-------------------------------------------------------------------# # Double-check IF the existing Cookie is still valid. If it's not, # attempt to get a NEW login session Cookie by logging in again. @@ -10632,6 +10628,10 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or fi fi + echo + Say "Flashing ${GRNct}${firmware_file}${NOct}...\n${REDct}Please wait for reboot in about 4 minutes or less.${NOct}" + echo + #----------------------------------------------------------------------------------# # **IMPORTANT NOTE**: # Due to the nature of 'nohup' and the specific behavior of this 'Curl' request, From ee394bb21bada0d0d4a71b690ded61537be1508a Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Fri, 25 Sep 2026 20:44:57 -0400 Subject: [PATCH 08/10] wording improvements wording improvements --- MerlinAU.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 5c41e8ac..1ffdb319 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -10610,7 +10610,7 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or loginOwner="$(nvram get login_ip_str 2>/dev/null)" if [ -n "$loginOwner" ] && [ "$loginOwner" != "0.0.0.0" ] then - _MsgToSysLog_ "*WARNING*: WebUI session owner [$loginOwner] is still active before Router Login 2nd Attempt. Restarting web server." + _MsgToSysLog_ "*WARNING*: WebUI owner [$loginOwner] is holding the session. Restarting web server." /sbin/service restart_httpd >/dev/null 2>&1 sleep 3 fi From 3218bbb95aa385a69858c8a66113fdff2811beec Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Sat, 26 Sep 2026 07:45:41 -0400 Subject: [PATCH 09/10] Additional cleanup Additional cleanup --- MerlinAU.sh | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 1ffdb319..b0b49ee2 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -2293,6 +2293,7 @@ readonly POST_UPDATE_EMAIL_SCRIPT_HOOK="[ -x $ScriptFilePath ] && $POST_UPDATE_E ##----------------------------------------## _CleanUpOldLogFiles_() { + [ ! -d "$FW_LOG_DIR" ] && return 0 local retCode numLogFiles topLogFile savedTopLogFile="" numLogFiles="$(ls -1lt "$FW_LOG_DIR"/*.log 2>/dev/null | wc -l)" @@ -4626,9 +4627,9 @@ _CheckForMinimumModelSupport_() "$routerModelCheckFailed" && return 1 || return 0 } -##------------------------------------------## -## Modified by ExtremeFiretop [2026-Sep-24] ## -##------------------------------------------## +##----------------------------------------## +## Modified by Martinski W. [2026-Sep-20] ## +##----------------------------------------## _DoMainRouterLogin_() { if [ $# -lt 3 ] || [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] @@ -5527,9 +5528,9 @@ _GetNodeURL_() echo "${urlProto}://${nodeIPv4addr}${urlPort}" } -##------------------------------------------## -## Modified by ExtremeFiretop [2026-Sep-24] ## -##------------------------------------------## +##----------------------------------------## +## Modified by Martinski W. [2026-Sep-20] ## +##----------------------------------------## _DoMeshNodeLogin_() { if [ $# -lt 4 ] || [ -z "$1" ] || \ From 187f8012a98d41ed5c4eed48e3bea322f7ebdf01 Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Sat, 26 Sep 2026 07:47:25 -0400 Subject: [PATCH 10/10] Correct date Correct date --- MerlinAU.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index b0b49ee2..30a495e6 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -5529,7 +5529,7 @@ _GetNodeURL_() } ##----------------------------------------## -## Modified by Martinski W. [2026-Sep-20] ## +## Modified by Martinski W. [2026-Sep-21] ## ##----------------------------------------## _DoMeshNodeLogin_() {