Title: Using systemd to make a Minecraft server to start on-demand and
       stop when it has no player
       Author: Solène
       Date: 20 August 2022
       Tags: minecraft nixos systemd automation
       Description: This article explains how to use systemd to start a
       network daemon upon connection and make it stop when it's not needed
       anymore, using Minecraft as a real world example.
       
       # Introduction
       
       Sometimes it feels I have specific use cases I need to solve alone. 
       Today, I wanted to have a local Minecraft server running on my own
       workstation, but only when someone needs it.  The point was that
       instead of having a big java server running all the system, Minecraft
       server would start upon connection from a player, and would stop when
       no player remains.
       
       However, after looking a bit more into this topic, it seems I'm not the
       only one who need this.
       
 (HTM) on-demand-minecraft: a project to automatically start a remote cloud server for whitelisted players
 (HTM) minecraft-server-hibernation: a wrapper that starts and stop a Minecraft server upon condition
       
       As often, I prefer not to rely on third party tools when I can, so I
       found a solution to implement this using only systemd.
       
       Even better, note that this method can work with any daemon given you
       can programmatically get the information whether to let it running or
       stop.  In this example, I'm using Minecraft and the server stop is
       decided based on the player connecting fetch through rcon (a remote
       administration protocol).
       
       # The setup
       
       I made a simple graph to show the dependencies, there are many systemd
       components used to build this.
       
 (DIR) systemd dependency graph
       
       The important part is the use of the systemd proxifier, it's a command
       to accept a connection over TCP and relay it to another socket,
       meanwhile you can do things such as starting a server and wait for it
       to be ready.  This is the key of this setup, without it, this couldn't
       be possible.
       
       Basically, listen-minecraft.socket listens on the public TCP port and
       runs listen-minecraft.service upon connection.  This service needs
       hook-minecraft.service which is responsible for stopping or starting
       minecraft, but will also make listen-minecraft.service wait for the TCP
       port to be open so the proxifier will relay the connection to the
       daemon.
       
       Then, minecraft-server.service is started alongside with
       stop-minecraft.timer which will regularly run stop-minecraft.service to
       try to stop the server if possible.
       
       # Configuration
       
       I used NixOS to configure my on-demand Minecraft server.  This is
       something you can do on any systemd capable system, but I will provide
       a NixOS example, it shouldn't be hard to translate to a regular systemd
       configuration files.
       
       ```nix
       { config, lib, pkgs, modulesPath, ... }:
       let
       
         # check every 20 seconds if the server
         # need to be stopped
         frequency-check-players = "*-*-* *:*:0/20";
       
         # time in second before we could stop the server
         # this should let it time to spawn
         minimum-server-lifetime = 300;
       
         # minecraft port
         # used in a few places in the code
         # this is not the port that should be used publicly
         # don't need to open it on the firewall
         minecraft-port = 25564;
       
         # this is the port that will trigger the server start
         # and the one that should be used by players
         # you need to open it in the firewall
         public-port = 25565;
       
         # a rcon password used by the local systemd commands
         # to get information about the server such as the
         # player list
         # this will be stored plaintext in the store
         rcon-password = "260a368f55f4fb4fa";
       
         # a script used by hook-minecraft.service
         # to start minecraft and the timer regularly
         # polling for stopping it
         start-mc = pkgs.writeShellScriptBin "start-mc" ''
           systemctl start minecraft-server.service
           systemctl start stop-minecraft.timer
         '';
       
         # wait 60s for a TCP socket to be available
         # to wait in the proxifier
         # idea found in https://blog.developer.atlassian.com/docker-systemd-socket-activation/
         wait-tcp = pkgs.writeShellScriptBin "wait-tcp" ''
           for i in `seq 60`; do
             if ${pkgs.libressl.nc}/bin/nc -z 127.0.0.1 ${toString minecraft-port} > /dev/null ; then
               exit 0
             fi
             ${pkgs.busybox.out}/bin/sleep 1
           done
           exit 1
         '';
       
         # script returning true if the server has to be shutdown
         # for minecraft, uses rcon to get the player list
         # skips the checks if the service started less than minimum-server-lifetime
         no-player-connected = pkgs.writeShellScriptBin "no-player-connected" ''
           servicestartsec=$(date -d "$(systemctl show --property=ActiveEnterTimestamp minecraft-server.service | cut -d= -f2)" +%s)
           serviceelapsedsec=$(( $(date +%s) - servicestartsec))
       
           # exit if the server started less than 5 minutes ago
           if [ $serviceelapsedsec -lt ${toString minimum-server-lifetime} ]
           then
             echo "server is too young to be stopped"
             exit 1
           fi
       
           PLAYERS=`printf "list\n" | ${pkgs.rcon.out}/bin/rcon -m -H 127.0.0.1 -p 25575 -P ${rcon-password}`
           if echo "$PLAYERS" | grep "are 0 of a"
           then
             exit 0
           else
             exit 1
           fi
         '';
       
       in
       {
       
         # use NixOS module to declare your Minecraft
         # rcon is mandatory for no-player-connected
         services.minecraft-server = {
           enable = true;
           eula = true;
           openFirewall = false;
           declarative = true;
           serverProperties = {
             server-port = minecraft-port;
             difficulty = 3;
             gamemode = "survival";
             force-gamemode = true;
             max-players = 10;
             level-seed = 238902389203;
             motd = "NixOS Minecraft server!";
             white-list = false;
             enable-rcon = true;
             "rcon.password" = rcon-password;
           };
         };
       
         # don't start Minecraft on startup
         systemd.services.minecraft-server = {
             wantedBy = pkgs.lib.mkForce [];
         };
       
         # this waits for incoming connection on public-port
         # and triggers listen-minecraft.service upon connection
         systemd.sockets.listen-minecraft = {
           enable = true;
           wantedBy = [ "sockets.target" ];
           requires = [ "network.target" ];
           listenStreams = [ "${toString public-port}" ];
         };
       
         # this is triggered by a connection on TCP port public-port
         # start hook-minecraft if not running yet and wait for it to return
         # then, proxify the TCP connection to the real Minecraft port on localhost
         systemd.services.listen-minecraft = {
           path = with pkgs; [ systemd ];
           enable = true;
           requires = [ "hook-minecraft.service" "listen-minecraft.socket" ];
           after =    [ "hook-minecraft.service" "listen-minecraft.socket"];
           serviceConfig.ExecStart = "${pkgs.systemd.out}/lib/systemd/systemd-socket-proxyd 127.0.0.1:${toString minecraft-port}";
         };
       
         # this starts Minecraft is required
         # and wait for it to be available over TCP
         # to unlock listen-minecraft.service proxy
         systemd.services.hook-minecraft = {
           path = with pkgs; [ systemd libressl busybox ];
           enable = true;
           serviceConfig = {
               ExecStartPost = "${wait-tcp.out}/bin/wait-tcp";
               ExecStart     = "${start-mc.out}/bin/start-mc";
           };
         };
       
         # create a timer running every frequency-check-players
         # that runs stop-minecraft.service script on a regular
         # basis to check if the server needs to be stopped
         systemd.timers.stop-minecraft = {
           enable = true;
           timerConfig = {
             OnCalendar = "${frequency-check-players}";
             Unit = "stop-minecraft.service";
           };
           wantedBy = [ "timers.target" ];
         };
       
         # run the script no-player-connected
         # and if it returns true, stop the minecraft-server
         # but also the timer and the hook-minecraft service
         # to prepare a working state ready to resume the
         # server again
         systemd.services.stop-minecraft = {
           enable = true;
           serviceConfig.Type = "oneshot";
           script = ''
             if ${no-player-connected}/bin/no-player-connected
             then
               echo "stopping server"
               systemctl stop minecraft-server.service
               systemctl stop hook-minecraft.service
               systemctl stop stop-minecraft.timer
             fi
           '';
         };
       
       }
       ```
       
       # Conclusion
       
       I'm really happy to have figured out this smart way to create an
       on-demand Minecraft, and the design can be reused with many other kinds
       of daemons.