ebenoit.info

More code necromancy

Having managed to run LW Beta 5 on a modern(ish) stack, I figured I'd try the same thing with Beta 6.

LegacyWorlds Beta 6 was a full rewrite of the game in Java, with a backend server managing everything in the game and a pair of front server (one for the admin pages, and another for the game). It ran from 2010 to 2011 as a tech test - the game was far from feature-complete, compared to LWB5. It did include a single, once-per-minute tick, which was the most important addition I had planned on adding. I grew bored with LegacyWorlds around that time so development stopped.

What I remember about the tech

I'm writing this part before looking at the source code as a way to jog my memory; because of that, what I'm saying here might be inexact.

If I remember correctly, most of the game logic was implemented in the database itself (I was in a "put logic as close to the data as possible" phase at the time), using PL/PgSQL. The backend server would, in most cases, just call a few stored procedures and get whatever results it needed.

Speaking of the backend server, it was based on Spring and could be communicated with using Java RMI and a whole bunch of classes which represented messages to and from the server. It also included a scheduler, similar to Beta 5's tick manager.

Two other Spring-based applications implemented the admin- and user- facing interfaces. They were mostly empty shells that managed a session, called the backend as appropriate, and generated templates to be sent to the client. I believe the client interface was much lighter, including almost no Javascript.

Finally there was a command line tool which could manage the game. I don't remember how that thing worked at all.

The plan

I'll start by trying to get the Java code to build with the appropriate Java version (Java 7? 8 possibly? I don't remember), generating container images using multi-stage builds (I've never done that so it's a good occasion to try it out).

I'll also have to try and load the PostgreSQL code, which is way more hairy than Beta 5's. My idea is to try in on the latest version of the RDBMS, and move back to older and older versions until I get it to load.

At that point I'll be ready to try and upgrade the technical dependencies, which will no doubt be quite painful. One does not simply ~~walk into Mordor~~ upgrade both Java and Spring and expect the code to still build.

In order to limit the amount of work, I'll be using the B6M1 version as a starting point rather than the trunk, which was work in progress towards B6M2. I'll think about integrating the WIP code later on.

Running the original code

Building the Java code

So it turns out that the Maven build file indicates that the code was built using Java 1.6, and there's no official image for that. The oldest Maven + JDK images I could find were of Maven with Java 7, so I decided to build the application using them.

Getting the code itself to build inside a container wasn't all that hard. I took the time to make sure it isn't built by the root user, though. My initial attempt went something like this, except I had missed the ,uid=... part of the mount option, so the build failed due to Maven being unable to write to its own directory.

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"]

It is easy to turn this into a multi-stage build for the backend and command line tool (I didn't remember that, but it turns out that they are in fact contained within the same Java program), adding a script that would act as an entry point for both and generate the configuration file. I added that to a Compose file in order to test building it from there.

At this point, I wanted to move on to the next task, which was building images for the two sites. I started poking around because I wanted to reuse the build stage from the backend. Turns out I had misunderstood how multistage build worked and how they interact with Compose. I had missed the fact that you could specify the target to build from the same file. I also had to find an image for Apache Tomcat 6 running on Java 7.

I started looking for a way to configure the address of the backend server the frontends connect to... and found out it was hardcoded to localhost:9137. I think I used to run the frontend services with a stunnel instance connecting them to the backend container, which was itself running stunnel. I wasn't going to do that here, so I had to find another solution. Turns out Spring supports environment vars if one adds <context:property-placeholder /> element to the beans file, so I modified both interfaces' definitions like this:

<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>

Loading the database

Taking a quick look at the main database script revealed that it already supported parameters, although it did not read them from the environment. It was easy enough to replace these \set instructions with \getenv, and to reuse most of the init script and Dockerfile I had written for LWB5.

I was quite surprised when the initialization code actually loaded the whole thing without a hitch. I suspect the PL/PgSQL code contains incompatibilities but that they won't become visible until the functions are called.

The backend still did not load, I ended up with an exception when trying to connect:

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.

This means I have to either upgrade the PostgreSQL JDBC driver or downgrade PostgreSQL. I tried the first option, upgrading to 42.2.28.jre7, the latest version on Maven Central, and that worked.

But the backend wouldn't start, because of an exception in one of the components (com.deepclone.lw.interfaces.prefs.PreferenceDefinitionException so, well, my fault).

Doing some digging

While the README file found in the LWB5 repo was clearly missing a lot of stuff, at least it had one. I was quite sure that the exception I faced was due to some missing initialization, but had no clue what it was and what I was supposed to do.

I'm the kind of person who usually archives everything. I even have some code from my teenage years around! So I figured I'd try and see if I had something in there that could help. And that's when I found a bunch of old tar archives... The most important one is a copy of the Git repos that were used while working on B6M2; I thought I had lost these! I'll need to import the commits they contain on top of the lwb6 archive repo later on.

In addition, I found a bunch of shell scripts that used to run on a separate VM and that basically implemented a CI/CD pipeline. They would be triggered by pushes to the staging repo through CGI, and would merge everything into another repo (main), incrementing the build ID in the version number. They would then try to build the various JARs and WARs then run all tests. If the build failed they'd email the log to me, but it it succeeded they'd instead add the binaries to the main repo as well, leading to something like this:

* 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

In one of the files used by the build system, I was able to find a piece of script that was used to fully initialize a (B6M2 WIP) server and boot it up, checking whether it would actually start:

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

While this bit of code contains some stuff that is definitely B6M2-specific, it is a big clue for which parts to look at / for. I quickly found the corresponding parts in the B6M1 repo:

So, the next step would be to try and run the import commands in order to initialize the game.

Starting the game

I updated the Dockerfile to include the data. When I checked, it turned out the BUILD.sh script I had looked at when writing the initial version of the Dockerfile actually copied the data... It could have been a clue if I had noticed it.

I then added a volume to the backend container. This volume would contain simple marker files corresponding to the initialization stages.

Finally, I modified the entrypoint script to run the three import commands that seemed to be needed for B6M1:

Definitely in that order: technology definitions require translations, and "buildables" (ships and buildings) depend on technologies.

I also fixed a bunch of issues I noticed along the way: a bug in the tool part of the entrypoint script, incorrect privileges on /app, the main interface's WAR being deployed to the admin interface container, and issues with the builder stage which made rebuilding images take much longer than it should.

With that I was able to start the game. I figured I'd create an user and an administrator account:

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

I located the addresses of both web interfaces using docker network inspect and was able to log on to the admin interface. On the game interface, my account was disabled (CreateUser creates inactive game accounts, and should only be used to add accounts for the administrators). When I tried to activate it, something familiar popped up.

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;

I spent some time trying to get the bean configuration to work using environment variables, but I wasn't able to because having a user or password set to an empty string would cause it to try and authenticate. So instead I moved the mail sender bean initialization to data-source.xml and had the entrypoint script generate it. This is not very satisfactory but it works. I couldn't get STARTTLS to work either; setting mail.smtp.starttls.enable to true through the javaMailProperties property seemed to have no effect.

Conclusion

I decided to stop at this point, at least for now. I got B6M1 working, albeit with a few limitations (for example, the SMTP/STARTTLS part irks me). My worries about the PL/PgSQL code didn't materialize so far. However, it took longer than I thought it would for me to get it to work. On the bright side, I'm quite happy having found that old archive of mine!

I don't know what the next step would be: should I try and incorporate the B6M2 development branch? Should I just upgrade the technical components? Since these versions were mostly tech tests, I'm not really sure I want to spend more time on them, to be honest. At least for now.

The updated B6M1 code, including the Docker configuration parts, can be found here.