Resilience : ecran pause, reconnexion WS auto, watchdog systemd, WiFi monitor
- WebSocket : reconnexion avec backoff exponentiel au lieu de reload page - Ecran pause avec tasse de cafe quand le backend est injoignable - Endpoint /api/health pour monitoring externe - Watchdog systemd (sd_notify READY=1 + WATCHDOG=1 toutes les 10s) - Service systemd durci (Type=notify, WatchdogSec=30, KillMode, limites) - WiFi watchdog : script + timer systemd, verifie la gateway toutes les 60s - Destinations : fallback url_tunnel (WireGuard) en priorite Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,8 @@ def distribuer_photo(chemin_photo: Path, imprimee: bool = False):
|
||||
if dest.get("ftp", False):
|
||||
envoyer_ftp(chemin_photo, dest, sous_dossier)
|
||||
|
||||
# Incrementer le compteur
|
||||
# Incrementer le compteur (photos imprimees seulement)
|
||||
if imprimee:
|
||||
compteur = config.get("compteur", {})
|
||||
if compteur.get("actif", False):
|
||||
compteur["photos_prises"] = compteur.get("photos_prises", 0) + 1
|
||||
@@ -98,47 +99,43 @@ def envoyer_ftp(chemin_photo: Path, config_dest: dict, sous_dossier: str | None
|
||||
return False
|
||||
|
||||
|
||||
def envoyer_booth(chemin_photo: Path, config_booth: dict):
|
||||
"""Envoie une photo vers la galerie live booth."""
|
||||
url = config_booth.get("url", "").rstrip("/")
|
||||
api_key = config_booth.get("api_key", "")
|
||||
event_id = config_booth.get("event_id", "default")
|
||||
|
||||
if not url:
|
||||
log.warning("Booth non configure (URL manquante)")
|
||||
return False
|
||||
|
||||
try:
|
||||
import mimetypes
|
||||
def _upload_booth(url: str, api_key: str, event_id: str, chemin_photo: Path) -> bool:
|
||||
boundary = "----BoothUpload"
|
||||
filename = chemin_photo.name
|
||||
|
||||
with open(chemin_photo, "rb") as f:
|
||||
file_data = f.read()
|
||||
|
||||
body = (
|
||||
f"--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="photo"; filename="{filename}"\r\n'
|
||||
f"Content-Type: image/jpeg\r\n\r\n"
|
||||
).encode() + file_data + f"\r\n--{boundary}--\r\n".encode()
|
||||
|
||||
req = Request(
|
||||
f"{url}/api/{event_id}/upload",
|
||||
data=body,
|
||||
method="POST",
|
||||
)
|
||||
req = Request(f"{url}/admin/gallery/{event_id}/upload", data=body, method="POST")
|
||||
req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
|
||||
req.add_header("X-Api-Key", api_key)
|
||||
|
||||
with urlopen(req, timeout=10) as resp:
|
||||
if resp.status == 200:
|
||||
log.info(f"Photo envoyee au booth : {filename}")
|
||||
return True
|
||||
else:
|
||||
log.error(f"Booth erreur HTTP {resp.status}")
|
||||
return resp.status == 200
|
||||
|
||||
|
||||
def envoyer_booth(chemin_photo: Path, config_booth: dict):
|
||||
"""Envoie une photo vers la galerie live booth."""
|
||||
url = config_booth.get("url", "").rstrip("/")
|
||||
url_tunnel = config_booth.get("url_tunnel", "").rstrip("/")
|
||||
api_key = config_booth.get("api_key", "")
|
||||
config = charger_config()
|
||||
event_id = config.get("evenement", {}).get("event_id") or config_booth.get("event_id", "default")
|
||||
|
||||
if not url and not url_tunnel:
|
||||
log.warning("Booth non configure (URL manquante)")
|
||||
return False
|
||||
|
||||
for tentative_url in [u for u in (url_tunnel, url) if u]:
|
||||
try:
|
||||
if _upload_booth(tentative_url, api_key, event_id, chemin_photo):
|
||||
log.info(f"Photo envoyee au booth via {tentative_url} : {chemin_photo.name}")
|
||||
return True
|
||||
log.error(f"Booth erreur HTTP via {tentative_url}")
|
||||
except (URLError, OSError) as e:
|
||||
log.error(f"Erreur envoi booth : {e}")
|
||||
log.warning(f"Echec envoi booth via {tentative_url} : {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -147,21 +144,19 @@ def recuperer_booth_password() -> dict:
|
||||
config = charger_config()
|
||||
booth = config.get("booth", {})
|
||||
url = booth.get("url", "").rstrip("/")
|
||||
url_tunnel = booth.get("url_tunnel", "").rstrip("/")
|
||||
api_key = booth.get("api_key", "")
|
||||
event_id = booth.get("event_id", "default")
|
||||
|
||||
if not url:
|
||||
return {"password": None}
|
||||
event_id = config.get("evenement", {}).get("event_id") or booth.get("event_id", "default")
|
||||
|
||||
for tentative_url in [u for u in (url_tunnel, url) if u]:
|
||||
try:
|
||||
req = Request(f"{url}/api/{event_id}/info")
|
||||
req = Request(f"{tentative_url}/admin/gallery/{event_id}/info")
|
||||
req.add_header("X-Api-Key", api_key)
|
||||
with urlopen(req, timeout=5) as resp:
|
||||
import json
|
||||
data = json.loads(resp.read())
|
||||
return data
|
||||
return json.loads(resp.read())
|
||||
except (URLError, OSError) as e:
|
||||
log.error(f"Erreur recuperation booth info : {e}")
|
||||
log.warning(f"Echec recuperation booth info via {tentative_url} : {e}")
|
||||
return {"password": None}
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
@@ -273,6 +274,24 @@ def _appliquer_config_camera():
|
||||
camera.configurer_flash(flash_integre)
|
||||
|
||||
|
||||
async def _watchdog_systemd():
|
||||
"""Notifie systemd que le service est vivant + surveille la santé."""
|
||||
try:
|
||||
import socket
|
||||
addr = os.environ.get("NOTIFY_SOCKET")
|
||||
if not addr:
|
||||
return
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||
if addr[0] == "@":
|
||||
addr = "\0" + addr[1:]
|
||||
sock.sendto(b"READY=1", addr)
|
||||
while True:
|
||||
await asyncio.sleep(10)
|
||||
sock.sendto(b"WATCHDOG=1", addr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Demarrage et arret de l'application."""
|
||||
@@ -287,10 +306,12 @@ async def lifespan(app: FastAPI):
|
||||
task_dslr = asyncio.create_task(surveiller_dslr())
|
||||
task_push = asyncio.create_task(_pusher_preview())
|
||||
task_spool = asyncio.create_task(tache_spool_demarrage())
|
||||
task_watchdog = asyncio.create_task(_watchdog_systemd())
|
||||
yield
|
||||
task_dslr.cancel()
|
||||
task_push.cancel()
|
||||
task_spool.cancel()
|
||||
task_watchdog.cancel()
|
||||
log.info("Arret du photobooth")
|
||||
camera.deconnecter()
|
||||
|
||||
@@ -311,6 +332,18 @@ async def page_admin():
|
||||
return FileResponse(str(RACINE / "frontend" / "index.html"))
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
async def api_health():
|
||||
return {
|
||||
"status": "ok",
|
||||
"camera": camera.mode if camera.connectee else "disconnected",
|
||||
"clients": len(clients_ws),
|
||||
"uptime": int(time.time() - _start_time),
|
||||
}
|
||||
|
||||
_start_time = time.time()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def page_principale(request: Request):
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
@@ -373,6 +373,7 @@
|
||||
<button class="onglet" data-onglet="personnalisation">Personnalisation</button>
|
||||
<button class="onglet" data-onglet="evenement">Evenement</button>
|
||||
<button class="onglet" data-onglet="fonctions">Fonctions</button>
|
||||
<button class="onglet" data-onglet="eclairage" onclick="demarrerEclairageLive()">Eclairage</button>
|
||||
<button class="onglet" data-onglet="infos" onclick="chargerInfosSysteme()">Infos</button>
|
||||
</div>
|
||||
|
||||
@@ -691,6 +692,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Panneau Infos systeme -->
|
||||
<div class="admin-panneau" id="panneau-eclairage">
|
||||
<div style="display:flex;gap:1rem;align-items:flex-start;flex-wrap:wrap">
|
||||
<div style="flex:1;min-width:280px">
|
||||
<img id="eclairage-preview" style="width:100%;border-radius:8px;background:#111" alt="Preview">
|
||||
</div>
|
||||
<div style="flex:0 0 200px;text-align:center">
|
||||
<div id="eclairage-score-box" style="font-size:3rem;font-weight:800;padding:1.5rem;border-radius:16px;background:#222;margin-bottom:1rem">--</div>
|
||||
<div id="eclairage-action" style="font-size:1.2rem;font-weight:600;margin-bottom:.5rem">--</div>
|
||||
<div id="eclairage-visages" style="font-size:.9rem;color:#aaa">--</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-panneau" id="panneau-infos">
|
||||
<div class="infos-systeme-header">
|
||||
<h3>Informations systeme</h3>
|
||||
@@ -772,9 +786,26 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/websocket.js?v=3"></script>
|
||||
<script src="/js/app.js?v=6"></script>
|
||||
<script src="/js/camera.js?v=13"></script>
|
||||
<!-- Ecran de pause (deconnexion backend) -->
|
||||
<div id="ecran-pause" style="display:none;position:fixed;inset:0;z-index:9999;background:#0a0a0f;flex-direction:column;align-items:center;justify-content:center;text-align:center">
|
||||
<div style="font-size:6rem;animation:pause-bob 2s ease-in-out infinite">☕</div>
|
||||
<h1 style="color:#fff;font-size:2.2rem;margin:1rem 0 .5rem;font-weight:700">Petite pause...</h1>
|
||||
<p style="color:#888;font-size:1.2rem;max-width:400px;line-height:1.5">Le photobooth revient dans un instant.<br>Pas de panique, on s'occupe de tout !</p>
|
||||
<div style="margin-top:2rem;display:flex;gap:8px">
|
||||
<span class="pause-dot" style="width:12px;height:12px;border-radius:50%;background:#e91e63;animation:pause-dot .6s ease-in-out infinite"></span>
|
||||
<span class="pause-dot" style="width:12px;height:12px;border-radius:50%;background:#e91e63;animation:pause-dot .6s ease-in-out .2s infinite"></span>
|
||||
<span class="pause-dot" style="width:12px;height:12px;border-radius:50%;background:#e91e63;animation:pause-dot .6s ease-in-out .4s infinite"></span>
|
||||
</div>
|
||||
<p id="pause-status" style="color:#555;font-size:.85rem;margin-top:2rem">Reconnexion en cours...</p>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes pause-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-20px)} }
|
||||
@keyframes pause-dot { 0%,100%{opacity:.2;transform:scale(.8)} 50%{opacity:1;transform:scale(1.2)} }
|
||||
</style>
|
||||
|
||||
<script src="/js/websocket.js?v=16"></script>
|
||||
<script src="/js/app.js?v=15"></script>
|
||||
<script src="/js/camera.js?v=15"></script>
|
||||
<script src="/js/effects.js?v=4"></script>
|
||||
<script src="/js/gallery.js?v=3"></script>
|
||||
<script src="/js/share.js?v=7"></script>
|
||||
|
||||
@@ -118,9 +118,34 @@ function allerA(ecran) {
|
||||
chargerGalerie();
|
||||
} else if (ecran === 'admin') {
|
||||
chargerAdmin();
|
||||
demarrerTimeoutAdmin();
|
||||
} else {
|
||||
arreterTimeoutAdmin();
|
||||
}
|
||||
}
|
||||
|
||||
let _adminTimeoutId = null;
|
||||
const ADMIN_TIMEOUT_MS = 30000;
|
||||
|
||||
function demarrerTimeoutAdmin() {
|
||||
arreterTimeoutAdmin();
|
||||
_adminTimeoutId = setTimeout(() => { allerA('accueil'); }, ADMIN_TIMEOUT_MS);
|
||||
const ecranAdmin = document.getElementById('ecran-admin');
|
||||
ecranAdmin.removeEventListener('pointerdown', _resetAdminTimeout);
|
||||
ecranAdmin.addEventListener('pointerdown', _resetAdminTimeout);
|
||||
}
|
||||
|
||||
function _resetAdminTimeout() {
|
||||
if (_adminTimeoutId) {
|
||||
clearTimeout(_adminTimeoutId);
|
||||
_adminTimeoutId = setTimeout(() => { allerA('accueil'); }, ADMIN_TIMEOUT_MS);
|
||||
}
|
||||
}
|
||||
|
||||
function arreterTimeoutAdmin() {
|
||||
if (_adminTimeoutId) { clearTimeout(_adminTimeoutId); _adminTimeoutId = null; }
|
||||
}
|
||||
|
||||
function recommencer() {
|
||||
photosSession = [];
|
||||
photoFinale = null;
|
||||
|
||||
@@ -48,13 +48,19 @@ function lancerPreview() {
|
||||
if (previewActif) return;
|
||||
previewActif = true;
|
||||
afficherErreurPreview(false);
|
||||
// Flou le temps que la 1ere frame fraiche arrive (le miroir DSLR doit remonter
|
||||
// apres la capture precedente) : evite l'impression d'image figee sur l'ancienne photo.
|
||||
document.getElementById('img-preview')?.classList.add('preview-chargement');
|
||||
wsEnvoyer({ type: 'preview_start' });
|
||||
resetPreviewTimeout();
|
||||
}
|
||||
|
||||
function demarrerEclairageLive() {
|
||||
if (!previewActif) {
|
||||
previewActif = true;
|
||||
wsEnvoyer({ type: 'preview_start' });
|
||||
resetPreviewTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
function arreterPreview() {
|
||||
if (!previewActif) return;
|
||||
previewActif = false;
|
||||
@@ -81,6 +87,10 @@ wsOnMessage('preview', (msg) => {
|
||||
};
|
||||
img.src = msg.image;
|
||||
}
|
||||
const adminPrev = document.getElementById('eclairage-preview');
|
||||
if (adminPrev && document.getElementById('panneau-eclairage')?.classList.contains('actif')) {
|
||||
adminPrev.src = msg.image;
|
||||
}
|
||||
afficherErreurPreview(false);
|
||||
resetPreviewTimeout();
|
||||
});
|
||||
@@ -175,33 +185,32 @@ wsOnMessage('camera_ok', () => {
|
||||
|
||||
wsOnMessage('eclairage', (msg) => {
|
||||
if (captureEnCours) return;
|
||||
let el = document.getElementById('indicateur-eclairage');
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
el.id = 'indicateur-eclairage';
|
||||
el.style.cssText = 'position:fixed;top:12px;left:12px;padding:8px 16px;border-radius:12px;font-size:.9rem;font-weight:600;z-index:90;transition:all .3s;pointer-events:none;display:flex;align-items:center;gap:8px;';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
const score = msg.score >= 0 ? msg.score : '?';
|
||||
const faces = msg.visages || 0;
|
||||
// Admin panel eclairage
|
||||
const scoreBox = document.getElementById('eclairage-score-box');
|
||||
const actionEl = document.getElementById('eclairage-action');
|
||||
const visagesEl = document.getElementById('eclairage-visages');
|
||||
if (scoreBox) {
|
||||
const score = msg.score >= 0 ? msg.score : '--';
|
||||
scoreBox.textContent = score;
|
||||
if (msg.action === 'allumer') {
|
||||
el.style.background = 'rgba(239,68,68,.9)';
|
||||
el.style.color = '#fff';
|
||||
el.innerHTML = `☀️ ALLUMER <span style="opacity:.7;font-size:.8rem">${score}/100 · ${faces} visage${faces > 1 ? 's' : ''}</span>`;
|
||||
scoreBox.style.background = '#dc2626'; scoreBox.style.color = '#fff';
|
||||
actionEl.textContent = 'ALLUMER LA LUMIERE';
|
||||
actionEl.style.color = '#f87171';
|
||||
} else if (msg.action === 'attention') {
|
||||
el.style.background = 'rgba(245,158,11,.85)';
|
||||
el.style.color = '#fff';
|
||||
el.innerHTML = `⚠️ Faible <span style="opacity:.7;font-size:.8rem">${score}/100 · ${faces} visage${faces > 1 ? 's' : ''}</span>`;
|
||||
scoreBox.style.background = '#d97706'; scoreBox.style.color = '#fff';
|
||||
actionEl.textContent = 'Eclairage faible';
|
||||
actionEl.style.color = '#fbbf24';
|
||||
} else if (msg.action === 'ok') {
|
||||
el.style.background = 'rgba(34,197,94,.7)';
|
||||
el.style.color = '#fff';
|
||||
el.innerHTML = `✓ <span style="opacity:.7;font-size:.8rem">${score}/100 · ${faces} visage${faces > 1 ? 's' : ''}</span>`;
|
||||
} else if (msg.action === 'no_face') {
|
||||
el.style.background = 'rgba(100,100,120,.6)';
|
||||
el.style.color = '#ccc';
|
||||
el.innerHTML = `<span style="font-size:.8rem">Aucun visage</span>`;
|
||||
scoreBox.style.background = '#16a34a'; scoreBox.style.color = '#fff';
|
||||
actionEl.textContent = 'Eclairage OK';
|
||||
actionEl.style.color = '#4ade80';
|
||||
} else {
|
||||
scoreBox.style.background = '#222'; scoreBox.style.color = '#888';
|
||||
actionEl.textContent = 'Aucun visage';
|
||||
actionEl.style.color = '#888';
|
||||
}
|
||||
visagesEl.textContent = msg.visages > 0 ? msg.visages + ' visage' + (msg.visages > 1 ? 's' : '') + ' detecte' + (msg.visages > 1 ? 's' : '') : '';
|
||||
}
|
||||
el.style.display = '';
|
||||
});
|
||||
|
||||
// --- Capture ---
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
/* Communication WebSocket avec le backend */
|
||||
/* Communication WebSocket avec le backend — reconnexion résiliente */
|
||||
|
||||
let ws = null;
|
||||
let wsReconnectTimer = null;
|
||||
let _wsReconnectDelay = 1000;
|
||||
let _wsWasConnected = false;
|
||||
const wsCallbacks = {};
|
||||
|
||||
function wsConnecter() {
|
||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
ws = new WebSocket(`${proto}//${location.host}/ws`);
|
||||
try { ws = new WebSocket(`${proto}//${location.host}/ws`); } catch(e) { _wsScheduleReconnect(); return; }
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connecte');
|
||||
if (wsReconnectTimer) {
|
||||
clearInterval(wsReconnectTimer);
|
||||
wsReconnectTimer = null;
|
||||
_wsReconnectDelay = 1000;
|
||||
if (_wsWasConnected) {
|
||||
_masquerPause();
|
||||
// Recharger la config pour resynchroniser l'état
|
||||
if (typeof chargerConfig === 'function') chargerConfig();
|
||||
}
|
||||
_wsWasConnected = true;
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
const cbs = wsCallbacks[msg.type];
|
||||
if (cbs) cbs.forEach(cb => cb(msg));
|
||||
} catch(e) {}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket deconnecte, rechargement dans 5s...');
|
||||
setTimeout(() => { location.reload(); }, 5000);
|
||||
console.log('WebSocket deconnecte');
|
||||
ws = null;
|
||||
_afficherPause();
|
||||
_wsScheduleReconnect();
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
@@ -32,6 +40,26 @@ function wsConnecter() {
|
||||
};
|
||||
}
|
||||
|
||||
function _wsScheduleReconnect() {
|
||||
const delay = Math.min(_wsReconnectDelay, 10000);
|
||||
_wsReconnectDelay = Math.min(_wsReconnectDelay * 1.5, 10000);
|
||||
setTimeout(wsConnecter, delay);
|
||||
}
|
||||
|
||||
function _afficherPause() {
|
||||
const el = document.getElementById('ecran-pause');
|
||||
if (el) el.style.display = 'flex';
|
||||
}
|
||||
|
||||
function _masquerPause() {
|
||||
const el = document.getElementById('ecran-pause');
|
||||
if (el) { el.style.display = 'none'; }
|
||||
// Relancer le preview si on était sur l'accueil
|
||||
if (typeof lancerPreview === 'function' && document.getElementById('ecran-accueil')?.classList.contains('actif')) {
|
||||
lancerPreview();
|
||||
}
|
||||
}
|
||||
|
||||
function wsEnvoyer(msg) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(msg));
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
[Unit]
|
||||
Description=Photobooth
|
||||
Description=Photobooth backend
|
||||
After=network.target graphical.target
|
||||
Wants=graphical.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Type=notify
|
||||
User=jules
|
||||
WorkingDirectory=/home/jules/Documents/Projet/photobooth-app
|
||||
ExecStart=/home/jules/Documents/Projet/photobooth-app/scripts/start.sh
|
||||
WorkingDirectory=/home/jules/photobooth
|
||||
ExecStart=/home/jules/photobooth/scripts/start.sh
|
||||
ExecStopPost=/bin/bash -c 'pkill -9 -f "python.*backend.main" 2>/dev/null; sleep 1'
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
RestartSec=3
|
||||
WatchdogSec=30
|
||||
KillMode=control-group
|
||||
KillSignal=SIGTERM
|
||||
TimeoutStopSec=10
|
||||
StartLimitIntervalSec=120
|
||||
StartLimitBurst=10
|
||||
Environment=DISPLAY=:0
|
||||
Environment=XAUTHORITY=/home/jules/.Xauthority
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
||||
|
||||
6
scripts/wifi-watchdog.service
Normal file
6
scripts/wifi-watchdog.service
Normal file
@@ -0,0 +1,6 @@
|
||||
[Unit]
|
||||
Description=WiFi watchdog check
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/home/jules/photobooth/scripts/wifi-watchdog.sh
|
||||
28
scripts/wifi-watchdog.sh
Normal file
28
scripts/wifi-watchdog.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# Surveille la connexion WiFi et la rétablit si elle tombe.
|
||||
# Conçu pour Surface Pro (wlan0) — lancé par cron ou systemd timer.
|
||||
|
||||
IFACE="wlan0"
|
||||
GATEWAY=$(ip route show default dev "$IFACE" 2>/dev/null | awk '{print $3}' | head -1)
|
||||
LOG_TAG="wifi-watchdog"
|
||||
|
||||
if [ -z "$GATEWAY" ]; then
|
||||
logger -t "$LOG_TAG" "Pas de gateway sur $IFACE, tentative reconnexion..."
|
||||
nmcli device connect "$IFACE" 2>&1 | logger -t "$LOG_TAG"
|
||||
sleep 5
|
||||
GATEWAY=$(ip route show default dev "$IFACE" 2>/dev/null | awk '{print $3}' | head -1)
|
||||
fi
|
||||
|
||||
if [ -n "$GATEWAY" ]; then
|
||||
if ! ping -c 2 -W 3 -I "$IFACE" "$GATEWAY" > /dev/null 2>&1; then
|
||||
logger -t "$LOG_TAG" "Ping gateway $GATEWAY echoue, reconnexion WiFi..."
|
||||
nmcli device disconnect "$IFACE" 2>&1 | logger -t "$LOG_TAG"
|
||||
sleep 2
|
||||
nmcli device connect "$IFACE" 2>&1 | logger -t "$LOG_TAG"
|
||||
sleep 5
|
||||
if ! ping -c 2 -W 3 -I "$IFACE" "$GATEWAY" > /dev/null 2>&1; then
|
||||
logger -t "$LOG_TAG" "Toujours pas de connexion, restart NetworkManager..."
|
||||
systemctl restart NetworkManager
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
9
scripts/wifi-watchdog.timer
Normal file
9
scripts/wifi-watchdog.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=WiFi watchdog - check every minute
|
||||
|
||||
[Timer]
|
||||
OnBootSec=30
|
||||
OnUnitActiveSec=60
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Reference in New Issue
Block a user