⬅ חזרה לתיקייה
✏️ עריכת tripserver_admin_supervisor.py
C:\TripServer\tripserver_admin_supervisor.py
# -*- coding: utf-8 -*- """ TripServer Admin Supervisor Runs outside admin_server.py and owns starting/restarting/updating it. Install as a Windows Scheduled Task at user logon using install_tripserver_supervisor.cmd. """ import os import sys import time import json import shutil import socket import urllib.request import urllib.error import subprocess import py_compile from pathlib import Path BASE_DIR = os.environ.get("TRIPSERVER_BASE_DIR", r"C:\TripServer") ADMIN_PORT = int(os.environ.get("TRIPSERVER_ADMIN_PORT", "8000")) ADMIN_SCRIPT = os.environ.get("TRIPSERVER_ADMIN_SCRIPT", os.path.join(BASE_DIR, "admin_server.py")) SUPERVISOR_DIR = os.path.join(BASE_DIR, "supervisor") PENDING_ADMIN = os.path.join(SUPERVISOR_DIR, "pending_admin_server.py") REQUEST_FILE = os.path.join(SUPERVISOR_DIR, "restart_request.json") REQUEST_DONE_FILE = os.path.join(SUPERVISOR_DIR, "restart_request.done.json") REQUEST_FAILED_FILE = os.path.join(SUPERVISOR_DIR, "restart_request.failed.json") LOCK_FILE = os.path.join(SUPERVISOR_DIR, "tripserver_supervisor.lock") LOG_PATH = os.path.join(BASE_DIR, "tripserver_supervisor.log") BACKUP_DIR = os.path.join(BASE_DIR, "backups", "admin_server") CHECK_INTERVAL = float(os.environ.get("TRIPSERVER_SUPERVISOR_INTERVAL", "3")) START_GRACE_SECONDS = float(os.environ.get("TRIPSERVER_SUPERVISOR_START_GRACE", "8")) CREATE_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0) DETACHED_PROCESS = getattr(subprocess, "DETACHED_PROCESS", 0) CREATE_NEW_PROCESS_GROUP = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) def now(): return time.strftime("%Y-%m-%d %H:%M:%S") def ensure_dirs(): os.makedirs(BASE_DIR, exist_ok=True) os.makedirs(SUPERVISOR_DIR, exist_ok=True) os.makedirs(BACKUP_DIR, exist_ok=True) def log(msg): ensure_dirs() line = f"{now()} | supervisor | {msg}" try: with open(LOG_PATH, "a", encoding="utf-8", errors="replace") as f: f.write(line + "\n") except Exception: pass def get_python_console(): exe = sys.executable or "python.exe" if os.name == "nt": folder = os.path.dirname(exe) if os.path.basename(exe).lower() == "pythonw.exe" and folder: cand = os.path.join(folder, "python.exe") if os.path.exists(cand): return cand return exe def file_sha256(path): import hashlib h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): h.update(chunk) return h.hexdigest() def http_ping(timeout=1.5): try: with urllib.request.urlopen(f"http://127.0.0.1:{ADMIN_PORT}/ping.json", timeout=timeout) as r: data = r.read(200) return r.status == 200 and b"ok" in data except Exception: return False def port_open(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(0.5) try: return s.connect_ex(("127.0.0.1", ADMIN_PORT)) == 0 finally: try: s.close() except Exception: pass def port_pids(): if os.name != "nt": return [] try: cp = subprocess.run(["netstat", "-ano"], capture_output=True, text=True, errors="replace", timeout=8) pids = set() for line in (cp.stdout or "").splitlines(): if f":{ADMIN_PORT}" not in line: continue if "LISTENING" not in line.upper(): continue parts = line.split() if parts and parts[-1].isdigit(): pids.add(int(parts[-1])) return sorted(pids) except Exception as exc: log("port_pids failed: " + repr(exc)) return [] def kill_pids(pids): for pid in pids: if pid == os.getpid(): continue try: cp = subprocess.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True, text=True, errors="replace", timeout=10) log(f"taskkill pid={pid} rc={cp.returncode} out={(cp.stdout or '').strip()} err={(cp.stderr or '').strip()}") except Exception as exc: log(f"taskkill pid={pid} failed: {exc!r}") def start_admin(): if not os.path.exists(ADMIN_SCRIPT): log("admin script missing: " + ADMIN_SCRIPT) return False py = get_python_console() flags = 0 if os.name == "nt": flags = CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP try: proc = subprocess.Popen( [py, ADMIN_SCRIPT], cwd=os.path.dirname(ADMIN_SCRIPT) or BASE_DIR, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=flags, close_fds=False if os.name == "nt" else True, start_new_session=(os.name != "nt"), ) log(f"started admin pid={proc.pid} python={py} script={ADMIN_SCRIPT}") time.sleep(START_GRACE_SECONDS) log(f"post-start ping={http_ping()} port_open={port_open()} pids={port_pids()}") return True except Exception as exc: log("start_admin failed: " + repr(exc)) return False def stop_admin(): pids = port_pids() if pids: log("stopping admin pids=" + repr(pids)) kill_pids(pids) for _ in range(20): if not port_open(): return True time.sleep(0.5) log("port still open after stop; pids=" + repr(port_pids())) return not port_open() def compile_admin(path): py_compile.compile(path, doraise=True) def backup_active(): if not os.path.exists(ADMIN_SCRIPT): return None stamp = time.strftime("%Y%m%d_%H%M%S") target = os.path.join(BACKUP_DIR, f"admin_server_supervisor_backup_{stamp}.py") shutil.copy2(ADMIN_SCRIPT, target) return target def handle_update_request(): if not os.path.exists(REQUEST_FILE): return False ensure_dirs() try: with open(REQUEST_FILE, "r", encoding="utf-8", errors="replace") as f: req = json.load(f) except Exception as exc: log("bad request file: " + repr(exc)) try: shutil.move(REQUEST_FILE, REQUEST_FAILED_FILE) except Exception: pass return True log("update request: " + json.dumps(req, ensure_ascii=False)) pending = req.get("pending_admin_path") or PENDING_ADMIN try: if not os.path.exists(pending): raise RuntimeError("pending admin file missing: " + pending) compile_admin(pending) expected = req.get("expected_sha256") actual = file_sha256(pending) if expected and actual.lower() != str(expected).lower(): raise RuntimeError(f"pending sha mismatch expected={expected} actual={actual}") backup = backup_active() stop_admin() tmp_active = ADMIN_SCRIPT + ".supervisor_new" shutil.copy2(pending, tmp_active) os.replace(tmp_active, ADMIN_SCRIPT) log(f"admin replaced from pending | sha={actual} | backup={backup}") try: os.remove(pending) except Exception: pass start_admin() req["handled_at"] = now() req["result"] = "ok" req["backup"] = backup with open(REQUEST_DONE_FILE, "w", encoding="utf-8") as f: json.dump(req, f, ensure_ascii=False, indent=2) try: os.remove(REQUEST_FILE) except Exception: pass return True except Exception as exc: log("update request failed: " + repr(exc)) req["handled_at"] = now() req["result"] = "failed" req["error"] = repr(exc) try: with open(REQUEST_FAILED_FILE, "w", encoding="utf-8") as f: json.dump(req, f, ensure_ascii=False, indent=2) except Exception: pass # Keep request file so user can inspect/retry after fixing; avoid tight loop by renaming it. try: os.replace(REQUEST_FILE, REQUEST_FILE + ".failed") except Exception: pass return True def single_instance_ok(): ensure_dirs() if os.name != "nt": return True # Simple stale lock: if another supervisor is running, this is best-effort only. try: if os.path.exists(LOCK_FILE): age = time.time() - os.path.getmtime(LOCK_FILE) if age < 30: log("another supervisor lock is fresh; exiting") return False with open(LOCK_FILE, "w", encoding="utf-8") as f: f.write(str(os.getpid())) return True except Exception: return True def loop(): ensure_dirs() log("starting supervisor | python=" + sys.executable + " | admin=" + ADMIN_SCRIPT) if not single_instance_ok(): return last_ok = 0 while True: try: # update lock heartbeat try: with open(LOCK_FILE, "w", encoding="utf-8") as f: f.write(str(os.getpid()) + " " + now()) except Exception: pass if handle_update_request(): time.sleep(CHECK_INTERVAL) continue if http_ping(): last_ok = time.time() else: po = port_open() pids = port_pids() if po else [] if po: log("ping failed but port open; killing stuck admin pids=" + repr(pids)) kill_pids(pids) time.sleep(1.5) else: log("admin not listening; starting") start_admin() except Exception as exc: log("loop error: " + repr(exc)) time.sleep(CHECK_INTERVAL) if __name__ == "__main__": loop()
שמור קובץ