ebenoit.info

Un peu plus de nécromancie sur code

Ayant réussi à faire tourner LW Beta 5 sur une pile moderne (ou peu s'en faut), je me suis dit que j'allais tenter la même chose avec la Beta 6.

LegacyWorlds Beta 6 était une réécriture complète du jeu en Java, avec un serveur backend gérant tous les aspects du jeu et une paire de serveurs frontend (un pour les pages d'administration et un autre pour le jeu). Cette version a tourné de 2010 à 2011 en tant que test technologique – elle était loin d'être complète en termes de fonctionnalités par rapport à LWB5. Elle incluait cependant un unique tick par minute, ce qui était le changement le plus important que j'avais prévu. Je me suis lassé de LegacyWorlds à cette époque, donc le développement a cessé.

Ce dont je me souviens

J'écris cette partie avant de regarder le code source, afin de raviver ma mémoire ; de ce fait, ce que je dis ici pourrait être inexact.

Si je me souviens bien, la majeure partie de la logique du jeu était implémentée directement dans la base de données (j'étais dans une phase « logique aussi proche des données que possible » à cette époque), en utilisant PL/PgSQL. Le serveur backend se contentait dans la plupart des cas d'appeler quelques procédures stockées et de récupérer les résultats nécessaires.

En parlant du serveur backend, il était basé sur Spring et pouvait être contacté via Java RMI, avec tout un ensemble de classes représentant les messages échangés avec le serveur. Il incluait également un planificateur, similaire au gestionnaire de ticks de Beta 5.

Deux autres applications basées sur Spring implémentaient les interfaces administratives et utilisateur. Elles étaient principalement des coquilles vides qui géraient une session, appelaient le backend au besoin et généraient les pages à envoyer au client à partir de modèles. Il me semble que l'interface client était beaucoup plus légère et ne contenait presque pas de JavaScript.

Enfin, il y avait un outil en ligne de commande permettant de gérer certains éléments du jeu. Je ne me souviens plus du tout comment il fonctionnait.

Le plan

Je vais commencer par essayer de compiler le code Java avec la version appropriée (Java 7 ? 8 peut-être ? je ne me souviens plus), en générant des images de conteneur à l'aide de builds multi-étapes (je n'ai jamais fait ça, donc c'est l'occasion d'essayer).

Je vais également tenter de charger le code PostgreSQL, qui est bien plus complexe que celui de Beta 5. Mon idée est d'essayer d'abord avec la dernière version du SGBD, puis de revenir progressivement vers des versions plus anciennes jusqu'à ce que le chargement fonctionne.

À ce stade, je serai prêt à essayer de mettre à jour les dépendances techniques, ce qui sera sans doute assez douloureux.

Afin de limiter la quantité de travail, je vais utiliser la version B6M1 comme point de départ plutôt que la version « Trunk », qui contient du développement vers B6M2. Je réfléchirai plus tard à intégrer (ou non) ce code-là.

Exécution du code original

Compilation du code Java

Le fichier de build Maven indique que le code était construit avec Java 1.6, et il n'existe pas (plus ?) d'images officielles pour cette version. Les plus anciennes images Maven + JDK que j'ai trouvées étaient basées sur Maven avec Java 7, donc j'ai décidé d'utiliser celles-ci.

Faire compiler le code dans un conteneur n'a pas été très difficile. J'ai pris le temps de m'assurer que la compilation ne se fait pas en tant que root. Ma première tentative ressemblait à ceci, sauf que j'avais oublié la partie ,uid=... de l'option de montage, ce qui faisait échouer la compilation à cause de permissions sur le dossier Maven :

FROM maven:3-jdk-7
ENV BUILD_UID=1000
ENV BUILD=/src
ENV MAVEN_HOME=/var/maven
RUN mkdir -p $MAVEN_HOME && chown $BUILD_UID $MAVEN_HOME
ADD --chown=$BUILD_UID:$BUILD_UID .. $BUILD
USER $BUILD_UID
WORKDIR $BUILD
RUN --mount=type=cache,target=$MAVEN_HOME/.m2,uid=$BUILD_UID \
    mvn -e -Duser.home=$MAVEN_HOME package
ENTRYPOINT ["/bin/bash"]

Il a été facile de transformer cela en build multi-étapes pour le backend et l'outil en ligne de commande. J'ai ajouté un script d'entrée pour générer le fichier de configuration. Puis j'ai intégré cela dans un fichier Compose pour tester la construction depuis cet outil.

Ensuite, je voulais générer des images pour les deux sites et réutiliser l'étape de build du backend. J'ai dû chercher un peu pour comprendre comment fonctionnent les builds multi-étapes en interaction avec Compose. Finalement, j'ai trouvé qu'on peut spécifier la cible à construire depuis le même fichier via la clé target de la section build des services. J'ai également dû trouver une image pour Apache Tomcat 6 tournant sous Java 7.

En regardant la configuration du backend pour le frontend, j'ai découvert que l'adresse était codée en dur à localhost:9137. Je crois me souvenir qu'à l'époque, je faisais tourner sur les containeurs OpenVZ qui hébergeaient les services frontend un stunnel qui les connectait au backend, qui lui aussi avait un stunnel, mais dans l'autre sens. Comme je ne voulais pas faire ça ici, j'ai ajouté un support des variables d'environnement à Spring avec <context:property-placeholder />, puis modifié la configuration des interfaces comme ceci :

<context:property-placeholder />

<!-- ... -->

<!-- Session service connector -->
<bean id="sessionSrv" class="org.springframework.remoting.rmi.RmiProxyFactoryBean"
    scope="prototype">
    <property name="serviceInterface" value="com.deepclone.lw.session.SessionAccessor" />
    <property name="serviceUrl" value="rmi://${LW_BACKEND_HOST}:9137/sessionSrv" />
</bean>

Chargement de la base de données

Une inspection rapide du script SQL principal révèle qu'il supporte déjà une forme de paramétrisation, même si les valeurs ne sont pas lues depuis l'environnement. Je l'ai donc modifie pour qu'il le fasse avec \getenv au lieu de lire depuis un fichier avec des \set. Ensuite, j'ai réutilisé la majorité du script d'initialisation et du Dockerfile que j'ai mis en place pour LWB5.

À ma grande surprise, le code s'est chargé sans problème. Je m'attendais à des incompatibilités au niveau des procédures en PL/PgSQL, mais aucune ne s'est manifestée.

Cependant, le backend ne démarrait toujours pas, et j'ai obtenu cette erreur :

Caused by: org.postgresql.util.PSQLException: The authentication type 10 is
not supported. Check that you have configured the pg_hba.conf file to include
the client's IP address or subnet, and that it is using an authentication scheme
supported by the driver.

Cela signifiait que je devais soit mettre à jour le driver PostgreSQL JDBC, soit passer PostgreSQL à une version plus ancienne. J'ai opté pour la première solution, en essayant la version 42.2.28.jre7 du driver, et cela a fonctionné.

Mais le backend ne voulait pas démarrer, à cause d'une exception dans l'un des composants (com.deepclone.lw.interfaces.prefs.PreferenceDefinitionException, donc, bon, clairement de ma faute).

Fouillons les poubelles

Bien que le fichier README trouvé dans le dépôt de LWB5 omettait visiblement beaucoup d'informations, il avait le mérite d'exister. J'étais presque certain que l'exception que je rencontrais était due à une commande d'initialisation manquante, mais je n'avais aucune idée de quelle(s) commande(s) exactement, ni de ce que j'étais censé faire.

Je suis du genre à tout archiver. J'ai même encore du code que j'avais écrit quand j'étais ado qui traîne ! Du coup, je me suis dit que j'allais fouiller dans mes archives pour voir si je trouvais quelque chose qui pourrait m'aider. Et c'est là que je suis tombé sur tout un tas de vieux fichiers .tar...

Le plus important d'entre eux est une copie des dépôts Git utilisés lors du développement de B6M2 ; je pensais les avoir perdus ! Je vais pouvoir importer les commits qu'ils contiennent dans le dépôt d'archive lwb6 plus tard.

En plus de cela, j'ai retrouvé un paquet de scripts shell qui tournaient sur une machine virtuelle distincte et qui implémentaient grosso modo un pipeline CI/CD. Ils étaient déclenchés par des pushs dans le dépôt staging via un script CGI (lol), puis fusionnaient le tout dans un autre dépôt (main), incrémentant l'identifiant de build dans le numéro de version. Ensuite, ils tentaient de compiler les différents JAR et WAR, puis exécutaient tous les tests. Si la compilation échouait, ils m'envoyaient le journal d'erreurs par e-mail, alors que si elle réussissait, ils ajoutaient aussi les binaires au dépôt main, ce qui donnait quelque chose comme ça :

* 5f5833c (HEAD -> master, origin/master, origin/HEAD) (13 years ago) buildsystem@lw-build.internal.nocternity.net Build failure: 5.99.0-54
*   b39f8ff (13 years ago) buildsystem@lw-build.internal.nocternity.net Merge remote branch 'staging/master'
|\
| * 0c6ea3e (13 years ago) tseeker@legacyworlds.com Event database access
| * ee876c8 (13 years ago) tseeker@legacyworlds.com Event processing task
| * 6d2d4d3 (13 years ago) tseeker@legacyworlds.com Package for event handling components
* | 1c1907e (13 years ago) buildsystem@lw-build.internal.nocternity.net Successful build: 5.99.0-53
* | e0a3d21 (13 years ago) buildsystem@lw-build.internal.nocternity.net Merge remote branch 'staging/master'
|\|
| * 36a0bcd (13 years ago) tseeker@legacyworlds.com Events storage procedure
| * b42bc47 (13 years ago) tseeker@legacyworlds.com Event-related functions split
* | 076a2d9 (13 years ago) buildsystem@lw-build.internal.nocternity.net Successful build: 5.99.0-52
* | 2eeccb0 (13 years ago) buildsystem@lw-build.internal.nocternity.net Merge remote branch 'staging/master'
|\|
| * 55b0c44 (13 years ago) tseeker@legacyworlds.com Events database structure
| * 91ea271 (13 years ago) tseeker@legacyworlds.com Priority settings update procedures
| * 1eeaa50 (13 years ago) tseeker@legacyworlds.com Comments in event definition data classes
* | 286617d (13 years ago) buildsystem@lw-build.internal.nocternity.net Successful build: 5.99.0-51

Dans l'un des fichiers utilisés par le système de build, j'ai trouvé un bout de script qui servait à initialiser complètement un serveur (B6M2 en cours de développement) et à le démarrer, afin de vérifier s'il fonctionnait réellement :

java legacyworlds-server-main-*.jar --run-tool ImportText data/i18n-text.xml || exit 1
java legacyworlds-server-main-*.jar --run-tool ImportEvents data/event-definitions.xml || exit 1
java legacyworlds-server-main-*.jar --run-tool ImportResources data/resources.xml || exit 1
java legacyworlds-server-main-*.jar --run-tool ImportTechs data/techs.xml || exit 1
java legacyworlds-server-main-*.jar --run-tool ImportTechGraph data/tech-graph.xml || exit 1
java legacyworlds-server-main-*.jar --run-tool ImportBuildables data/buildables.xml || exit 1

java legacyworlds-server-main-*.jar &
sleep 10
if ! ps ux | grep -q 'java -jar legacyworlds-server-main'; then
    exit 1;
fi
java legacyworlds-server-main-*.jar --run-tool Stop || {
    killall java
    exit 1;
}

java -jar legacyworlds-server-main-1.0.0-0.jar --run-tool CreateUser 'test@example.org 12blah34bleh en' || exit 1
java -jar legacyworlds-server-main-1.0.0-0.jar --run-tool CreateSuperuser 'test@example.org Turlututu' || exit 1

Bien que ce bout de code contienne des éléments clairement spécifiques à B6M2, il constitue un indice précieux sur les parties à examiner. J'ai rapidement trouvé les éléments correspondants dans le dépôt de B6M1 :

La prochaine étape consistait donc à essayer d'exécuter les commandes d'import pour initialiser le jeu.

Lancement du jeu

J'ai mis à jour le Dockerfile pour inclure les données. En vérifiant, je me suis rendu compte que le script BUILD.sh que j'avais consulté en écrivant la première version du Dockerfile copiait déjà les données... J'aurais pu le remarquer plus tôt. Bref.

J'ai ensuite ajouté un volume au conteneur du backend. Ce volume contient de simples fichiers marqueurs correspondant aux différentes étapes d'initialisation.

Enfin, j'ai modifié le script d'entrée pour exécuter les trois commandes d'import qui semblaient nécessaires pour B6M1 :

Dans cet ordre précis : les définitions technologiques nécessitent des traductions, et les "buildables" (vaisseaux et bâtiments) dépendent des technologies.

J'en ai aussi profité pour corriger plusieurs problèmes que j'avais remarqués en cours de route : un bug dans la partie tool du script d'entrée, des droits incorrects sur /app, le fichier WAR de l'interface principale déployé dans le conteneur de l'interface d'administration, ainsi que des problèmes dans l'étape de build qui rendaient la reconstruction des images beaucoup plus lente que nécessaire.

Avec tout ça, j'ai pu lancer le jeu. J'ai alors décidé de créer un compte utilisateur et un compte administrateur :

docker compose run backend tool CreateUser tseeker@nocternity.net userpass en
docker compose run backend tool CreateSuperuser tseeker@nocternity.net TSeeker

J'ai trouvé les adresses des deux interfaces web en utilisant docker network inspect et j'ai pu me connecter à l'interface d'administration. Sur l'interface du jeu, mon compte était désactivé (CreateUser crée des comptes inactifs par défaut et ne devrait être utilisé que pour ajouter des comptes administrateurs). Lorsque j'ai essayé de l'activer, quelque chose de familier est apparu.

23:04:26,995  INFO SystemLogger:118 - Mailer - could not send mail to tseeker@nocternity.net
org.springframework.mail.MailSendException: Mail server connection failed; nested exception is javax.mail.MessagingException: Could not connect to SMTP host: 127.0.0.1, port: 25;

J'ai passé un certain temps à essayer de faire fonctionner la configuration du bean d'envoi de mail avec des variables d'environnement, mais ce n'était pas possible, car si un nom d'utilisateur ou un mot de passe était défini même avec une chaîne vide, cela entraînait une tentative d'authentification. J'ai donc déplacé l'initialisation du bean d'envoi de mails dans data-source.xml et modifié le script d'entrée pour le générer dynamiquement. Ce n'est pas très satisfaisant, mais ça fonctionne.

Je n'ai pas non plus réussi à faire fonctionner STARTTLS ; définir mail.smtp.starttls.enable à true via la propriété javaMailProperties ne semblait avoir aucun effet.

Conclusion

J'ai décidé de m'arrêter là, au moins pour le moment. J'ai réussi à faire tourner B6M1, bien qu'avec quelques limitations (par exemple, la partie SMTP/STARTTLS m'embête encore). Mes inquiétudes concernant le code PL/PgSQL ne se sont pas concrétisées pour l'instant. Cependant, cela m'a pris plus de temps que je ne le pensais. La bonne surprise, c'est que j'ai retrouvé cette vieille archive !

Je ne sais pas encore quelle sera la prochaine étape : devrais-je tenter d'intégrer la branche de développement de B6M2 ? Me contenter de mettre à jour les composants techniques ? Étant donné que ces versions étaient surtout des tests techniques, je ne suis pas sûr de vouloir y consacrer plus de temps, honnêtement. Du moins, pour l'instant.

Le code mis à jour de B6M1, y compris la configuration Docker, est disponible ici.