From e7beee24622648b3e42f9982671da6cf7e8901f4 Mon Sep 17 00:00:00 2001 From: frarol96 Date: Sun, 24 Aug 2025 16:34:47 +0000 Subject: [PATCH] Update wrapper/wrapper.py --- wrapper/wrapper.py | 156 ++++++++++++--------------------------------- 1 file changed, 41 insertions(+), 115 deletions(-) diff --git a/wrapper/wrapper.py b/wrapper/wrapper.py index 45e4830..8f39d95 100644 --- a/wrapper/wrapper.py +++ b/wrapper/wrapper.py @@ -1,18 +1,12 @@ -import os, sys, time, shutil, subprocess, signal, json, pathlib, re, datetime +import os, sys, time, shutil, subprocess, json, pathlib, re, datetime from typing import Tuple -# ---------- Config (env) ---------- -# Provide sane defaults so a plain run works without envs. REPO_URL = os.getenv("REPO_URL", "https://git.rolfsvaag.no/frarol96/shaiwatcher.git").strip() REPO_BRANCH = os.getenv("REPO_BRANCH", "main").strip() -REPO_TOKEN = os.getenv("REPO_AHTOKEN", os.getenv("REPO_TOKEN", "")).strip() # optional +REPO_TOKEN = os.getenv("REPO_AHTOKEN", os.getenv("REPO_TOKEN", "")).strip() -# prefer CHECK_TIME_UTC; fall back to old RECHECK_UTC (kept for compatibility with your wrapper envs only) -CHECK_TIME_UTC = os.getenv("CHECK_TIME_UTC", os.getenv("RECHECK_UTC", "03:00")).strip() - -# 1 = ignore test-only bumps (e.g. v1.2.3.4-T3 -> v1.2.3.4-T4) +CHECK_TIME_UTC = os.getenv("CHECK_TIME_UTC", "03:00").strip() IGNORE_TEST_LEVEL = os.getenv("IGNORE_TEST_LEVEL", "1").strip() == "1" - PIP_INSTALL = os.getenv("PIP_INSTALL_REQUIREMENTS", "1").strip() == "1" CACHE_DIR = pathlib.Path("/cache/app") @@ -23,63 +17,48 @@ DATA_DIR = pathlib.Path("/data") RUN_TIMEOUT = int(os.getenv("WRAPPER_STOP_TIMEOUT", "25")) ROLLBACK_MAX_FAILS = 3 -# ---------- Helpers ---------- -def log(msg: str): - print(f"[wrapper] {msg}", flush=True) - -def run(*cmd, cwd=None, check=True) -> subprocess.CompletedProcess: +def log(m): print(f"[wrapper] {m}", flush=True) +def run(*cmd, cwd=None, check=True): log(f"$ {' '.join(cmd)}") return subprocess.run(cmd, cwd=cwd, check=check, text=True, capture_output=True) def ensure_git(): - try: - run("git","--version") + try: run("git","--version") except subprocess.CalledProcessError as e: - log(f"git missing? {e.stderr}") - sys.exit(1) - -def utc_now() -> datetime.datetime: - return datetime.datetime.utcnow() + log(f"git missing? {e.stderr}"); sys.exit(1) +def utc_now(): return datetime.datetime.utcnow() def next_utc(hhmm: str) -> float: hh, mm = map(int, hhmm.split(":")) now = utc_now() tgt = now.replace(hour=hh, minute=mm, second=0, microsecond=0) - if tgt <= now: - tgt = tgt + datetime.timedelta(days=1) + if tgt <= now: tgt += datetime.timedelta(days=1) return (tgt - now).total_seconds() -_VERSION_RE = re.compile(r'^\s*VERSION\s*=\s*[\'"]([^\'"]+)[\'"]', re.M) +_VER_RE = re.compile(r'^\s*VERSION\s*=\s*[\'"]([^\'"]+)[\'"]', re.M) def extract_version_from(path: pathlib.Path) -> str: try: txt = path.read_text(encoding="utf-8", errors="ignore") - m = _VERSION_RE.search(txt) + m = _VER_RE.search(txt) return m.group(1).strip() if m else "v0.0.0.0" except Exception: return "v0.0.0.0" -def parse_version(ver: str) -> Tuple[int,int,int,int,bool]: - # Format: vMajor.Minor.Enh.Patch[-T...] - # Example: v1.2.3.4-T7 +def parse_version(ver: str): test = "-T" in ver core = ver.split("-T")[0].lstrip("v") parts = [int(p or 0) for p in core.split(".")+["0","0","0","0"]][:4] - return parts[0], parts[1], parts[2], parts[3], test + return (*parts, test) -def should_update(old: str, new: str) -> bool: - """ - Update if the numeric tuple increases. - Ignore updates that change *only* the test suffix (e.g., v1.2.3.4-T1 -> v1.2.3.4-T2). - """ +def should_update(old, new): oM,oE,oN,oP,ot = parse_version(old) nM,nE,nN,nP,nt = parse_version(new) if (oM,oE,oN,oP) != (nM,nE,nN,nP): return True - # numeric parts same → only test suffix changed return not IGNORE_TEST_LEVEL and (ot != nt) def clone_or_fetch(target: pathlib.Path): - if target.exists() and (target / ".git").exists(): + if target.exists() and (target/".git").exists(): try: run("git","fetch","--all","-p", cwd=target) run("git","reset","--hard", f"origin/{REPO_BRANCH}", cwd=target) @@ -89,30 +68,20 @@ def clone_or_fetch(target: pathlib.Path): shutil.rmtree(target, ignore_errors=True) url = REPO_URL - # optional token (only if provided) if REPO_TOKEN and REPO_URL.startswith("https://"): url = REPO_URL.replace("https://", f"https://{REPO_TOKEN}@") - run("git","clone","--branch",REPO_BRANCH,"--depth","1", url, str(target)) + run("git","clone","--branch", REPO_BRANCH, "--depth","1", url, str(target)) def prime_tmp_then_decide(): - # Pull to TMP, compare versions (bot.py VERSION), decide if we swap CACHE TMP_DIR.mkdir(parents=True, exist_ok=True) shutil.rmtree(TMP_DIR, ignore_errors=True) clone_or_fetch(TMP_DIR) new_ver = extract_version_from(TMP_DIR / "bot.py") old_ver = extract_version_from(CACHE_DIR / "bot.py") if (CACHE_DIR / "bot.py").exists() else "v0.0.0.0" - log(f"cached version: {old_ver} / remote version: {new_ver}") - - if not (CACHE_DIR / "bot.py").exists(): - # First time - do_swap = True - reason = f"first fetch -> {new_ver}" - else: - do_swap = should_update(old_ver, new_ver) - reason = f"update allowed: {do_swap} (old={old_ver}, new={new_ver})" - - return do_swap, old_ver, new_ver, reason + do_swap = not (CACHE_DIR / "bot.py").exists() or should_update(old_ver, new_ver) + why = "first fetch" if not (CACHE_DIR / "bot.py").exists() else f"update allowed: {do_swap} (old={old_ver}, new={new_ver})" + return do_swap, old_ver, new_ver, why def swap_cache_to_prev(): PREV_DIR.mkdir(parents=True, exist_ok=True) @@ -127,106 +96,69 @@ def copy_tmp_to_cache(): def pip_install(cwd: pathlib.Path): req = cwd / "requirements.txt" if PIP_INSTALL and req.exists(): - try: - run(sys.executable, "-m", "pip", "install", "-r", str(req)) + try: run(sys.executable, "-m", "pip", "install", "-r", str(req)) except subprocess.CalledProcessError as e: - log("pip install failed (will continue anyway)") - log(e.stdout + "\n" + e.stderr) + log("pip install failed (continuing)"); log(e.stdout + "\n" + e.stderr) def set_boot_env(status: str, old_ver: str, new_ver: str, commit: str = "", subject: str = ""): - # Env passed to the bot; if you later choose to use them, they're non-SHAI now. os.environ["BOOT_STATUS"] = status os.environ["BOOT_OLDVER"] = old_ver os.environ["BOOT_NEWVER"] = new_ver os.environ["BUILD_COMMIT"] = commit os.environ["BUILD_SUBJECT"] = subject -def get_head_info(cwd: pathlib.Path) -> Tuple[str,str]: +def get_head_info(cwd: pathlib.Path): try: - c1 = run("git","rev-parse","--short","HEAD", cwd=cwd, check=True) - sha = c1.stdout.strip() - c2 = run("git","log","-1","--pretty=%s", cwd=cwd, check=True) - subj = c2.stdout.strip() + sha = run("git","rev-parse","--short","HEAD", cwd=cwd).stdout.strip() + subj = run("git","log","-1","--pretty=%s", cwd=cwd).stdout.strip() return (sha, subj) except Exception: - return ("", "") + return ("","") def start_bot(cwd: pathlib.Path) -> subprocess.Popen: env = os.environ.copy() - # Make sure data file env is present under the new name (no SHAI_). - env.setdefault("DATA_FILE", "/data/data.json") - # Run from the cached code directory + env.setdefault("DATA_FILE", "/data/data.json") # new name return subprocess.Popen([sys.executable, "-u", "bot.py"], cwd=cwd, env=env) -def graceful_restart(proc: subprocess.Popen) -> bool: +def graceful_stop(proc: subprocess.Popen) -> bool: try: proc.terminate() try: - proc.wait(timeout=RUN_TIMEOUT) - return True + proc.wait(timeout=RUN_TIMEOUT); return True except subprocess.TimeoutExpired: - proc.kill() - proc.wait(timeout=10) - return True + proc.kill(); proc.wait(timeout=10); return True except Exception: return False def run_loop(): ensure_git() + updated, old_ver, new_ver, why = prime_tmp_then_decide() + if updated: log(f"updating cache: {why}"); swap_cache_to_prev(); copy_tmp_to_cache() + else: log(f"no update: {why}") - # initial fetch/decide - updated, old_ver, new_ver, reason = prime_tmp_then_decide() - if updated: - log(f"updating cache: {reason}") - swap_cache_to_prev() - copy_tmp_to_cache() - else: - log(f"no update: {reason}") - - # pip install if needed (requirements.txt inside repo) pip_install(CACHE_DIR) - - # boot status env sha, subj = get_head_info(CACHE_DIR) - if updated: - set_boot_env( - f"Successfully fetched, cached, and booted new version", - old_ver, new_ver, sha, subj - ) - else: - msg = "Successfully booted from cached version" - if sha or subj: - msg += " (repo reachable)" - set_boot_env(msg, old_ver, new_ver, sha, subj) - - # start bot + set_boot_env( + "Successfully fetched, cached, and booted new version" if updated else "Successfully booted from cached version", + old_ver, new_ver, sha, subj + ) proc = start_bot(CACHE_DIR) log(f"bot started pid={proc.pid}") consecutive_failures = 0 - while True: - # sleep until next check (03:00 UTC by default) delay = next_utc(CHECK_TIME_UTC) - # FIX: use CHECK_TIME_UTC here (your original printed RECHECK_UTC) log(f"sleeping {int(delay)}s until {CHECK_TIME_UTC} UTC for update check") time.sleep(delay) - # check for update try: upd, cur_ver, remote_ver, why = prime_tmp_then_decide() log(f"nightly check: {why}") if not upd: - # no update -> continue loop continue - # graceful restart into new version log("updating to new version at nightly window") - ok = graceful_restart(proc) - if not ok: - log("warning: bot did not stop cleanly") - - # swap and boot + graceful_stop(proc) swap_cache_to_prev() copy_tmp_to_cache() pip_install(CACHE_DIR) @@ -241,8 +173,7 @@ def run_loop(): consecutive_failures += 1 if consecutive_failures < ROLLBACK_MAX_FAILS and PREV_DIR.exists() and (PREV_DIR / "bot.py").exists(): log("attempting rollback to previous cached version") - if proc.poll() is None: - graceful_restart(proc) + if proc.poll() is None: graceful_stop(proc) shutil.rmtree(CACHE_DIR, ignore_errors=True) shutil.copytree(PREV_DIR, CACHE_DIR, dirs_exist_ok=False) pip_install(CACHE_DIR) @@ -250,15 +181,10 @@ def run_loop(): proc = start_bot(CACHE_DIR) elif consecutive_failures >= ROLLBACK_MAX_FAILS: log("critical: failed 3 times to update/restart; entering freeze mode") - # Optional: DM owner could be done in a tiny fallback bot here if OWNER_ID provided. - # For now, just idle to allow SSH/exec into container. try: - if proc.poll() is None: - graceful_restart(proc) - except Exception: - pass - while True: - time.sleep(3600) + if proc.poll() is None: graceful_stop(proc) + except Exception: pass + while True: time.sleep(3600) if __name__ == "__main__": run_loop()