Blog
Comment j'ai (presque) foutu en l'air une demi-journée de boulot
Un article qui va être un petit retour d'expérience sur une grosse tuile qui m'est arrivée ce matin… et sur comment j'ai fini par m'en sortir. On sort un peu des making-ofs habituels, mais je me dis que cette expérience pourra servir à d'autres.
Le contexte
J'en ai déjà parlé, le contenu logique du jeu est écrit dans des fichiers YAML : comme il y en a un bon paquet et qu'une erreur est vite arrivée, j'ai mis en place un petit script qui lit le contenu du jeu et le « valide » selon un certain nombre de règles.
En gros, on teste plein de choses comme :
- si un niveau a besoin d'un objet « bouteille », on vérifie que
data/objects/bouteille.yaml
existe ; - on vérifie que le skin (l'image) de cet objet existe ;
- pour chaque action sur l'objet (le prendre, le regarder), on vérifie
que le script fait appel à des fonctions qui existent et qui sont
bien formées (exemple : la fonction
talk: []
qui va prendre 2 arguments, le personnage qui parle et la phrase prononcée) ; - pour les phrases prononcées, vérifier que la traduction en anglais existe ;
- etc.
Quelques tests sont faits au runtime, mais pour la grande majorité, ça a plus de sens de les déporter dans un script à part, pour la simple bonne raison que le contenu du jeu ne va pas varier d'un run sur l'autre, et qu'il n'est pas nécessaire de tout tester à chaque chargement : il suffit que je valide les niveaux une fois avant la distribution du jeu, et ça roule.
Brrrrrref.
Il se trouve que récemment, pas mal de petites choses ont évolué dans le moteur du jeu et dans les formats des données du jeu. De fait, le script de validation n'était plus à jour (il ne testait plus les bonnes choses et ne testait pas du tout certaines fonctionnalités récentes).
Comme le script commençait à être un peu bordélique, je me suis pris quelques heures pour le réorganiser, le mettre à jour, le nettoyer, et en refaire un truc fonctionnel. C'était long, c'était chiant, mais à la fin c'était chouette, ça marchait bien, et j'étais content.
Là où j'ai merdé
La toute première connerie, et sans doute la plus importante : je n'ai pas commité les changements. J'étais occupé à faire tourner le script et à corriger les niveaux selon les erreurs trouvées, et j'ai zappé. Grosse erreur.
Quelques jours passent, j'oublie et je bosse sur d'autres trucs. À un moment donné, j'implémente une fonctionnalité pour tester quelque chose dans le jeu, et puis finalement je me dis que je ne suis pas sûr de vouloir la garder. Qu'est-ce que je fais ? Je me dis que pour l'instant, je vais la stocker sur le repo Git distant, et la virer localement. Je la commit, je la push…
Et là, deuxième énorme connerie : pour récupérer ma branche à l'état
d'avant ce commit que je ne veux pas… je fais un git reset --hard
[le_commit_d_avant]
. J'ai l'habitude d'utiliser cette commande qui a
le gros avantage de nettoyer la branche et de vous donner un truc
propre qui correspond à un commit donné.
Sauf que…
Sauf que les modifications de mon script n'étaient pas commitées. Du
coup elles n'étaient « enregistrées » nulle part, ni localement, ni
sur la branche distante… et elles ont fait partie des trucs
« nettoyés » par mon git reset --hard
.
Je ne m'en rends compte que quelques jours après, à un moment où, ayant modifié quelques niveaux du jeu, je veux refaire tourner le script de validation… et je me rends compte qu'il est revenu à l'ancienne version obsolète.
Je vous avouerai qu'à ce moment-là, je pige assez vite le problème, et je suis au bord des larmes et de la crise de nerf. Je sais pertinemment que j'ai merdé dans les grandes largeurs et que je ne peux m'en prendre qu'à moi-même ; j'ai passé plusieurs heures, une bonne demi-journée sur la mise à jour de ce script, en pure perte alors que je suis déjà ric-rac sur les délais ; je comprends que je vais devoir tout refaire, un travail qui était long et chiant, et que je vais probablement remettre autant de temps, refaire les mêmes erreurs, etc.
Le sauvetage miracle
Je recherche un peu sur internet comment récupérer les données après
une telle couille, mais c'est peine perdue : même le fameux git
reflog
, qui permet de sauver pas mal de trucs, ne peut rien à partir
du moment où les changements perdus étaient unstaged.
Et puis soudain, je me rappelle qu'il y a quelques années, j'avais réussi à récupérer un texte perdu avec une commande toute con…
Oui, parce que vous le savez sans doute : quand vous supprimez un fichier, le contenu du fichier n'est pas « vraiment » supprimé dans la mémoire, c'est simplement l'indexation du fichier qui est supprimée : la mémoire précédemment occupée par votre fichier est simplement considérée comme « libre » ou « disponible », mais tant que rien n'a été écrit dessus, le contenu reste inchangé. Ajoutons qu'avec les mécanismes de journalisation et de redondance, le contenu d'un fichier peut se retrouver copié à plusieurs endroits du disque.
Bref, je n'ai pas à proprement parler supprimé le fichier, c'est Git qui a remplacé le contenu par une ancienne version, mais je me dis qu'en interne, ça doit revenir au même, et je tente.
La commande magique ? La voilà :
$ grep -a -C 2000 "global_objects\.add" /dev/sda2 | tee recovered_data
Oui, c'est juste grep
. La magie de Gnunux : pas besoin d'installer
un logiciel de récupération de données complexe et chiant à utiliser,
c'est juste une bête commande disponible partout.
Explications :
grep
, vous connaissez sans doute, ça va tout simplement chercher une chaîne de caractères dans un fichier, et vous la renvoyer avec plus ou moins de lignes de contexte ;-a
est une option qui signifie qu'on veut traiter les fichiers binaires « comme si » c'était des fichiers textes : normalement la commande ne marche que sur du texte, mais si vous voulez chercher une chaîne de caractère dans les octets qui composent une image JPG, libre à vous ;-C 2000
, « C » pour contexte : si la chaîne de caractères est trouvée, je veux quegrep
me renvoie les 2000 lignes qui « entourent » la ligne où la chaîne apparaît. Comme mon script faisait dans les 1000 lignes, j'ai tapé large. Vous pouvez aussi jouer avec-B
(pour before) et-A
(pour after) pour avoir un contexte non-symmétrique ;"global_objects\.add"
: une chaîne de caractères dont je me rappelais qu'elle était présente dans le script en question. Si vous avez perdu un texte, essayez de vous rappeler d'un titre ou d'une séquence de mots spécifique. Pour ma part, je me souvenais qu'à un moment, je remplissais unset
nomméglobal_objects
;/dev/sda2
: le truc magique, c'est quegrep
peut tourner sur une partition complète ! Ici, la partition qui correspond à un mon dossier perso ;tee recovered_data
: ça, c'est optionnel, mais ça permet, lorsque la chaîne est trouvée, à la fois de l'afficher et de la stocker dans le fichierrecovered_data
.
Pour faire simple : on demande à grep
de scanner l'intégralité du
disque dur (ou de la partition en question) comme si c'était un gros
texte, et si jamais il trouve la chaîne global_objects\.add
, de nous
afficher les 2000 lignes de texte qui l'encadrent.
Alors bien sûr, ça prend un petit peu de temps, on parle d'un disque
de 800 Gio… mais au bout d'une vingtaine de minutes… bingo, le script
apparaît. Bon, il est entouré d'un petit merdier d'octets qui ne
veulent rien dire (encore une fois, j'avais tapé large avec mes 2000
lignes de contexte), mais ça se vire facilement et on retrouve notre
beau fichier perdu, intact. Une demi-journée de boulot de sauvée avec
un petit grep
.
Joie, bonheur, soulagement. Que j'aime Gnunux et ses petits outils qui peuvent faire des miracles.
Conclusion
Je ne vais pas vous dire de toujours faire des sauvegardes ou de bien commiter vos changements : c'est ce que je fais, je fais gaffe, tout le code source est bien archivé sur Git, je fais des sauvegardes régulières… mais voilà, un peu de fatigue, un peu d'inattention, et ça arrive vite de merder. Et des fois, quand ça arrive, ça craint violemment. J'ai regardé toutes mes sauvegardes, aucune ne datait d'entre la mise à jour du script et son écrasement.
Sachez juste que si vous perdez un fichier texte, et que vous vous en
rendez compte relativement vite (au bout d'un moment, le bloc
mémoire finira par être écrasé, c'est sûr) : vous pouvez le
récupérer avec un simple grep
. C'est facile, c'est rapide, ça
marche. Ça peut vous sauver la vie.
(Et si vous perdez des choses plus compliquées comme des images, il existe aussi des logiciels pour les récupérer, mais c'est forcément moins évident.)
Voilà, c'était mon petit retour d'expérience sur l'ascenseur émotionnel du jour. Sur ce, je retourne tester mes niveaux (et oui, bien sûr, dès la récupération, j'ai commité ce foutu script de validation).