What is a correct minecraft server setup on Linux?

I’m used to host Minecraft servers for me and some friends for years. I discovered how to use Linux step by step when I started hosting my servers on VPSs and then on dedicated machines. However, there are still good practices I don’t know and I’m looking for advices about how to do things correctly.

I’m used to run my servers through screen command and using my admin account. I’m sure it’s probably not the safest way to do this. For now, I was hosting small servers for my friends but I plan to host a bigger network with a proxy (Velocity) and many Minecraft servers behind. This network will be opened to a larger community and thus I worry about security and performance.

I would like to:

  1. Launch the servers safely.
  2. Automatically restart all/some servers at a specific time of the day.
  3. Ensure that performance is equally distributed on all my servers.

I’ve heard multiple things but I don’t know what is correct and I would like to have the opinion of experienced system administrators. Here is few questions I have. Sorry if some of them are really dumb but I need to be sure of what I do.

a. Should I create an unix account called “minecraft” to launch the servers ?
b. Where should I put the servers folders ? (I mean the recommended location, probably not / or /home/admin/ for e.g.)
c. What should be the permissions of the servers files ?
d. Should I use cron to restart the servers ?
e. Should I create an unix service called “minecraft” ? (I don’t why, but I’ve seen it somewhere)
f. Should I use Docker ? (I don’t know how)

1 Like

Good questions. When it comes to running things on Unix I can offer some advice;

a. Should I create an unix account called “minecraft” to launch the servers ?

You should create an account for each server that you wish to run. It will cost you nothing and segregate all the files and processes to that user. In the future, if you ever collaborate with other users on your machine, you can share this account without fear as long as you trust them. But that is rare. It goes to the permissions question you ask later though. But in general, yes. Make a user for the server, not just for ‘minecraft’. Also, in Unix, it’s generally practice to keep user names under 8 characters and even less is best. (When I write server I mean the actual process running the game, not the hardware on which the process is running. I know it means both and can mean more. But I meant one user per server process ie., per “game”)

In the following, the # is the shell prompt of root (UID = 0)… I started using Unix decades before we had sudo so I don’t believe in it. I don’t need it. If I needed that extra guard rail on how I do “things as root” you might as well wheel me into the old-folks home now. But you’re not going to do that.

IMHO, If you’re root, then you’re root. Be a big boy. On the other hand, never, absolutely NEVER give the root password to anyone. Not your mom, not your girlfriend, not anyone. Ever. If you have to share responsibility to do admin things with other users and YOU have the root password, then let them use sudo. Gone are the days we trust people with root password. I remember those days, good times.

If you see $ prompt, it means the shell prompt of a non-root user (UID != 0)

Also, some other conventions… when you see a path /etc/blah we pronouce it /et-see/
We do not say /et-cet-era/

OK, now that is done and out of the way, let’s continue, shall we!

The command is simple:

# adduser foo

On Linux systems this usually does a few things.

  1. It creates a user foo in the /etc/passwd file.
  2. It creates a home directory (typically) /home/foo
  3. It creates a group called foo and that is added to the /etc/group
  4. It puts the user foo in the group foo

The group foo and the user foo are not the same. Different things.
If you wanted to make a new group that had a name bar then you can add that group to the system (before making the user foo)

# addgroup bar

Back to adduser: There are usually flags you can give the command to simultaneously add the user foo into group bar

(users can be members of multiple groups)

