mirror of
https://github.com/migatu/conjurer.git
synced 2026-07-14 13:34:40 +00:00
fix: make bot startup resilient (self-healing state, gated cogs, loud failures)
Container/native startup died silently on any missing file/service. Now: constants.py - self-healing runtime layout at import: - create missing dirs (log dir, LOGSTORE, GRAPHICS_PATH, MUSIC_FOLDER) - seed missing state files from the repo templates shipped next to constants.py (settings/system_gpt/pamiec/pamiec_muzyki/accident_log), falling back to safe empty JSON; existing files are NEVER overwritten bot.py: - log to stdout too, so 'docker logs' finally shows the crash reason - missing Discord token = loud sys.exit with mount/env instructions (was: silent return -> container crash-loop with empty logs) - every cog loads independently (one broken cog = skipped with traceback, bot continues) - musician/librarian cogs are health-gated: enabled only when the service answers HTTP; a watchdog re-checks every 5 min and enables them the moment the service comes alive (no restart needed); tree re-synced - on_ready reconnects no longer re-load extensions requirements_conan.txt + Dockerfile.bot: aiomcrcon (Python <=3.11 only) moved to best-effort extras so the 3.13 image builds clean and the conanjurer cog stays dormant without it. DOCKER_PROXMOX.md: startup model (core vs gated cogs) + crash-loop troubleshooting incl. the 'disappearing files' checklist (nothing in the stack deletes host files; bind mount = live state). Verified: fresh-volume seeding creates dirs+templates, existing files untouched, missing-token exits with FATAL message, health-gating logic (stub-based runpy tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -202,6 +202,65 @@ PORT_ADDRESS = int(os.getenv("CONJURER_DISCORD_PORT", "5000"))
|
||||
API_SHARED_KEY = os.getenv("CONJURER_API_KEY", "")
|
||||
|
||||
|
||||
# *=========================================== Self-healing runtime layout
|
||||
# A fresh host/volume must never kill the bot at import time. Missing
|
||||
# directories are created and missing state files are seeded - first from the
|
||||
# templates shipped alongside this file (repo checkout / docker image), then
|
||||
# from a safe empty structure. Existing files are never touched, so preserved
|
||||
# history always wins.
|
||||
|
||||
_TEMPLATE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def _ensure_dir(path: str) -> None:
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
except OSError as exc:
|
||||
logger.warning("Cannot create directory %s: %s", path, exc)
|
||||
|
||||
|
||||
def _seed_file(path: str, template_name: str, empty_content: str) -> None:
|
||||
"""Create *path* from the repo template (or *empty_content*) if missing."""
|
||||
if not path or os.path.exists(path):
|
||||
return
|
||||
_ensure_dir(os.path.dirname(path) or ".")
|
||||
template = os.path.join(_TEMPLATE_DIR, template_name)
|
||||
try:
|
||||
if os.path.exists(template) and os.path.abspath(template) != os.path.abspath(path):
|
||||
import shutil
|
||||
|
||||
shutil.copyfile(template, path)
|
||||
logger.warning("Seeded missing %s from repo template", path)
|
||||
else:
|
||||
with open(path, "w", encoding=ENCODING) as handle:
|
||||
handle.write(empty_content)
|
||||
logger.warning("Created missing %s as empty state", path)
|
||||
except OSError as exc:
|
||||
logger.warning("Cannot seed %s: %s", path, exc)
|
||||
|
||||
|
||||
def _ensure_runtime_layout() -> None:
|
||||
for directory in (
|
||||
os.path.dirname(LOGFILE) or ".",
|
||||
LOGSTORE,
|
||||
GRAPHICS_PATH,
|
||||
MUSIC_FOLDER,
|
||||
):
|
||||
_ensure_dir(directory)
|
||||
|
||||
# (target path, template shipped next to this file, empty fallback)
|
||||
_seed_file(SETTINGS_FILE, "settings.json", "{}")
|
||||
_seed_file(SYSTEM_GPT_SETTINGS, "system_gpt_settings.json", "{}")
|
||||
_seed_file(MEMORY_FIVE_SIARA, "pamiec.json", "[]")
|
||||
_seed_file(MEMORY_FIVE_MUZYKA, "pamiec_muzyki.json", "[]")
|
||||
_seed_file(ACCIDENT_LOG, "accident_log.json", "[]")
|
||||
|
||||
|
||||
_ensure_runtime_layout()
|
||||
|
||||
|
||||
# *=========================================== Defensive state loading
|
||||
def _load_json(path: str, fallback):
|
||||
"""Load JSON from *path*, falling back gracefully on missing/corrupt files."""
|
||||
|
||||
Reference in New Issue
Block a user