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