Tuesday, August 20, 2019

Toggling the Minecraft Server using systemd features

The new school year is upon us, suddenly the kids are playing Minecraft much less.

This means that the Minecraft server is sitting there churning all day and night, spawning and unspawning, eating CPU and generating heat for a sparse collection of occasional players now. It's an old Sempron 145 (45w, single core), so a single world sitting idle still consumes 40% CPU.

We already use systemd to start and stop the server. Let's add a couple new features to stop the server during the school day. Oh, and let's stop it during the deep night, also.

Here's what we currently have: A basic start/stop/restart systemd service that brings up the server at start:

   ## /etc/systemd/system/minecraft.service

   [Unit]
   Description=Minecraft Server
   After=network.target

   [Service]
   RemainAfterExit=yes
   WorkingDirectory=/home/minecraft
   User=minecraft
   Group=minecraft

   # Start Screen, Java, and Minecraft
   ExecStart=screen -s mc -d -m java -server -Xms512M -Xmx1024M -jar server.jar nogui

   # Tell Minecraft to gracefully stop.
   # Ending Minecraft will terminate Java
   # systemd will kill Screen after the 10-second delay. No explicit kill for Screen needed
   ExecStop=screen -p 0 -S mc -X eval 'stuff "say SERVER SHUTTING DOWN. Saving map..."\\015'
   ExecStop=screen -p 0 -S mc -X eval 'stuff "save-all"\\015'
   ExecStop=screen -p 0 -S mc -X eval 'stuff "stop"\\015'
   ExecStop=sleep 10

   [Install]
   WantedBy=multi-user.target


If you do something like this, remember to:

    $ sudo systemctl daemon-reload
    $ sudo systemctl enable/disable minecraft.service  // Autostart at boot
    $ sudo systemctl start/stop minecraft.service      // Manual start/stop


We need to start with a little bit of planning. After looking at the myriad of hours and days that the server should be available (Summer, Holidays, Weekends, School Afternoons), I don't see a way to make all those work smoothly together inside a cron job or systemd timer.

Instead, let's move the logic into a full-fledged Python script, and let the script decide whether the server should be on or off. Our systemd timer will run the script periodically.

Wait...that's not right. Systemd timers run only services. So the timer must trigger a service, the service runs the script, the script decides if the server should be on or off, and uses the existing service to do so.

Let's draw that out

minecraft-hourly.timer -+  (timers can only run services)
                        |
                        v
                minecraft-hourly.service -+  (service can run a script)
                                          |
                                          v
                                   minecraft-hourly.py -+ (start/stop logic and decision)
                                                        |
                                                        v
                                                 minecraft.service (start/stop the server)


We know where we are going, let's work backwards to get there. We need a Python script with logic, and the ability to decide if the server should be off or on based upon any give time or date.

## /home/me/minecraft-hourly.py

#!/usr/bin/env python3
import datetime, subprocess

def ok_to_run_server():
    """Determine if the server SHOULD be up"""
 
   now = datetime.datetime.now()

    ## All days, OK to run 0-2, 5-8, 16-24
    if -1 < now.hour < 2 or 4 < now.hour < 8 or 15 < now.hour < 24:
        return True

    ## OK to run on weekends -- now.weekday() =  6 or 7 
    if now.weekday() > 5:
        return True

    ## OK to run during Summer Vacation (usually mid May - mid Aug)
    if 5 < now.month < 8:
        return True 
    if now.month == 5 and now.day > 15:
        return True
    if now.month == 8 and now.day < 15:
        return True

    ## OK to run on School Holidays 2019-20
    ## Fill in these holidays!
    school_holidays = ["Aug 30 Fri","Sep 02 Mon"]
    if now.strftime("%b %d %a") in school_holidays:
        return True

    return False

def server_running():
    """Determine if the Minecraft server is currently up"""
    cmd = '/bin/systemctl is-active minecraft.service'
    proc = subprocess.Popen(cmd, shell=True,stdout=subprocess.PIPE)
    if proc.communicate()[0].decode().strip('\n') == 'active':
        return True
    else:
        return False

def run_server(run_flag=True):
    """run_flag=True will start the service. False will stop the service"""
    cmd = '/bin/systemctl start minecraft.service'
    if not run_flag:
        cmd = '/bin/systemctl stop minecraft.service'
    proc = subprocess.Popen(cmd, shell=True,stdout=subprocess.PIPE)
    proc.communicate()
    return

## If the server is stopped, but we're in an ON window, then start the server
if ok_to_run_server() and not server_running():
    run_server(True)
 
## If the server is running, but we're in a OFF window, then stop the server
elif not ok_to_run_server and server_running():
    run_server(False)


This script should be executable, and since it tells systemctl to start/stop services, it should be run using sudo. Let's try this during school hours on a school day:

    $ chmod +x /home/me/minecraft-hourly.py

    $ sudo /home/me/minecraft-hourly.py
        // No output

    $ systemctl status minecraft.service 
        ● minecraft.service - Minecraft Server
           Loaded: loaded (/etc/systemd/system/minecraft.service; enabled; vendor preset: enabled)
           Active: inactive (dead)
        // It worked!


Still working backward, let's create the systemd service that runs the script. The 'type' is 'oneshot' - this is not an always-available daemon. It's a script that does it's function, then terminates.

## /etc/systemd/system/minecraft-hourly.service.

[Unit]
Description=Minecraft shutdown during school and night
After=network.target

[Service]
Type=oneshot
ExecStart=/home/me/minecraft-hourly.py
StandardOutput=journal

[Install]
WantedBy=multi-user.target


We want the hourly script to be triggered by TWO events: Either the hourly timer OR by the system starting up. This also means that we DON'T want minecraft.service to automatically start anymore. We want the script to automatically start, and to decide.

    $ sudo systemctl daemon-reload                     // We added a new service
    $ sudo systemctl enable minecraft-hourly.service   // Run at boot
    $ sudo systemctl disable minecraft.service         // No longer needs to run at boot


Let's test it again during school hours. It should shut down the Minecraft server. It did.

    $ sudo systemctl start minecraft.service       // Wait for it to finish loading (1-2 minutes)
    $ sudo systemctl start minecraft-hourly.service
    $ systemctl status minecraft.service
        ● minecraft.service - Minecraft Server
           Loaded: loaded (/etc/systemd/system/minecraft.service; disabled; vendor preset: enabled)
           Active: inactive (dead)



Finally, let's set up a systemd timer to launch the hourly service...well, hourly.

## /etc/systemd/system/minecraft-hourly.timer:

[Unit]
Description=Run the Minecraft script hourly
[Timer]
OnBootSec=0min
OnCalendar=*-*-* *:00:00
Unit=minecraft-hourly.service

[Install]
WantedBy=multi-user.target


Writing a timer, like writing a service, isn't enough. Remember to activate them.

    $ sudo systemctl daemon-reload
    $ sudo systemctl enable minecraft-hourly.timer   // Start at boot
    $ sudo systemctl start minecraft-hourly.timer    // Start now


And let's check to see if the new timer is working

    $ systemctl list-timers | grep minecraft
        Tue 2019-08-20 15:00:30 CDT  30min left    Tue 2019-08-20 14:00:52 CDT  29min ago     minecraft-hourly.timer       minecraft-hourly.service

No comments: