From 4f8a5288ff8a7fd439cf032affee463012e15a1e Mon Sep 17 00:00:00 2001 From: Folzo Date: Sat, 1 Aug 2026 08:20:35 +0200 Subject: [PATCH] jean-router: ajout Wake-on-LAN - Detection si Jean/Ollama inaccessible - Envoi magic packet WoL vers TARGET_MAC (fallback PC_GAMING_MAC) - Attente WAKE_TIMEOUT secondes que la machine demarre - Integration dans get_presets, get_ollama_models, chat_completions - Verrous pour eviter les WoL simultanes - Active via ENABLE_WOL=true --- jean-router/app.py | 94 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/jean-router/app.py b/jean-router/app.py index c13703d..c12ff2d 100644 --- a/jean-router/app.py +++ b/jean-router/app.py @@ -9,6 +9,7 @@ import os import time import logging import threading +import socket from typing import Optional import requests @@ -30,6 +31,12 @@ SWITCH_TIMEOUT = int(os.getenv("SWITCH_TIMEOUT", "90")) CHECK_INTERVAL = int(os.getenv("CHECK_INTERVAL", "2")) JEAN_STOP_TIMEOUT = int(os.getenv("JEAN_STOP_TIMEOUT", "30")) OLLAMA_UNLOAD_TIMEOUT = int(os.getenv("OLLAMA_UNLOAD_TIMEOUT", "15")) +ENABLE_WOL = os.getenv("ENABLE_WOL", "false").lower() == "true" +TARGET_MAC = os.getenv("TARGET_MAC", os.getenv("PC_GAMING_MAC", "")) +BROADCAST_IP = os.getenv("BROADCAST_IP", "192.168.1.255") +WAKE_TIMEOUT = int(os.getenv("WAKE_TIMEOUT", "120")) +wol_in_progress = threading.Lock() +jean_alive_cache = {"alive": False, "timestamp": 0} logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") log = logging.getLogger(__name__) @@ -65,10 +72,83 @@ def get_presets(force_refresh=False): return presets_cache["data"] except Exception as e: log.warning(f"[Presets] Erreur récupération: {e}") + if ENABLE_WOL and (not presets_cache["data"] or force_refresh): + wait_for_jean(trigger_wol=True) + try: + r = requests.get(f"http://{JEAN_WEB_HOST}:{JEAN_WEB_PORT}/api/presets", timeout=5) + if r.ok: + presets_cache["data"] = r.json() + presets_cache["timestamp"] = now + return presets_cache["data"] + except Exception: + pass return presets_cache["data"] +def send_wol(): + """Envoie un magic packet WoL vers TARGET_MAC""" + if not TARGET_MAC: + log.warning("[WoL] TARGET_MAC non defini, WoL desactive") + return + + mac = TARGET_MAC.replace(":", "").replace("-", "") + if len(mac) != 12: + log.error(f"[WoL] MAC invalide: {TARGET_MAC}") + return + + data = bytes.fromhex("FF" * 6 + mac * 16) + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + for i in range(3): + s.sendto(data, (BROADCAST_IP, 9)) + log.info(f"[WoL] Magic packet #{i+1} envoye vers {TARGET_MAC}") + + +def jean_alive() -> bool: + """Verifie si Jean est accessible""" + now = time.time() + if now - jean_alive_cache["timestamp"] < 5: + return jean_alive_cache["alive"] + + try: + url = f"http://{JEAN_WEB_HOST}:{JEAN_WEB_PORT}/api/presets" + r = requests.get(url, timeout=3) + alive = r.ok + except Exception: + alive = False + + jean_alive_cache["alive"] = alive + jean_alive_cache["timestamp"] = now + return alive + + +def wait_for_jean(trigger_wol=True): + """Attend que Jean soit accessible. Si ENABLE_WOL et trigger_wol, envoie WoL d'abord.""" + if jean_alive(): + return + + if not ENABLE_WOL or not trigger_wol: + return + + with wol_in_progress: + if jean_alive(): + return + + log.info("[WoL] Jean inaccessible, envoi WoL...") + send_wol() + + deadline = time.time() + WAKE_TIMEOUT + while time.time() < deadline: + if jean_alive(): + jean_alive_cache["timestamp"] = 0 + log.info(f"[WoL] Jean UP apres {int(time.time() - deadline + WAKE_TIMEOUT)}s") + return + time.sleep(CHECK_INTERVAL) + + log.error(f"[WoL] Jean ne repond pas apres {WAKE_TIMEOUT}s") + + def get_ollama_models(force_refresh=False): """Récupère la liste des modèles depuis Ollama""" if not ENABLE_OLLAMA: @@ -88,6 +168,17 @@ def get_ollama_models(force_refresh=False): return data except Exception as e: log.warning(f"[Ollama] Erreur récupération: {e}") + if ENABLE_WOL and (not ollama_cache["data"] or force_refresh): + wait_for_jean(trigger_wol=True) + try: + r = requests.get(f"http://{OLLAMA_PROXY_HOST}:{OLLAMA_PROXY_PORT}/api/tags", timeout=5) + if r.ok: + data = r.json().get("models", []) + ollama_cache["data"] = data + ollama_cache["timestamp"] = now + return data + except Exception: + pass return ollama_cache["data"] @@ -667,6 +758,8 @@ def chat_completions(): if model_id in jean_preset_ids or model_id in jean_names: internal_id = jean_name_to_id.get(model_id, model_id) + wait_for_jean(trigger_wol=True) + if not ensure_vram_for_backend("jean"): return jsonify({"error": "Failed to prepare VRAM for Jean"}), 503 @@ -695,6 +788,7 @@ def chat_completions(): upstream_url = f"http://{JEAN_PROXY_HOST}:{JEAN_PROXY_PORT}/v1/chat/completions" log.info(f"[Router] → Jean ({model_id})") elif ENABLE_OLLAMA: + wait_for_jean(trigger_wol=True) ollama_models = get_ollama_models() ollama_ids = [m.get("name", "").replace(":latest", "") for m in ollama_models]