From c42dc7e013299e5b712b247723bfe52da120da8d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 19:05:53 +0000 Subject: [PATCH 1/2] Retry web admin port bind instead of crashing on the first race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live-confirmed: OSError: [Errno 98] Address already in use on the web admin's TCPServer bind, right after a container recreate under network_mode: host. Unlike bridge-mode port publishing, there's no Docker-managed mapping to instantly free on teardown — the previous container's own web admin process has to actually die first, and a fast recreate-right-after-recreate can race that. The process crashed immediately instead of retrying, so the web admin silently never came up despite entrypoint.sh correctly launching it. Retry the bind up to 10 times with a 2s backoff before giving up. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi --- vendor/easy-asterisk/easy-asterisk-v0.10.0.sh | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/vendor/easy-asterisk/easy-asterisk-v0.10.0.sh b/vendor/easy-asterisk/easy-asterisk-v0.10.0.sh index a3b1c55..d16fa12 100755 --- a/vendor/easy-asterisk/easy-asterisk-v0.10.0.sh +++ b/vendor/easy-asterisk/easy-asterisk-v0.10.0.sh @@ -4116,6 +4116,7 @@ import json import subprocess import os import re +import time import base64 import hashlib import html @@ -6134,7 +6135,25 @@ class WebAdminHandler(http.server.BaseHTTPRequestHandler): self.wfile.write(json.dumps(data).encode()) def main(): - with socketserver.TCPServer(("", PORT), WebAdminHandler) as httpd: + # A container recreate under network_mode: host depends on the *previous* + # container's web admin process actually dying before the port is free — + # there's no Docker-managed port mapping to instantly release it like + # there would be in bridge mode. A fast recreate-right-after-recreate can + # briefly race that teardown. Retry instead of crashing on the first + # failure, which otherwise means the web admin silently never comes up. + httpd = None + last_err = None + for attempt in range(10): + try: + httpd = socketserver.TCPServer(("", PORT), WebAdminHandler) + break + except OSError as e: + last_err = e + print(f"Port {PORT} not free yet ({e}), retrying...") + time.sleep(2) + if httpd is None: + raise last_err + with httpd: print(f"Easy Asterisk Web Admin running on port {PORT}") httpd.serve_forever() From 4d2829f116983590e72b3838b00dd6359589be80 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 19:12:47 +0000 Subject: [PATCH 2/2] Wait for the web admin process to actually die on shutdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complements the retry fix on the bind side: pkill only sends SIGTERM and returns immediately, it doesn't wait for the process to exit and release its socket. Under network_mode: host there's no Docker- managed port mapping to tear down, so the next container's bind attempt was racing however long this process actually took to die — sometimes still holding the port when the next container started. Poll for it to actually exit (up to 2s), falling back to SIGKILL if it's still lingering, before proceeding with the rest of shutdown. With a clean handoff here, the web admin's own bind-retry (previous commit) should rarely even need to kick in. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015X1jRGHwrvovz2qkhKfDZi --- vendor/easy-asterisk/docker/entrypoint.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/vendor/easy-asterisk/docker/entrypoint.sh b/vendor/easy-asterisk/docker/entrypoint.sh index 943b3d6..7bdf744 100755 --- a/vendor/easy-asterisk/docker/entrypoint.sh +++ b/vendor/easy-asterisk/docker/entrypoint.sh @@ -464,7 +464,20 @@ fi # ── 11. Signal handling for clean shutdown ──────────────────── cleanup() { log_info "Shutting down..." - pkill -f "easy-asterisk-webadmin" 2>/dev/null || true + # pkill only sends the signal and returns immediately — it doesn't wait + # for the process to actually exit and release its listening socket. + # Under network_mode: host there's no Docker-managed port mapping to + # tear down, so the next container's bind attempt races however long + # this process actually takes to die. Wait for it (briefly) instead of + # racing the next container's startup — it retries too now, but a + # clean handoff here means it usually shouldn't need to. + if pkill -f "easy-asterisk-webadmin" 2>/dev/null; then + for i in $(seq 1 20); do + pgrep -f "easy-asterisk-webadmin" >/dev/null 2>&1 || break + sleep 0.1 + done + pkill -9 -f "easy-asterisk-webadmin" 2>/dev/null || true + fi asterisk -rx "core stop now" 2>/dev/null || true exit 0 }