#!/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 <