diff --git a/configs/releng/airootfs/usr/local/bin/fws-gameboot b/configs/releng/airootfs/usr/local/bin/fws-gameboot new file mode 100644 index 0000000..158ce59 --- /dev/null +++ b/configs/releng/airootfs/usr/local/bin/fws-gameboot @@ -0,0 +1,218 @@ +#!/bin/bash +# ============================================================ +# fws-gameboot — orchestrateur dual-boot « hibernate-swap » (FWS ⇄ Windows) +# +# But : jouer à un jeu à anticheat kernel (Valorant/Vanguard) qui REFUSE la +# virtualisation, sans quitter durablement FWS. On hiberne FWS (RAM → swap), on +# arme un boot UEFI one-shot (BootNext) vers un Windows 11 BARE-METAL, et on +# redémarre. Windows tourne sur le vrai matériel (AUCUN hyperviseur) → Vanguard +# content. Au retour de Windows, le noyau reprend la session FWS via resume=. +# +# Ce N'EST PAS une VM et il n'y a AUCUN hyperviseur : c'est un dual-boot +# orchestré. Le « seamless » vient de l'hibernation (session restaurée), pas +# d'une exécution simultanée (impossible sans hyperviseur = ban Vanguard). +# +# Filet de sûreté PERMANENT : BootOrder[0] = FWS (posé par fws-gameboot-bootfix +# à chaque démarrage). BootNext ne sert QUE pour les transitions one-shot ; tout +# crash/reboot non géré retombe sur FWS. +# +# ⚠ Exige une VALIDATION MATÉRIELLE réelle (hibernation + resume GPU NVIDIA, +# sémantique BootNext du firmware). Voir docs/hibernate-swap-dualboot.md §7. +# ============================================================ +set -u + +CONF=/etc/fws/gameboot.conf +# shellcheck source=/dev/null +[ -r "$CONF" ] && . "$CONF" +: "${FWS_WINDOWS_BOOTNUM:=}" +: "${FWS_HIBERNATE_FALLBACK:=reboot}" +: "${FWS_SWAP_MARGIN_MB:=2048}" + +log() { printf '\e[36m[gameboot]\e[0m %s\n' "$*" >&2; } +warn() { printf '\e[33m[gameboot] %s\e[0m\n' "$*" >&2; } +die() { printf '\e[31m[gameboot] ERREUR : %s\e[0m\n' "$*" >&2; exit 1; } + +# Notifie l'utilisateur graphique (best-effort ; on tourne en root via pkexec). +notify_user() { + command -v notify-send >/dev/null 2>&1 || return 0 + local u uid + while IFS=: read -r u _ uid _; do + [ "$uid" -ge 1000 ] && [ "$uid" -le 60000 ] || continue + [ -S "/run/user/$uid/bus" ] || continue + sudo -u "$u" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" \ + notify-send -a FWS -u critical "FWS Gaming" "$1" 2>/dev/null || true + done < /etc/passwd +} + +need_root() { [ "$(id -u)" -eq 0 ] || die "root requis — lance : pkexec fws-gameboot ${1:-}"; } + +# --- Entrée UEFI « Windows Boot Manager » (numéro Boot####, HEX MAJUSCULE) --- +find_windows_bootnum() { + local n + if [ -n "$FWS_WINDOWS_BOOTNUM" ]; then n="$FWS_WINDOWS_BOOTNUM"; else + n="$(efibootmgr 2>/dev/null | sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\*\? *Windows Boot Manager.*/\1/p' | head -1)" + fi + # Normaliser en MAJUSCULE : efibootmgr affiche « BootNext: 000A » en majuscule ; + # une valeur config en minuscule ferait échouer la relecture (m6). + printf '%s' "${n^^}" +} +# --- Notre propre entrée (FWS ou, à défaut, GRUB) --- +find_fws_bootnum() { + efibootmgr 2>/dev/null | sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\*\? *\(FWS\|GRUB\).*/\1/p' | head -1 +} + +# --- VRAM en usage (Mo), 0 si pas de NVIDIA — recopiée dans l'image S4 --- +vram_used_mb() { + command -v nvidia-smi >/dev/null 2>&1 || { echo 0; return; } + nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits 2>/dev/null \ + | awk '{s+=$1} END{print s+0}' +} + +# Renvoie 0 si tout est OK pour hiberner. Décrit chaque échec sur stderr. +preflight() { + local mem_kb swap_kb vram_mb need_mb have_mb + grep -q disk /sys/power/state 2>/dev/null || { warn "hibernation indisponible (/sys/power/state sans « disk »)"; return 1; } + grep -q 'resume=' /proc/cmdline || { warn "resume= absent de la cmdline noyau (hibernation non reprenable)"; return 1; } + swap_kb=$(awk '/^SwapTotal:/{print $2}' /proc/meminfo) + [ "${swap_kb:-0}" -gt 0 ] || { warn "aucun swap actif (rien où écrire l'image)"; return 1; } + mem_kb=$(awk '/^MemTotal:/{print $2}' /proc/meminfo) + vram_mb=$(vram_used_mb) + need_mb=$(( mem_kb/1024 + vram_mb + FWS_SWAP_MARGIN_MB )) + have_mb=$(( swap_kb/1024 )) + if [ "$have_mb" -lt "$need_mb" ]; then + warn "swap trop petit : ${have_mb} Mo < ${need_mb} Mo requis (RAM $((mem_kb/1024)) + VRAM ${vram_mb} + marge ${FWS_SWAP_MARGIN_MB})" + return 1 + fi + return 0 +} + +# Résout le point de montage de l'ESP (FAT). Vide si aucun. +find_esp() { + findmnt -rno TARGET /boot/efi 2>/dev/null \ + || findmnt -rno TARGET /efi 2>/dev/null \ + || findmnt -rno TARGET /boot 2>/dev/null +} + +cmd_doctor() { + local sb f w + sb=$(bootctl status 2>/dev/null | sed -n 's/^ *Secure Boot: *//p' | head -1) + f="$(find_fws_bootnum)"; w="$(find_windows_bootnum)" + echo "== fws-gameboot doctor ==" + echo "Entrée FWS : ${f:-?}" + echo "Entrée Windows : ${w:-INTROUVABLE}" + echo "RAM : $(( $(awk '/^MemTotal:/{print $2}' /proc/meminfo)/1024 )) Mo" + echo "Swap : $(( $(awk '/^SwapTotal:/{print $2}' /proc/meminfo)/1024 )) Mo" + echo "VRAM en usage : $(vram_used_mb) Mo" + echo "resume= cmdline : $(grep -o 'resume=[^ ]*' /proc/cmdline || echo 'ABSENT')" + echo "Secure Boot : ${sb:-inconnu}" + echo -n "Préflight : " + if preflight 2>/dev/null; then printf '\e[32mOK\e[0m\n'; else printf '\e[31mÉCHEC\e[0m\n'; preflight; fi + case "$sb" in + enabled) ;; + *) warn "Secure Boot inactif → Vanguard refusera Windows (VAN9003). Configure d'abord : sudo fws-secureboot-setup" ;; + esac +} + +cmd_to_windows() { + need_root "to-windows" + local win esp bootnext_set="" rc m + win="$(find_windows_bootnum)" + [ -n "$win" ] || die "entrée « Windows Boot Manager » introuvable (Windows installé + entrée UEFI présente ?)" + esp="$(find_esp)" + + # Filet : ne JAMAIS laisser BootNext armé si on ne bascule pas réellement + # (sinon départ surprise vers Windows au prochain reboot manuel). + cleanup() { [ -n "$bootnext_set" ] && { efibootmgr -N >/dev/null 2>&1 || true; log "BootNext annulé (nettoyage)"; }; } + trap cleanup EXIT + + # Démonter toute NTFS montée : jamais rw persistante à travers le cycle. + # `findmnt -lno` (liste, PAS -r/raw) → chemins NON échappés (espaces OK) ; on + # se limite aux vrais types NTFS (pas fuseblk générique). + while IFS= read -r m; do + [ -n "$m" ] && umount "$m" 2>/dev/null || true + done < <(findmnt -lno TARGET -t ntfs,ntfs3 2>/dev/null) + + if ! preflight; then + case "$FWS_HIBERNATE_FALLBACK" in + reboot) + warn "Préflight hibernation KO → bascule en REBOOT SIMPLE (session FWS NON préservée)." + notify_user "Hibernation indisponible — bascule vers Windows par reboot simple (session non sauvegardée)." + efibootmgr -n "$win" >/dev/null || die "écriture de BootNext échouée" + bootnext_set=1 # ARMÉ : si la relecture échoue, le trap nettoie (M2) + efibootmgr 2>/dev/null | grep -qi "BootNext: $win" || die "BootNext non confirmé par le firmware" + trap - EXIT # bascule assumée + log "Reboot simple vers Windows (Boot$win)…" + exec systemctl reboot + ;; + *) die "Préflight hibernation KO et fallback='abort' → on reste sur FWS." ;; + esac + fi + + # --- Bascule NOMINALE : BootNext=Windows puis hibernation --- + efibootmgr -n "$win" >/dev/null || die "écriture de BootNext échouée" + bootnext_set=1 + # RELECTURE obligatoire : certains firmwares ignorent/écrasent BootNext. + efibootmgr 2>/dev/null | grep -qi "BootNext: $win" \ + || die "BootNext non pris en compte par le firmware (entrée Boot$win) — carte non compatible ?" + log "BootNext armé vers Windows (Boot$win). Hibernation…" + notify_user "Lancement de Windows pour le jeu — FWS s'endort, ta session sera restaurée au retour." + + # M3 : l'ESP (FAT, potentiellement partagée avec Windows) ne doit pas rester + # montée rw à travers le cycle — un cache FAT périmé réécrit au resume + # corromprait les bootloaders (seul chemin réaliste vers un vrai brick). On + # la remonte en lecture seule le temps de l'hibernation, rw au retour. + [ -n "$esp" ] && { mount -o remount,ro "$esp" 2>/dev/null || true; } + + # NB : on n'enveloppe PAS « systemctl hibernate » dans un timeout. Un + # hibernate RÉUSSI ne « rend la main » qu'au RESUME (après la session + # Windows, potentiellement > 1 h) : un timeout tuerait les hibernations + # réussies. Une hibernation qui HANG (driver refusant S4) laisse la machine + # allumée et n'est PAS mitigeable en userspace (le process est gelé) ; c'est + # un risque à valider sur matériel réel (docs §7 #6). Le préflight a déjà + # éliminé les causes courantes (swap absent/petit, resume= manquant). + if systemctl hibernate; then + # On revient ici au RESUME. Bascule réussie : le firmware a normalement + # consommé BootNext au boot de Windows — MAIS un override manuel (menu + # F12) peut le laisser armé. On le purge donc INCONDITIONNELLEMENT (M1) : + # no-op s'il est déjà consommé, rétablit le filet BootOrder=FWS sinon. + [ -n "$esp" ] && { mount -o remount,rw "$esp" 2>/dev/null || true; } + efibootmgr -N >/dev/null 2>&1 || true + bootnext_set="" + trap - EXIT + log "Session FWS restaurée. Bon retour." + else + rc=$? + [ -n "$esp" ] && { mount -o remount,rw "$esp" 2>/dev/null || true; } + warn "Hibernation échouée (rc=$rc)." + if [ "$FWS_HIBERNATE_FALLBACK" = reboot ]; then + notify_user "Hibernation impossible — bascule vers Windows par reboot simple (session non sauvegardée)." + if efibootmgr -n "$win" >/dev/null 2>&1 && efibootmgr 2>/dev/null | grep -qi "BootNext: $win"; then + bootnext_set="" # on assume la bascule, pas de nettoyage + trap - EXIT + log "Reboot simple vers Windows…" + exec systemctl reboot + fi + fi + die "Bascule annulée — on reste sur FWS (BootNext nettoyé)." + fi +} + +usage() { + cat <