Initial wrapper commit
This commit is contained in:
		
							parent
							
								
									79ea71af90
								
							
						
					
					
						commit
						87502af744
					
				
							
								
								
									
										86
									
								
								wrapper/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								wrapper/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,86 @@
 | 
			
		||||
version: "3.9"
 | 
			
		||||
services:
 | 
			
		||||
  shaiwatcher:
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: dockerfile
 | 
			
		||||
    container_name: shaiwatcher
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    environment:
 | 
			
		||||
      - DISCORD_TOKEN=${DISCORD_TOKEN}
 | 
			
		||||
      - REPO_URL=${REPO_URL}
 | 
			
		||||
      - REPO_BRANCH=${REPO_BRANCH}
 | 
			
		||||
      - SHAI_DATA=/data/data.json
 | 
			
		||||
      - CHECK_TIME_UTC=03:00
 | 
			
		||||
      - IGNORE_TEST_LEVEL=1
 | 
			
		||||
 | 
			
		||||
      # Secrets (choose strategy)
 | 
			
		||||
      - SHAI_MASTER_SECRET=${SHAI_MASTER_SECRET}
 | 
			
		||||
      - SHAI_BOT_SALT=${SHAI_BOT_SALT}
 | 
			
		||||
      # or:
 | 
			
		||||
      # - SHAI_REPO_SECRET=${SHAI_REPO_SECRET}
 | 
			
		||||
      # - SHAI_BOT_SECRET=${SHAI_BOT_SECRET}
 | 
			
		||||
 | 
			
		||||
      # Fallback DM
 | 
			
		||||
      - SHAI_HOME_GUILD_ID=${SHAI_HOME_GUILD_ID}
 | 
			
		||||
 | 
			
		||||
      # --- Paths ---
 | 
			
		||||
      - SHAI_DATA_FILE: /data/data.json
 | 
			
		||||
 | 
			
		||||
      # --- Reaction gating messages ---
 | 
			
		||||
      - SHAI_RULES_MESSAGE_ID: "1396831304460402738"
 | 
			
		||||
      - SHAI_ENGAGEMENT_MESSAGE_ID: "1397668657143742574"
 | 
			
		||||
      - SHAI_NICKNAME_MESSAGE_ID: "1403513532108247141"
 | 
			
		||||
 | 
			
		||||
      # --- Roles ---
 | 
			
		||||
      - SHAI_RULES_ROLE_ID: "1403146506596253817"
 | 
			
		||||
      - SHAI_ENGAGEMENT_ROLE_ID: "1403146604894224458"
 | 
			
		||||
      - SHAI_FULL_ACCESS_ROLE_ID: "1403146645121667082"
 | 
			
		||||
      - SHAI_ADMIN_ROLE_ID: "1402000098476425246"
 | 
			
		||||
      - SHAI_FIELD_MOD_ROLE_ID: "1402001335041261681"
 | 
			
		||||
      - SHAI_INTEL_MOD_ROLE_ID: "1402001000327417946"
 | 
			
		||||
      - SHAI_MODERATOR_ROLE_ID: "1396828779015573598"
 | 
			
		||||
 | 
			
		||||
      # --- Channels ---
 | 
			
		||||
      - SHAI_MOD_CHANNEL_ID: "1403139701522698240"
 | 
			
		||||
      - SHAI_MODLOG_CHANNEL_ID: "1403146993198436627"
 | 
			
		||||
      - SHAI_USERSLIST_CHANNEL_ID: "1403146908385542215"
 | 
			
		||||
      - SHAI_REPORT_CHANNEL_ID: "1403147077285843034"
 | 
			
		||||
      - SHAI_PIRATES_LIST_CHANNEL_ID: "1403147077285843034"
 | 
			
		||||
 | 
			
		||||
      # --- Auto-VC ---
 | 
			
		||||
      - SHAI_TRIGGER_CHANNEL_ID: "1403139044174594190"
 | 
			
		||||
      - SHAI_AUTO_VC_CATEGORY_ID: "1403138882958266428"
 | 
			
		||||
      - SHAI_VC_NAME_PREFIX: "DD Crew "              # trailing space intentional
 | 
			
		||||
      - SHAI_AUTO_VC_CLEANUP_DELAY: "30"
 | 
			
		||||
 | 
			
		||||
      # --- Threat weights ---
 | 
			
		||||
      - SHAI_THREAT_W_KILL: "0.30"
 | 
			
		||||
      - SHAI_THREAT_W_DESTRUCTION: "0.40"
 | 
			
		||||
      - SHAI_THREAT_W_GROUP: "0.20"
 | 
			
		||||
      - SHAI_THREAT_W_SKILL: "0.10"
 | 
			
		||||
      - SHAI_THREAT_GROUP_THRESHOLD: "3"
 | 
			
		||||
      - SHAI_THREAT_MIN_SAMPLES_FOR_STATS: "3"
 | 
			
		||||
 | 
			
		||||
      # --- Misc toggles ---
 | 
			
		||||
      - SHAI_RELEASE_VERSION: "false"
 | 
			
		||||
      - SHAI_NICK_NUDGE_LOOP_ENABLED: "false"
 | 
			
		||||
      - SHAI_HOME_GUILD_ID: "1396826999095427253"
 | 
			
		||||
      - SHAI_USER_CARDS_CRON_ENABLED: "true"
 | 
			
		||||
 | 
			
		||||
      # --- SpicePay defaults ---
 | 
			
		||||
      - SHAI_SPICEPAY_LSR_CUT_PERCENT: "10"
 | 
			
		||||
      - SHAI_SPICEPAY_BASE_WEIGHT: "25"
 | 
			
		||||
      - SHAI_SPICEPAY_CARRIER_BONUS: "12.5"
 | 
			
		||||
      - SHAI_SPICEPAY_CRAWLER_BONUS: "12.5"
 | 
			
		||||
 | 
			
		||||
      # --- Optional emojis (IDs) ---
 | 
			
		||||
      - SHAI_EMOJI_MELANGE_ID: "1401965356775510210"
 | 
			
		||||
      - SHAI_EMOJI_SAND_ID: "1401965308805255310"
 | 
			
		||||
      - SHAI_EMOJI_CARRIER_CRAWLER_ID: "1402285453037666386"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - shaiwatcher_data:/data
 | 
			
		||||
      - shaiwatcher_cache:/cache
 | 
			
		||||
