Un peu plus de nécromancie sur code
Écrit par Emmanuel BENOÎT - Créé le 2025-01-04
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 :
- Les données sont disponibles ici ;
- L'outil en ligne de commande ne contient que trois classes d'import.
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 :
ImportText data/i18n-text.xml
ImportTechs data/techs.xml
ImportBuildables data/buildables.xml
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.