Files
nocode 8d1b6bf23b fix(fws-gameboot): improve windows not found error messaging
Replace terse die message with user-friendly notification before the error.

When Windows Boot Manager entry is not found, first notify the user with
clear instructions about installing gaming Windows and checking UEFI boot
entries before terminating with the technical error message.
2026-07-08 23:55:41 +02:00

257 lines
12 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}"
: "${FWS_DEFAULT_GAME:=valorant}"
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 tok bootnext_set="" rc m game="${FWS_DEFAULT_GAME:-valorant}" game_req=""
while [ $# -gt 0 ]; do
case "$1" in
--game) game="${2:-}"; game_req=1; shift 2 ;;
--game=*) game="${1#--game=}"; game_req=1; shift ;;
*) die "argument inconnu : $1 (usage : to-windows [--game <id>])" ;;
esac
done
# Charte d'identifiant alignée sur les clés de games.json : [a-z0-9_-]
# (l'underscore doit passer, sinon divergence FWS↔Windows).
game="$(printf '%s' "$game" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-')"
[ -n "$game_req" ] && [ -z "$game" ] && warn "identifiant --game invalide après nettoyage — jeu par défaut appliqué."
win="$(find_windows_bootnum)"
if [ -z "$win" ]; then
notify_user "Windows introuvable — installe d'abord le Windows gaming (entrée de boot UEFI absente)."
die "entrée « Windows Boot Manager » introuvable (Windows installé + entrée UEFI présente ?)"
fi
esp="$(find_esp)"
# Jeton « quel jeu lancer », lu ET CONSOMMÉ par fws-play côté Windows.
# Écriture ATOMIQUE (.tmp puis mv) sur l'ESP FAT uniquement. En cas d'échec,
# on PURGE tout jeton périmé (jamais de rejeu d'un ancien choix) → Windows
# lancera le jeu par défaut plutôt qu'un mauvais jeu.
if [ -n "$esp" ] && [ "$(findmnt -no FSTYPE "$esp" 2>/dev/null)" = vfat ]; then
if [ -n "$game" ]; then
mkdir -p "$esp/EFI/FWS" 2>/dev/null || true
tok="$esp/EFI/FWS/launch.json"
if printf '{"game":"%s"}\n' "$game" > "$tok.tmp" 2>/dev/null && mv -f "$tok.tmp" "$tok" 2>/dev/null; then
log "Jeu demandé : $game"
else
rm -f "$tok" "$tok.tmp" 2>/dev/null || true
warn "écriture du jeton échouée → jeton purgé (Windows lancera le jeu par défaut)."
notify_user "Choix du jeu non transmis — Windows lancera le jeu par défaut."
fi
fi
elif [ -n "$game_req" ]; then
warn "ESP FAT introuvable → le choix du jeu ne peut pas être transmis à Windows."
fi
# 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, boot)
fws-gameboot to-windows [--game <id>] Hiberne FWS, bascule + lance <id> (root/pkexec)
fws-gameboot status Alias de « doctor »
Le jeu par défaut (FWS_DEFAULT_GAME) est « valorant ». Les identifiants connus
sont définis dans games.json côté Windows (valorant, lol, …).
Config : $CONF
Détails et risques : docs/hibernate-swap-dualboot.md
EOF
}
case "${1:-}" in
to-windows) shift; cmd_to_windows "$@" ;;
doctor|status) cmd_doctor ;;
''|-h|--help|help) usage ;;
*) die "sous-commande inconnue : $1 (voir --help)" ;;
esac