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
+69 -29
View File
@@ -28,7 +28,6 @@ import shutil
import signal import signal
import subprocess import subprocess
import sys import sys
import time
from pathlib import Path from pathlib import Path
from sys import stderr from sys import stderr
from threading import Thread from threading import Thread
@@ -241,37 +240,78 @@ def run_caddy(origins: list[str], vite_port: int) -> subprocess.Popen | None:
stderr.write(f"┃ ⚠️ Caddy couldn't start: {e}\n") stderr.write(f"┃ ⚠️ Caddy couldn't start: {e}\n")
return None return None
# Wait briefly to check if Caddy exits immediately (e.g., port binding failure) # Read stderr line by line until Caddy signals it's ready or exits
time.sleep(0.2) # Caddy outputs JSON logs; "serving initial configuration" means it's ready
exit_code = caddy_process.poll() ready = False
if exit_code is not None: while True:
# Read and display stderr output, filtering and formatting JSON log lines exit_code = caddy_process.poll()
caddy_stderr = caddy_process.stderr.read().decode() if caddy_process.stderr else "" if exit_code is not None:
stderr.write(f"┃ ⚠️ Caddy startup failed\n") # Process exited - read remaining stderr and report failure
if caddy_stderr: remaining = caddy_process.stderr.read().decode() if caddy_process.stderr else ""
for line in caddy_stderr.splitlines(): stderr.write(f"┃ ⚠️ Caddy startup failed (exit code {exit_code})\n")
if not line: if remaining:
continue for line in remaining.splitlines():
try: if line:
log = json.loads(line) stderr.write(f"{line}\n")
# Filter out info-level messages return None
if log.get("level") == "info":
continue # Read one line from stderr (blocks until data available)
# Filter out expected "admin endpoint disabled" warning line = caddy_process.stderr.readline().decode()
if log.get("logger") == "admin" and "admin endpoint disabled" in log.get("msg", ""): if not line:
continue continue
# Format: [level] logger: msg
level = log.get("level", "?").upper() # Try to parse as JSON log
logger = log.get("logger", "") 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", "") msg = log.get("msg", "")
logger = log.get("logger", "")
# Skip expected warnings
if logger == "admin" and "admin endpoint disabled" in msg:
continue
if logger: if logger:
stderr.write(f"┃ [{level}] {logger}: {msg}\n") stderr.write(f"┃ [{level.upper()}] {logger}: {msg}\n")
else: else:
stderr.write(f"┃ [{level}] {msg}\n") stderr.write(f"┃ [{level.upper()}] {msg}\n")
except json.JSONDecodeError: except json.JSONDecodeError:
# Not JSON, print as-is (e.g., "Error: ..." lines) stderr.write(line)
stderr.write(f"{line}\n")
return None drain_thread = Thread(target=drain_stderr, daemon=True)
drain_thread.start()
def cleanup(): def cleanup():
if caddy_process: if caddy_process: