3d91ef3ac5
Implement fws-gameboot, an orchestrator for seamless dual-boot between FWS and Windows bare-metal using hibernate-swap mechanism. Enables playing kernel anticheat games (e.g., Valorant/Vanguard) without persistent virtualization by hibernating FWS to swap, setting BootNext to Windows UEFI one-shot, and rebooting. Upon Windows shutdown, the kernel resumes the FWS session via resume= parameter. Features: - doctor/status subcommand: validates hibernation readiness (swap size, resume= parameter, VRAM usage, Secure Boot status) - to-windows subcommand: hibernates FWS, unmounts NTFS filesystems, arms BootNext, and initiates hibernation; resumes FWS session on return from Windows - Safety mechanisms: BootOrder[0] fallback to FWS via fws-gameboot-bootfix, permanent BootNext cleanup on resume, optional simple reboot fallback - Configuration via /etc/fws/gameboot.conf (Windows boot entry number, swap margin, hibernation fallback mode) - User notification via D-Bus notify-send for graphical feedback - Comprehensive error handling and preflight validation Requires validated hardware support for NVIDIA GPU hibernation/resume and firmware BootNext semantics.
219 lines
10 KiB
Bash
219 lines
10 KiB
Bash
#!/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 <<EOF
|
|
fws-gameboot — dual-boot hibernate-swap (FWS ⇄ Windows bare-metal, anticheat kernel)
|
|
|
|
fws-gameboot doctor Vérifie l'environnement (préflight, entrées de boot)
|
|
fws-gameboot to-windows Hiberne FWS et bascule vers Windows (root/pkexec)
|
|
fws-gameboot status Alias de « doctor »
|
|
|
|
Config : $CONF
|
|
Détails et risques : docs/hibernate-swap-dualboot.md
|
|
EOF
|
|
}
|
|
|
|
case "${1:-}" in
|
|
to-windows) cmd_to_windows ;;
|
|
doctor|status) cmd_doctor ;;
|
|
''|-h|--help|help) usage ;;
|
|
*) die "sous-commande inconnue : $1 (voir --help)" ;;
|
|
esac
|