volumes:
 | 
			
		||||
  shaiwatcher_data:
 | 
			
		||||
  shaiwatcher_cache:
 | 
			
		||||
							
								
								
									
										22
									
								
								wrapper/dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								wrapper/dockerfile
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
# wrapper/Dockerfile
 | 
			
		||||
FROM python:3.11-slim
 | 
			
		||||
 | 
			
		||||
ENV PYTHONUNBUFFERED=1 \
 | 
			
		||||
    PIP_DISABLE_PIP_VERSION_CHECK=1
 | 
			
		||||
 | 
			
		||||
# deps: git (fetch code), tzdata (UTC), tini (pid1), bash
 | 
			
		||||
RUN apt-get update && apt-get install -y --no-install-recommends \
 | 
			
		||||
    git tzdata bash tini ca-certificates && \
 | 
			
		||||
    rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# runtime dirs
 | 
			
		||||
WORKDIR /wrapper
 | 
			
		||||
RUN mkdir -p /cache/app /cache/tmp /cache/prev /data
 | 
			
		||||
 | 
			
		||||
# copy wrapper
 | 
			
		||||
COPY start.sh /wrapper/start.sh
 | 
			
		||||
COPY wrapper.py /wrapper/wrapper.py
 | 
			
		||||
 | 
			
		||||
# tini as entrypoint for clean sig handling
 | 
			
		||||
ENTRYPOINT ["/usr/bin/tini","--"]
 | 
			
		||||
CMD ["bash","/wrapper/start.sh"]
 | 
			
		||||
							
								
								
									
										255
									
								
								wrapper/wrapper.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								wrapper/wrapper.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,255 @@
 | 
			
		||||
import os, sys, time, shutil, subprocess, signal, json, pathlib, re, datetime
 | 
			
		||||
from typing import Tuple
 | 
			
		||||
 | 
			
		||||
# ---------- Config (env) ----------
 | 
			
		||||
REPO_URL           = os.getenv("REPO_URL", "").strip()  # e.g. https://git.rolfsvaag.no/frarol96/shaiwatcher
 | 
			
		||||
REPO_BRANCH        = os.getenv("REPO_BRANCH", "main").strip()
 | 
			
		||||
REPO_TOKEN         = os.getenv("REPO_TOKEN", "").strip()  # optional (for private), not used if empty
 | 
			
		||||
RECHECK_UTC        = os.getenv("RECHECK_UTC", "03:00").strip()  # HH:MM (UTC)
 | 
			
		||||
PIP_INSTALL        = os.getenv("PIP_INSTALL_REQUIREMENTS", "1").strip() == "1"
 | 
			
		||||
 | 
			
		||||
CACHE_DIR   = pathlib.Path("/cache/app")      # current code
 | 
			
		||||
TMP_DIR     = pathlib.Path("/cache/tmp")      # temp checkout
 | 
			
		||||
PREV_DIR    = pathlib.Path("/cache/prev")     # rollback
 | 
			
		||||
DATA_DIR    = pathlib.Path("/data")           # persistent data volume
 | 
			
		||||
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:
 | 
			
		||||
    log(f"$ {' '.join(cmd)}")
 | 
			
		||||
    return subprocess.run(cmd, cwd=cwd, check=check, text=True, capture_output=True)
 | 
			
		||||
 | 
			
		||||
def ensure_git():
 | 
			
		||||
    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()
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
    return (tgt - now).total_seconds()
 | 
			
		||||
 | 
			
		||||
