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 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,38 +240,79 @@ 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
ready = False
while True:
exit_code = caddy_process.poll() exit_code = caddy_process.poll()
if exit_code is not None: if exit_code is not None:
# Read and display stderr output, filtering and formatting JSON log lines # Process exited - read remaining stderr and report failure
caddy_stderr = caddy_process.stderr.read().decode() if caddy_process.stderr else "" remaining = caddy_process.stderr.read().decode() if caddy_process.stderr else ""
stderr.write(f"┃ ⚠️ Caddy startup failed\n") stderr.write(f"┃ ⚠️ Caddy startup failed (exit code {exit_code})\n")
if caddy_stderr: if remaining:
for line in caddy_stderr.splitlines(): for line in remaining.splitlines():
if not line: if 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)
stderr.write(f"{line}\n") stderr.write(f"{line}\n")
return None 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(): def cleanup():
if caddy_process: if caddy_process:
caddy_process.terminate() caddy_process.terminate()