Wait until Caddy startup is actually complete, keep reading stderr in the background.

This commit is contained in:
Leo Vasanko
2025-12-06 22:55:03 +00:00
parent f74b32c70b
commit 82095fd7ac
+68 -28
View File
@@ -28,7 +28,6 @@ import shutil
import signal
import subprocess
import sys
import time
from pathlib import Path
from sys import stderr
from threading import Thread
@@ -241,38 +240,79 @@ def run_caddy(origins: list[str], vite_port: int) -> subprocess.Popen | None:
stderr.write(f"┃ ⚠️ Caddy couldn't start: {e}\n")
return None
# Wait briefly to check if Caddy exits immediately (e.g., port binding failure)
time.sleep(0.2)
# Read stderr line by line until Caddy signals it's ready or exits
# Caddy outputs JSON logs; "serving initial configuration" means it's ready
ready = False
while True:
exit_code = caddy_process.poll()
if exit_code is not None:
# Read and display stderr output, filtering and formatting JSON log lines
caddy_stderr = caddy_process.stderr.read().decode() if caddy_process.stderr else ""
stderr.write(f"┃ ⚠️ Caddy startup failed\n")
if caddy_stderr:
for line in caddy_stderr.splitlines():
if not line:
continue
try:
log = json.loads(line)
# Filter out info-level messages
if log.get("level") == "info":
continue
# Filter out expected "admin endpoint disabled" warning
if log.get("logger") == "admin" and "admin endpoint disabled" in log.get("msg", ""):
continue
# Format: [level] logger: msg
level = log.get("level", "?").upper()
logger = log.get("logger", "")
msg = log.get("msg", "")
if logger:
stderr.write(f"┃ [{level}] {logger}: {msg}\n")
else:
stderr.write(f"┃ [{level}] {msg}\n")
except json.JSONDecodeError:
# Not JSON, print as-is (e.g., "Error: ..." lines)
# Process exited - read remaining stderr and report failure
remaining = caddy_process.stderr.read().decode() if caddy_process.stderr else ""
stderr.write(f"┃ ⚠️ Caddy startup failed (exit code {exit_code})\n")
if remaining:
for line in remaining.splitlines():
if line:
stderr.write(f"{line}\n")
return None
# Read one line from stderr (blocks until data available)
line = caddy_process.stderr.readline().decode()
if not line:
continue
# Try to parse as JSON log
try:
log = json.loads(line)
msg = log.get("msg", "")
level = log.get("level", "")
# Check for ready signal
if "serving initial configuration" in msg:
ready = True
break
# Show errors/warnings to user
if level in ("error", "fatal"):
logger = log.get("logger", "")
if logger:
stderr.write(f"┃ [{level.upper()}] {logger}: {msg}\n")
else:
stderr.write(f"┃ [{level.upper()}] {msg}\n")
except json.JSONDecodeError:
# Not JSON - might be sudo password prompt or other output
# Pass through to user
stderr.write(line)
stderr.flush()
if ready:
break
# Start a background thread to drain stderr and show errors
def drain_stderr():
while True:
line = caddy_process.stderr.readline().decode()
if not line:
break
try:
log = json.loads(line)
level = log.get("level", "")
if level in ("error", "fatal", "warn"):
msg = log.get("msg", "")
logger = log.get("logger", "")
# Skip expected warnings
if logger == "admin" and "admin endpoint disabled" in msg:
continue
if logger:
stderr.write(f"┃ [{level.upper()}] {logger}: {msg}\n")
else:
stderr.write(f"┃ [{level.upper()}] {msg}\n")
except json.JSONDecodeError:
stderr.write(line)
drain_thread = Thread(target=drain_stderr, daemon=True)
drain_thread.start()
def cleanup():
if caddy_process:
caddy_process.terminate()