2017-12-09

OpenTTD under systemd

Just got OpenTTD running satisfactorily on Ubuntu 17.10 Server using systemd, and thought I'd make a note for future reference. When the system is rebooted, OpenTTD shuts down gracefully, saving the game state. Then it comes back up resumed from the saved state.

I happen to have installed OpenTTD from source, just to ensure it has the right version to match current Android apps (1.7.1), and installed in /usr/local, borrowing data from apt-installed packages:

sudo apt install openttd-{data,opengfx,openmsx}
ln -s /usr/share/games/openttd/baseset ~/.openttd/baseset

(That's probably not critical, and some of those packages might be unnecessary for a headless server.)

I run the whole thing in an openttd account to isolate it from anything else. It includes a script, which I've called ~/.install/share/server-process.sh, but you can call it what you like. It's meant to be run under the openttd account:

#!/bin/bash

## List the target file and all autosaves.
files=(~/.openttd/save/esp-main.sav ~/.openttd/save/autosave/*.sav)

## Choose the most recent file.
best="${files[0]}"
bestdate="$(date +'%s%N' -r "$best")"
files=("${files[@]:1}")
while [ ${#files[@]} -gt 0 ]
do
    cand="${files[0]}"

    ## Skip an unmatched wildcard.
    if [ "$cand" = ~/.openttd/save/autosave/\*.sav ]
    then
        continue
    fi

    ## Choose this candidate if it is newer than the best so far.
    canddate="$(date +'%s%N' -r "$cand")"
    if [ "$canddate" -gt "$bestdate" ]
    then
        best="$cand"
        bestdate="$canddate"
    fi

    ## Move on to next file.
    files=("${files[@]:1}")
done

## Save the best file just in case.
printf 'Best file is %s\n' "$best"
cp --reflink=auto "$best" ~/.openttd/save/best.sav

## Run a dedicated server with the best file.
exec /usr/local/games/openttd -g "$best" -D

The intention is to use the latest .sav from among the original file and all autosaves. If the server dies suddenly, it ought to be the last periodic autosave; otherwise, it will take the exit.sav file saved automatically on exit. I'm assuming that the server doesn't save any inconsistent files.

As root, create /etc/systemd/system/openttd.service:

[Unit]
Description=Open Transport Tycoon Deluxe
After=network.target

[Service]
User=openttd
Type=simple
ExecStart=/home/openttd/.install/share/server-process.sh

[Install]
WantedBy=multi-user.target

You might initially need to run this, or after every edit of openttd.service:

sudo systemctl daemon-reload

Test it with:

sudo systemctl start openttd.service
sudo systemctl status openttd.service
sudo systemctl stop openttd.service

Enable it to start on boot with:

sudo systemctl enable openttd.service

I tried using openttd -f, and Type=forking or Type=oneshot, but I think it had trouble killing it. Maybe it needed an explicit ExecStop directive.

Probably a lot more could be done with this to make it more robust, but it's a start.

2017-01-21

Enhancing the XML generated by the DAV SVN module

Suppose you set up Apache to serve Subversion repositories. You might do this:

<Location "/svn-repos/">
  DAV svn
  SVNParentPath /var/svn-repos
  SVNIndexXSLT "/templates/svn-dir.xsl"
</Location>

The SVNIndexXSLT directive tells the Subversion module to serve any directory in a repository as XML to ordinary browsers (not SVN clients, which talk WebDAV). The XML will include a link to the XSLT stylesheet under the virtual path /templates/svn-dir.xsl, and the browser is expected to fetch that too, and use it to transform the XML into HTML. A sample XML output could be:

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="/templates/svn-dir.xsl"?>
<!DOCTYPE svn [
  <!ELEMENT svn   (index)>
  <!ATTLIST svn   version CDATA #REQUIRED
                  href    CDATA #REQUIRED>
  <!ELEMENT index (updir?, (file | dir)*)>
  <!ATTLIST index name    CDATA #IMPLIED
                  path    CDATA #IMPLIED
                  rev     CDATA #IMPLIED
                  base    CDATA #IMPLIED>
  <!ELEMENT updir EMPTY>
  <!ATTLIST updir href    CDATA #REQUIRED>
  <!ELEMENT file  EMPTY>
  <!ATTLIST file  name    CDATA #REQUIRED
                  href    CDATA #REQUIRED>
  <!ELEMENT dir   EMPTY>
  <!ATTLIST dir   name    CDATA #REQUIRED
                  href    CDATA #REQUIRED>
]>
<svn version="1.9.3 (r1718519)"
     href="http://subversion.apache.org/">
  <index rev="16" path="/" base="cakes-test">
    <dir name="café" href="caf%c3%a9/" />
    <file name="test" href="test" />
  </index>
</svn>

This is fine if you only want to browse the rest of the repository in the same manner, as it contains properly escaped relative links to files and other directories in the repository. However, if you want to jump to any other pages on the site, ones that aren't under DAV control, you have to hard-wire absolute (local) links to them.

When I generate XSLT-transformable XML dynamically, I get the script to include an attribute with a configurable path prefix, and the XSLT can then generate absolute URLs from relative ones simple by attaching the prefix. But you can't do the same with the DAV/SVN-generated XML, because you don't generate it! There also seems to be a (reasonable) unwillingness to develop the DAV/SVN Apache module, so a feature request would likely be ignored.

Time for an Apache rewrite rule. It has to match only GETs within the Subversion hierarchy (/svn-repos/ in our example), and only if the first path element after the repository name is not !svn, the special marker used by WebDAV SVN clients:

RewriteEngine On
RewriteCond %{REQUEST_METHOD} =GET
RewriteCond %{REQUEST_URI} ^/svn-repos/
RewriteCond %{REQUEST_URI} !^/svn-repos/[^/]+/!svn/
RewriteRule ^/svn-repos(/.*)$ /scripts/svn-web.php$1 [PT]

This now intercepts (say) /svn-repos/cakes-test/, and internally redirects it to /scripts/svn-web.php/cakes-test/. The script will run with PATH_INFO set to /cakes-test/, so it will know what file or directory to look at. Then it can do its own authorization checks, and serve enhanced XML.

It would be nice if /scripts/svn-web.php could be an internal file path, rather than an exposed virtual path. RewriteRule can be given a filesystem path instead of a virtual path, and the server identifies as such by seeing if the first path element exists in the filesystem. However, this doesn't seem to work:

RewriteEngine On
RewriteCond %{REQUEST_METHOD} =GET
RewriteCond %{REQUEST_URI} ^/svn-repos/
RewriteCond %{REQUEST_URI} !^/svn-repos/[^/]+/!svn/
RewriteRule ^/svn-repos(/.*)$ /usr/local/share/scripts/svn-web.php$1 [PT]

The server will respond with a 404, claiming that /usr/local/share/scripts/svn-web.php doesn't exist. It seems to require an alternative set of flags to work:

RewriteEngine On
RewriteRule ^/svn-repos(/.*)$ /usr/local/share/scripts/svn-web.php$1 [L,H=application/x-httpd-php]

So now L turns off further rewriting, and H explicitly sets the handler to PHP.