_VERSION_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)
 | 
			
		||||
        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
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
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).
 | 
			
		||||
    """
 | 
			
		||||
    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 same -> only test part differs → do NOT update
 | 
			
		||||
    return False
 | 
			
		||||
 | 
			
		||||
def clone_or_fetch(target: pathlib.Path):
 | 
			
		||||
    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)
 | 
			
		||||
            return
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            log(f"fetch failed, recloning: {e}")
 | 
			
		||||
            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))
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
def swap_cache_to_prev():
 | 
			
		||||
    PREV_DIR.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
    shutil.rmtree(PREV_DIR, ignore_errors=True)
 | 
			
		||||
    if CACHE_DIR.exists():
 | 
			
		||||
        shutil.move(str(CACHE_DIR), str(PREV_DIR))
 | 
			
		||||
 | 
			
		||||
def copy_tmp_to_cache():
 | 
			
		||||
    shutil.rmtree(CACHE_DIR, ignore_errors=True)
 | 
			
		||||
    shutil.copytree(TMP_DIR, CACHE_DIR, dirs_exist_ok=False)
 | 
			
		||||
 | 
			
		||||
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))
 | 
			
		||||
        except subprocess.CalledProcessError as e:
 | 
			
		||||
            log("pip install failed (will continue anyway)")
 | 
			
		||||
            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; bot should read and post to modlog on_ready
 | 
			
		||||
    os.environ["SHAI_BOOT_STATUS"]  = status
 | 
			
		||||
    os.environ["SHAI_BOOT_OLDVER"]  = old_ver
 | 
			
		||||
    os.environ["SHAI_BOOT_NEWVER"]  = new_ver
 | 
			
		||||
    os.environ["SHAI_BUILD_COMMIT"] = commit
 | 
			
		||||
    os.environ["SHAI_BUILD_SUBJECT"]= subject
 | 
			
		||||
 | 
			
		||||
def get_head_info(cwd: pathlib.Path) -> Tuple[str,str]:
 | 
			
		||||
    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()
 | 
			
		||||
        return (sha, subj)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return ("", "")
 | 
			
		||||
 | 
			
		||||
def start_bot(cwd: pathlib.Path) -> subprocess.Popen:
 | 
			
		||||
    env = os.environ.copy()
 | 
			
		||||
    # Make sure data dir exists (the bot should use SHAI_DATA or SHAI_DATA_FILE or config)
 | 
			
		||||
    env.setdefault("SHAI_DATA", "/data/data.json")
 | 
			
		||||
    # Run from the cached code directory
 | 
			
		||||
    return subprocess.Popen([sys.executable, "-u", "bot.py"], cwd=cwd, env=env)
 | 
			
		||||
 | 
			
		||||
def graceful_restart(proc: subprocess.Popen) -> bool:
 | 
			
		||||
    try:
 | 
			
		||||
        proc.terminate()
 | 
			
		||||
        try:
 | 
			
		||||
            proc.wait(timeout=RUN_TIMEOUT)
 | 
			
		||||
            return True
 | 
			
		||||
        except subprocess.TimeoutExpired:
 | 
			
		||||
            proc.kill()
 | 
			
		||||
            proc.wait(timeout=10)
 | 
			
		||||
            return True
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
def run_loop():
 | 
			
		||||
    ensure_git()
 | 
			
		||||
 | 
			
		||||
    # 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
 | 
			
		||||
    proc = start_bot(CACHE_DIR)
 | 
			
		||||
    log(f"bot started pid={proc.pid}")
 | 
			
		||||
 | 
			
		||||
    consecutive_failures = 0
 | 
			
		||||
 | 
			
		||||
    while True:
 | 
			
		||||
        # sleep until next 03:00 UTC
 | 
			
		||||
        delay = next_utc(RECHECK_UTC)
 | 
			
		||||
        log(f"sleeping {int(delay)}s until {RECHECK_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
 | 
			
		||||
            swap_cache_to_prev()
 | 
			
		||||
            copy_tmp_to_cache()
 | 
			
		||||
            pip_install(CACHE_DIR)
 | 
			
		||||
            sha, subj = get_head_info(CACHE_DIR)
 | 
			
		||||
            set_boot_env("Successfully fetched, cached, and booted new version", cur_ver, remote_ver, sha, subj)
 | 
			
		||||
            proc = start_bot(CACHE_DIR)
 | 
			
		||||
            log(f"bot restarted on new version pid={proc.pid}")
 | 
			
		||||
            consecutive_failures = 0
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            log(f"nightly update failed: {e}")
 | 
			
		||||
            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)
 | 
			
		||||
                shutil.rmtree(CACHE_DIR, ignore_errors=True)
 | 
			
		||||
                shutil.copytree(PREV_DIR, CACHE_DIR, dirs_exist_ok=False)
 | 
			
		||||
                pip_install(CACHE_DIR)
 | 
			
		||||
                set_boot_env("Rolled back to last known working version", "-", extract_version_from(CACHE_DIR / "bot.py"))
 | 
			
		||||
                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 __name__ == "__main__":
 | 
			
		||||
    run_loop()
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user