(The man page for commands is the best place to look. For Linux, the synopsis is as follows:

       adduser  [options]  [--home  DIR]  [--shell  SHELL]  [--no-create-home]
       [--uid ID] [--firstuid ID] [--lastuid ID] [--ingroup GROUP | --gid  ID]
       [--disabled-password]      [--disabled-login]      [--gecos      GECOS]
       [--add_extra_groups] [--encrypt-home] user

So thus, the command before to add foo user into group bar would look like:

# addgroup bar
# adduser --ingroup bar foo

Now foo is added as a user, and they belong to group bar. This fine difference can be helpful if you have multiple files/servers where SOME of the files are group-owned by bar but have user-ownership that is more finely sliced. Most of the time, most people just make new users and take the default action of a new group created per user. That wasn’t always the case. Back in the day, on university servers we’d make accounts per user, but they were all added to group students so that we could setup directories for them where they had group-wise permission as students. It’s a nuanced thing now days since most don’t work exclusively IN the Unix environment alone. That wasn’t always the case.

Anyway, I digress.

Make a user per MC Server. If the hardware has two servers running on it, you have two users, etc…

b. Where should I put the servers folders ? (I mean the recommended location, probably not / or /home/admin/ for e.g.)

Your system has a mount-table. To view it, type:

$ mount

Depending on how your Unix system was setup, you’ll probably have a handful of filesystems. Each mount point (directory) is the access to the filesystem.

N mount points means N filesystems. You might have more filesystems than what is mounted, but if they aren’t mounted, you won’t see them via mount(1)

Oh and another habit – when you see a (number) right after a command, it means what section the command is in. Section 1 are user commands. Section 8 are system-admin commands. Section 6 (my favorite) is games. Yup, look it up.

So, on your biggest filesystem, probably / if it’s a typical Ubuntu server is where you will make a new directory: /data/servers

# mkdir -p /data/servers

What does the -p flag do? It makes all the prerequisite directories before making the final one (servers)

This is a compromise though. If the system had already setup the partition table for you, then you have no choice but to put the servers all on the biggest partition you got.

On the other hand, if you’re making the system yourself, you can absolutely setup different partitions (different filesystems) and name them and mount them separately.

Not common and comes with a few gotchas:

  1. You have to know in advance how much data each filesystem will need. How does anyone know this? You can guess “big” and waste a lot of space or guess “small” and find yourself in the worse condition having to figure out how to merge data into a larger filesystem. I don’t like either case.

  2. On the flip side, having a separate filesystem for just minecraft gives you one benefit among many - but the one benefit is that IF for instance the system became screwed up so badly you had to re-image new Linux on it, you can leave the filesystem with the minecraft data UNTOUCHED. Nothing will affect it unless you literally mkfs(8) the data away. We used to make the root partition (/) as small as possible to hold the system files and then made a fat filesystem for data filesystems like /data and then symlink things to /data as necessary.

  3. If you use / as the filesystem to hold a sub-directory (/data/servers) then you avoid the #1 problem, but you’re taking a risk on #2. Good news: #2 doesn’t happen more often than the hardware failing. So go with #1.

What you didn’t ask about but should have asked about is how to investigate the remaining space on the filesystems:

$ df

And to find out how much data is being stored in a particular path then use the df command. Then with a bit of shell you can do nifty things like this:

$ cd /place/with/lots/of/data_and_files_and_directories
$ du -s * | sort -nr

I’ll let you figure out what that will do.

c. What should be the permissions of the servers files ?

You don’t get to worry about this unless you need to worry about this.
There’s a couple of concepts we need to cover first. Some of them you may already know, and some you probably haven’t given the nature of your questions. First what you probably don’t already know:

When files get created by a user, the permissions on the file are not just set to some default value. They are set in accordance with the default umask of the shell environment. Yep, it’s per shell environment. Log out of the shell, then the umask settings are gone until you re-set them the next time you login. This is why some users put the settings into their ~/.bashrc or whatever “run-commands - rc” file they have for the shell they use.

The mask is bit-wise and’d with the default 0666. Here’s a little experiment:

$ mkdir experiment ; cd experiment
$ umask
$ touch a
$ umask 0022
$ touch b
$ umask 0222
$ touch c
$ ls -l

The numbering scheme is the same used for chmod… (Say: /shmod/)
Each file permission is a Four groups of Three bits. We’ll not worry about the very first group yet. The problems there are sticky, pun intended.

Lets worry about the three groups that deal with USER (u), GROUP (g), and OTHER (o).
The three bits each for u, g and o range from 0-7
The bits correspond to the file permissions READ ®, WRITE (w), and EXEC (x)
They are in this order rwx
r is bit 2, or 2^2 = 4
w is bit 1 or 2^1 = 2
x is bit 0 or 2^0 = 1
If all three bits are set (rwx) then the numeric value of that group of bits is 7.
r + w + x = 4 + 2 + 1 = 7

USER, GROUP, OTHER Three sets of permission bits. Each set has 3 bits.

So the mode of a file 755 means:
USER permissions (USER means the owner of the file, always) = rwx (4 +2 +1) = 7
GROUP permissions = r-x ( 4 + 0 + 1 ) = 5
OTHER (OTHER means users NOT in the group that is the group-owner and not the USER (owner)) = r-x (4 + 0 + 1) = 5

Oh did we forget something?
Files have owners (that’s USER) Unix stores the owner of the file as the USER’s UID.
Files have group-owners (that’s GROUP) Unix stores the group-owner of the file as the GID (see /etc/group, the numeric field after the group-name)
and everyone else with respect to the file is OTHER

So, file permissions – a complex numbering scheme of 4 3-bit numbers. We are leaving out the first set because it’s not going to be helpful. Just the set for USER (u),
GROUP (g) and OTHER (o).

Back to our experiment:

$ mkdir experiment ; cd experiment
$ umask
$ touch a
$ umask 0022
$ touch b
$ umask 0222
$ touch c
$ ls -l

The modes you see are the permissions of the files. When I did the experiment I get this:

foo@skynet:~/ff$ umask
0002
foo@skynet:~/ff$ umask 0002
foo@skynet:~/ff$ touch a
foo@skynet:~/ff$ umask 0022
foo@skynet:~/ff$ touch b
foo@skynet:~/ff$ umask 0222
foo@skynet:~/ff$ touch c
foo@skynet:~/ff$ ls -l
total 0
-rw-rw-r-- 1 foo bar 0 Jan  3 20:45 a
-rw-r--r-- 1 foo bar 0 Jan  3 20:45 b
-r--r--r-- 1 foo bar 0 Jan  3 20:45 c

The user foo owns all three files. That’s what column 3 shows.
The group bar group-owns all three files. That’s what column 4 shows.
All were created on Jan 3 around 20:45.
The permissions of the files, in order are:

664
644
444

Remember the umask is going to mask off the bits from each group of permissions. The u, the g and the o groups were 6 each. The umask was originally 0002 (ignore the leading 0, the last three digits are the u, g, and o umask bits respectfully).
The 2 in the original umask removes the w permission from the o mode of 6 It’s sort of like a bit-clear operation. We’re clearing the 2 (w) bit from the 6 mode of the o permission. The 6 becomes a 4

Second test, umask 0022 This is setting the mask to remove the 2 (w) bit from the 6 mode of the g and o permissions. The 6's become 4's The u was unaffected because 6 &= ~(0) leaves the value unchanged for u

Third test, umask 0222. This is going to remove write permission from the u, g and o permissions. NO ONE will be able to write to the file. Not even the owner of the file.

So, where’s this all leading…

It’s all about your umask.

By default, most shells set the umask to 0002 which means only the owner and group-owner of the file can WRITE to the file. OTHER cannot. And this is a reasonable default to have for a multi-user system like Unix - Linux, etc…

BUT, if you’re going to have groups – groups that have some sort of groupwise permission across different servers then you will need to know what umask is so you can configure the shell environment of the users to set their umask correctly.

I left out one more thing about permissions. That x bit. It means different things depending on the kind of file it is.

Let’s get another thing out of the way. Everything in Unix. EVERYTHING. Everything is a file, in Unix. Some files are special, and we call them (you guessed it) special files. Some files are not special, and we just call them files.

A directory is a file.
A file is a file.
A socket is a file.
“stdout”, “stdin”, and “stderr” all are files.
The /dev/sda1 device you see in /dev/ is a file.
Even /proc is a file
Everything is a file.

And since everything is a file, you can perform C.R.U.D. (create, read, update, delete) on them.

So, the special file that we call “a directory” is a file that uses the x bit to allow the user involved to search the directory.

There’s nuance here.

Let’s say the file was this:

foo@skynet:~/ff$ mkdir baz
foo@skynet:~/ff$ ls -ld baz
dr-xr-xr-x 2 foo bar 4096 Jan  3 21:02 baz

The directory has mode 555

The directory is named baz. It’s owned by user foo. It’s group-owned by bar We can check WHICH groups the user foo is in just to make sure this setup is what we want:

$ groups foo
foo : bar

The r-x permission under u (the first of the three) means that foo can read and search the directory. By search we mean to descend into the directory. The group-owner bar also has r-x. That means ANY user in group bar can also, likewise read and search the directory. And to top it off, r-x is also applied to OTHER – so if the user isn’t the owner, and isn’t the group-owner, they can read and search. Pretty much open season – anyone can see anything IN that directory itself.

No users (not even the owner) can write to the directory though. That’s not really cool or what we want…

If we want to make sure only the owner (foo) and the group-owner (bar) can read and search it, then we need to change the mode. We can change the mode like this:

$ chmod 550 baz
$ ls -ld baz
foo@skynet:~/ff$ ls -ld baz
dr-xr-x--- 2 foo bar 4096 Jan  3 21:02 baz

Now only the owner foo and the group-owner bar can read and search it. Other users cannot. And NO ONE not even the owner can write files in the directory.

Let’s fix that bit

$ chmod 750 baz
$ ls -ld baz
foo@skynet:~/ff$ ls -ld baz
drwxr-x--- 2 foo baz 4096 Jan  3 21:02 baz

Much better. foo user can write files all day long, search it, read it, etc… Group owner bar has a small part to play. They can just inspect (read and search), but cannot make files IN the directory. Other users – shut out completely.

So, going all the way now back to the first question – should you make a user? YES.
And when you make the directory to HOLD the server data, what permission mode are you going to make it?
Let’s say the server is called: DarkerRealms and that is where the minecraft.properties file lives, etc…

# addgroup realms
# adduser --ingroup realms darker
# chown -R darker.realms /data/servers/DarkerRealms
# chgrp realms /data/servers
# su darker
darker$ cat >> ~/.bashrc <<EOF
umask 0002
EOF
darker$ cd /data/servers/DarkerRealms
darker$ chmod 750 .
darker$ exit
# exit

Then setup your server there.

Etc…

See a command I didn’t talk about:

$ man COMMAND

More later on this because your other questions also affect the decisions, let’s go!

d. Should I use cron to restart the servers ?

Yes and no. Cron is the best way to schedule things the simplest way. But it can also bite you in the ass. You need to write a shell script that is invoked by cron and IN the shell script you test things to decide whether or not to start the server or not. But cron itself should never actually run java -jar ... to start the server.

Best course of action to take:

You mentioned screen(1), right? So you need to work out a shell script that if run will check:

  1. Is the screen running?
  2. If it is, what’s the state of the server? (crash log – more grep and other shell script work)
  3. If it is running and not crashed, do you want it killed? stopped?
  4. If it is not running, do you want it started?
  5. Is your dog happy?
  6. Are these too many questions?

Yes. There’s a better way, IMHO, and that’s to involve both screen and expect.
If you don’t like expect then you can probably get by with just a shell script that will kill off the server, and re-start it. I don’t like that method. I’d prefer expect so I can actually send the stop command to the server via the screen. It’s up to you and I am running out of gas in this long post to work out the script. Maybe tomorrow when I am rested. PING ME later. I’ve used expect to communicate with the server console to issue a large number of commands in order (like setup permission nodes) from a shell script. It’s doable, but you need to do it carefully. I’ll look around for my sample.

But the nuts and bolts of the thing is – if you use cron to setup jobs to run, then you’ll need to organize a shell script that governs what happens when the java process exits (due to a crash or stoppage) – leaves a breadcrumb file to be checked by the cron-job and then based on the data there, decide if it should re-fire up the screen again and so on.

My advice is to work it out in shell scripts that run manually. Simulate the server stopping and inspect that your script systems are properly picking up the crumbs to determine what to do next. Then wrap it up so that your cron-job is merely checking the state and leaving new crumbs behind based on that analysis. Let the script that spawned the screen do the “infinite loop” that can exit only based on the heuristic you’ve designed by the way the scripts leave data behind. Use the filesystem as your data-storage to leave behind information on the next iteration of the loop that will kick off the server again.

e. Should I create an unix service called “minecraft” ? (I don’t why, but I’ve seen it somewhere)

No. No need for that. And, by the way, there is no such thing as a ‘unix service’. It’s called a process and if the process has lost it’s controlling tty, we call that a daemon. See also: “what is a daemon, zombie and orphan” in Unix.

f. Should I use Docker ? (I don’t know how)

I will risk the hate mail. No. Do not use Docker.

You don’t need Docker for this. But if you want to learn how to use Docker to do this, go ahead. I would caution you that the details of bridging between the non-virtual image (the Docker image) and the real system are not trivial. But I merely use docker time to time when I am forced to, not voluntarily.

You asked a lot of good questions and it drew out a very long response only because

  1. You mentioned Unix and that’s my thing.
  2. You asked questions seldom asked.

Good luck and keep me aware of your progress.

5 Likes

Thanks you for taking the time to respond! Your post is amazing! :wink:
Despite there were few bits I already known about Unix, I learned a lot, even more than I expected when writing my questions. In addition to the detailed answers, you mentioned conventions and that’s exactly what I wanted to know. Your post is gold nugget for me and probably for a lot of server owners. I would have loved to have it 4 years earlier when I started hosting my servers on VPSs.

I understand the idea of adding a user for each server and why it’s cleaner. But, what about a (minigames) network? I mean a proxy + many servers behind. At the beginning it will be a small fixed amount of servers, but later I might plan to have more server or especially a dynamic amount of servers. The proxy might create, start, stop servers depending on the amount of connected players. Of course, that’s an idea, I don’t know if it will ever happen, but in this case: Should I still keep the idea of a user per server? Or would a single user for all the servers of the network be sufficient?

I will try everything you said as soon as I can, and keep you updated! Again, thanks a lot! :smile:

That’s awesome! Thank you both @Yeregorix for asking the question and @sibomots for answering them in such detail!
I have a similar situation like Yeregorix with my server (using screen, running from one account) and i always wondered how to manage everything using actual users and groups and doing it the right way :smiley:
Never wondered enough to ask though ^^
So this here was great! Learned a lot!
<3

1 Like

I’ve had a rest so I can expand a bit on the nature of the question in better detail.

It doesn’t matter what exactly the process is, it should if it’s a well behaved process emit a stream of bytes to stdout and stderr, as well as open files and write data to them in the course of running the process.

In this case, the two files of interest are stdout (file descriptor 1) and of course the one or more log files that are written by the process. In this case the debug.log and other logs that are written to the Minecraft ./logs folder.

But the interaction your script will have is via the stdin and stdout file descriptors.

So, first write a script that invokes java -jar whatever.jar This is the invocation of the Minecraft server. And because we want to be able to get to the console manually later if for any reason we want to use screen. What screen does is maintain a controlling tty for the process that was spawned. The shell (parent process) of the program (java program) has controlling access to the program in basic terms. For instance, when you
press Ctrl-C what happens? The SIGINT signal is sent to the child. The child is the java process. You can simulate this from another shell. Open another shell
along side the shell that has run java. Then type: ps -ux

You’ll see the list of processes that are running by the current user.

The PID (process ID) will be listed.

On my terminal:

foo@skynet:~/server$ ps -eo user,pid,ppid,args | grep foo
root     31255  4138 sshd: foo [priv]
foo      31257     1 /lib/systemd/systemd --user
foo      31258 31257 (sd-pam)
foo      31330 31255 sshd: foo@pts/1
foo      31331 31330 -bash
root     31406  4138 sshd: foo[priv]
foo      31434 31406 sshd: foo@pts/3
foo      31435 31434 -bash
foo      31449 31331 java -jar forge-1.12.2-14.23.5.2838-universal.jar
foo      31640 31435 ps -eo user,pid,ppid,args
foo      31641 31435 grep --color=auto foo

The first column after the user foo is the PID. The next column is PPID (parent process ID)
See PID of 31449 ? That’s the PID of the java process… Who is the parent? 31331. And what is that process? It’s bash. Who’s the parent of that? The sshd daemon (which is understandable since I’m ssh'd into the server and so it goes).

From the other shell, invoke the kill command. Remember always that kill does not mean kill. It means to send a signal. We’re going to use kill to send a signal to a process.

$ kill -1 PID

Where PID is the process ID of the java process. Signal 1 is the numeric value of
SIGHUP. But wait, I said SIGINT. That is true. SIGINT is the signal sent to a process when Ctrl-C is issued in the foreground when a process is running. We’re going to simulate a different case. Suppose the window that was running the shell was closed. Think of it this way: The xterm window in which the bash shell was running (the xterm process is the parent of the bash shell process). Now the java process is the child of the bash process. If we stop the xterm, we’re effectively causing the signall SIGHUP to be sent to the children. SIGHUP is signal for “hang up”. It goes back to the age when dial-up terminals and other pedantic connection schemes were required. Pull the phone cord out, that’s a SIGHUP. It’s used still more than you may know. For instance, let’s say you’re runing a daemon and you want the daemon to re-load configuration files. The expected behavior of a daemon is to handle the SIGHUP signal – reload config files, re-initialize, and KEEP running. That’s why you may have seen things like $ kill -1 PID to cause a daemon to “reload” or whatever reload means for that daemon.

Here instead we’re going to see what Minecraft does in response to a SIGHUP for demonstration.

I’m going to simulate the event of the controlling TTY going away:

$ kill -1 

In the window where the java process was running, it actually stopped the server:

[12:21:44] [Server Shutdown Thread/INFO] [minecraft/MinecraftServer]: Stopping server

That’s actually a good sign. It means that the java process went through a semi-graceful shutdown. It was not as if the power was pulled. The process actually recognized what happend and we can only look at the other logs and behavior to see if more graceful shutdown processing occurred. It’'s not pretty, but it’s better than a power-pull.

Now, let’s simulate a signal that is not maskable. A masked signal is one that can be handled by a process in Unix. A non-maskable signal cannot be ‘caught’ or ‘handled’ by a process. That’s the SIGKILL signal.

To get the names-to-numbers for these signals you can do this by reading the header file for signal.h OR better just use the kill command itself:

foo@skynet:~/server$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

SIGHUP is 1. SIGINT is 2 (Ctrl-C), and SIGKILL is 9 If you really want to kill something off, use -9 but it will not be graceful. Some programs in Unix will not always “stop” upon a -9. For example in the days we used tape-drives to store data, if the command that was running the tape-drive was killed with -9 it would not actually die all the time since the program was being hung by a failure in hardware (the tape-drive itself). But for most non-peripheral controlling commands, the -9 will usually kill it off and not gracefully.

So as you can see there’s one piece of information you need when a process starts if you want to have “the process by the collar”. That’s the PID

When the process is started, we want the PID. Assume you started the java process with screen:

$ screen -dmS TEST java -jar forge-1.12.2-14.23.5.2838-universal.jar
$ screen -ls
There is a screen on:
        31745.TEST      (01/04/2020 12:34:07 PM)        (Detached)
1 Socket in /var/run/screen/S-foo.
$ ps --ppid 31745 --no-headers -o pid

I started the server with screen. The tty detatched. I got the shell prompt. The screen daemon is running. The java process is running too, as a background process. I got the PID of the screen session via screen -ls Then, I used the PID of the session to get the child PID of the screen session – the PID of the java process via ps -ppid 31745 --no-headers -o pid.

Now I know the PID of the minecraft server that’s running under the guise of the screen session called TEST.

I got the minecraft server process by the collar now. I have it anyway since I have it boxed in by the screen session.

We can echo this to a breadcrumb file so we can access things later via cron

Let’s make sure we have full control and can detect the output that marks known steps in the life of the server as it runs. In this test, we’re going to use expect to spawn
the server and then detect when the minecraft process is at it’s most ready point.

I’m making an assumption that the string in question that I’m looking for is the marker that minecraft/forge will emit when the server is ready.

set timeout -1
spawn java -jar forge-1.12.2-14.23.5.2838-universal.jar

expect -ex {[minecraft/DedicatedServer]: Done}
puts "\n\nSAW SERVER START\n\n"
send "help\r"
puts "\n\nSEND COMMAND\n\n"
expect  -ex {[minecraft/DedicatedServer]: ====}
puts "\n\nRECEIVED RESPONSE TO COMMAND\n\n"

A walk through of this expect script. First how to run it:

$ expect foo.ex

When it runs, it spawns a process java ....
It’s similar to the way screen spawns a process. But it is not a screen session.
It’s in the background. But, expect has it by the collar.

Now, expect will emit to stdout all the data that is sent to stdout by the spawned process. If java ... prints it, expect will emit this to stdout. Plus, it’s going to pass through the filter of expect. We can look for patterns in the output.

The first pattern we are looking for is the message that indicates that the Minecraft process has reached a near-idle point. After some testing, I found that the message that is clearly that point is this:

[13:13:46] [Server thread/INFO] [minecraft/DedicatedServer]: Done (4.464s)! For help, type "help" or "?"
>

But that contains a bit more information that we want to match on. Let’s just look for the tell-tale marker [minecraft/DedicatedServer]: Done We don’t want to parse the time, and we don’t care really about how long it took to start, nor do we want to regex parse the message with the variable number of seconds, etc.

[minecraft/DedicatedServer]: Done

is enough.

When expect parses that string, then it will continue the flow of the script to emit a debug message to stdout. The server never sees this. We’re not sending the string
to the server. We’re sending it to the stdout of the shell that invoked the expect script:

puts "\n\nSAW SERVER START\n\n"

Then, after that we’ll send a string to the process that was spawned. We’re going to send the server the help command (just as if you had typed it at the console).

send "help\r"

the \r is the equivalent to the carriage return.

As expected (heh, pun not intended), the output from the server (again on stdout) will be a page of help-info.

After testing, I saw that there was a definite syntax and pattern to the way the help info was emitted. There was a pattern to the end of a help-page. So, I am looking for that data:

expect  -ex {[minecraft/DedicatedServer]: ====}

That appears when the help-screen page is done. I could use the regex feature of expect to detect which page and capture the number of pages and send more commands back to the server to get all of the help pages, and so on. But that’s not the goal.

The point is, you can now detect when the server emits a message that indicates a fault that you would want to handle by restarting the server.

Since expect can detect this, and since you already have the bash shell tools above to find out the screen session and PID and so on, you can probably construct a shell script scheme that will:

  1. Check if Minecraft is running.
  2. If it is running, does the PID match the PID of the screen session.
    If not, then take action.
    If so, then things appear nominal.
  3. If it is not running, and the breadcrumbs are present, then reset the crumbs and start the server as nominal.
  4. If Minecraft is running and there is a breadcrumb left by expect that the server crashed, then clean up, restart the server as nominal

You can orchestrate the action/response system via your shell script and expect script to your own whim.

But start simple and start it in the actual shell. Don’t migrate anything to cron until you can demonstrate that your shell scripts running in the foreground operate correctly.

I can see you doing a fair bit of reading up on bash scripting, testing data in files/variables, and designing a breadcrumb/cleanup sequence that is going to prevent race-conditions and duplicate spawning, and so on.

That should get u started.

2 Likes