Skip to content

Commit

Permalink
argparse all the things!
Browse files Browse the repository at this point in the history
add a real shell argument parser, add --instance arg, documentation, refactoring, cleanup
  • Loading branch information
fuhry committed Jun 8, 2022
1 parent f9c55cc commit 88a9376
Show file tree
Hide file tree
Showing 7 changed files with 388 additions and 103 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ minecraftd-backup.service
minecraftd-backup.timer
[email protected]
[email protected]
[email protected]
minecraftd.conf
minecraftd.service
minecraftd.sh
Expand Down
27 changes: 18 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
SHELL = /bin/sh
INSTALL = install
INSTALL_PROGRAM = $(INSTALL) -m755
INSTALL_DIR = $(INSTALL) -d -m755
INSTALL_DATA = $(INSTALL) -m644
confdir = /etc/conf.d
prefix = /usr
Expand All @@ -20,10 +21,13 @@ CONFIG_PATH = /etc/conf.d/$(GAME)
SYSCONFDIR = /etc
INSTANCE_CONFIG_DIR = $(SYSCONFDIR)/$(GAME)
BACKUP_DEST = $(SERVER_ROOT)/backup
LIBRARY_PATH = $(libdir)/$(GAME)
BACKUP_PATHS = world
BACKUP_FLAGS = -z
KEEP_BACKUPS = 10
GAME_USER = $(GAME)
SERVER_MEMORY_INITIAL = 512
SERVER_MEMORY_MAXIMUM = 1024
MAIN_EXECUTABLE = minecraft_server.jar
ifeq ($(MAIN_EXECUTABLE),$(MAIN_EXECUTABLE:/%=))
MAIN_EXECUTABLE_ABSOLUTE = $(SERVER_ROOT)/$(MAIN_EXECUTABLE)
Expand All @@ -32,14 +36,14 @@ MAIN_EXECUTABLE_ABSOLUTE = $(MAIN_EXECUTABLE)
endif
TMUX_SOCKET_DIR = /run/$(GAME)/tmux
SESSION_NAME = $(GAME)
SERVER_START_CMD = java -Xms512M -Xmx1024M -jar ./$${MAIN_EXECUTABLE} nogui
SERVER_START_CMD = java -Xms@SERVER_MEMORY_INITIAL@M -Xmx@SERVER_MEMORY_MAXIMUM@M -jar @MAIN_EXECUTABLE@ nogui
SERVER_START_SUCCESS = done
IDLE_SERVER = false
IDLE_SESSION_NAME = idle_server_$${SESSION_NAME}
IDLE_SESSION_NAME = idle_server_@SESSION_NAME@
GAME_PORT = 25565
CHECK_PLAYER_TIME = 30
IDLE_IF_TIME = 1200
GAME_COMMAND_DUMP = /tmp/$${INAME}_$${SESSION_NAME}_command_dump.txt
GAME_COMMAND_DUMP = /tmp/@INAME@_@SESSION_NAME@_command_dump.txt

.MAIN = all

Expand All @@ -50,6 +54,7 @@ define replace_all
-e 's#@GAME@#$(GAME)#g' \
-e 's#@SERVER_ROOT@#$(SERVER_ROOT)#g' \
-e 's#@CONFIG_PATH@#$(CONFIG_PATH)#g' \
-e 's#@LIBRARY_PATH@#$(LIBRARY_PATH)#g' \
-e 's#@SYSCONFDIR@#$(SYSCONFDIR)#g' \
-e 's#@INSTANCE_CONFIG_DIR@#$(INSTANCE_CONFIG_DIR)#g' \
-e 's#@BACKUP_DEST@#$(BACKUP_DEST)#g' \
Expand All @@ -59,6 +64,8 @@ define replace_all
-e 's#@GAME_USER@#$(GAME_USER)#g' \
-e 's#@MAIN_EXECUTABLE@#$(MAIN_EXECUTABLE)#g' \
-e 's#@MAIN_EXECUTABLE_ABSOLUTE@#$(MAIN_EXECUTABLE_ABSOLUTE)#g' \
-e 's#@SERVER_MEMORY_INITIAL@#$(SERVER_MEMORY_INITIAL)#g' \
-e 's#@SERVER_MEMORY_MAXIMUM@#$(SERVER_MEMORY_MAXIMUM)#g' \
-e 's#@TMUX_SOCKET_DIR@#$(TMUX_SOCKET_DIR)#g' \
-e 's#@SESSION_NAME@#$(SESSION_NAME)#g' \
-e 's#@SERVER_START_CMD@#$(SERVER_START_CMD)#g' \
Expand Down Expand Up @@ -102,15 +109,17 @@ maintainer-clean: clean

install:
$(INSTALL_PROGRAM) -D minecraftd.sh "$(DESTDIR)$(bindir)/$(INAME)"
$(INSTALL_DATA) -D minecraftd.conf "$(DESTDIR)$(confdir)/$(GAME)"
$(INSTALL_DATA) -D minecraftd.service "$(DESTDIR)$(libdir)/systemd/system/$(INAME).service"
$(INSTALL_DATA) -D minecraftd-backup.service "$(DESTDIR)$(libdir)/systemd/system/$(INAME)-backup.service"
$(INSTALL_DATA) -D minecraftd-backup.timer "$(DESTDIR)$(libdir)/systemd/system/$(INAME)-backup.timer"
$(INSTALL_DIR) "$(DESTDIR)$(LIBRARY_PATH)"
$(INSTALL_DATA) -D argparse.sh "$(DESTDIR)$(LIBRARY_PATH)/argparse.sh"
$(INSTALL_DATA) -D minecraftd.conf "$(DESTDIR)$(confdir)/$(GAME)"
$(INSTALL_DATA) -D minecraftd.service "$(DESTDIR)$(libdir)/systemd/system/$(INAME).service"
$(INSTALL_DATA) -D minecraftd-backup.service "$(DESTDIR)$(libdir)/systemd/system/$(INAME)-backup.service"
$(INSTALL_DATA) -D minecraftd-backup.timer "$(DESTDIR)$(libdir)/systemd/system/$(INAME)-backup.timer"
$(INSTALL_DATA) -D [email protected] "$(DESTDIR)$(libdir)/systemd/system/$(INAME)@.service"
$(INSTALL_DATA) -D [email protected] "$(DESTDIR)$(libdir)/systemd/system/$(INAME)[email protected]"
$(INSTALL_DATA) -D [email protected] "$(DESTDIR)$(libdir)/systemd/system/$(INAME)[email protected]"
$(INSTALL_DATA) -D minecraftd.sysusers "$(DESTDIR)$(libdir)/sysusers.d/$(INAME).conf"
$(INSTALL_DATA) -D minecraftd.tmpfiles "$(DESTDIR)$(libdir)/tmpfiles.d/$(INAME).conf"
$(INSTALL_DATA) -D minecraftd.sysusers "$(DESTDIR)$(libdir)/sysusers.d/$(INAME).conf"
$(INSTALL_DATA) -D minecraftd.tmpfiles "$(DESTDIR)$(libdir)/tmpfiles.d/$(INAME).conf"

uninstall:
rm -f "$(bindir)/$(INAME)"
Expand Down
36 changes: 12 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@ Use the minecraft script under /usr/bin/minecraftd to start, stop or backup the

### How to configure the Server

Adjust the configuration file under /etc/conf.d/minecraft to your liking.
Adjust the configuration file under `/etc/conf.d/minecraft` to your liking.

If you are running multiple servers, the configuration file `/etc/minecraft/<instance name>` will be loaded if it exists, and will supersede any options set in the global configuration.

Any configuration variable can be overridden in the environment.

To see the effective configuration based on the instance name and all applicable configuration files, run `minecraftd print`, `minecraftd -i server2 print`, etc. To print the plain value of a single configuration key, use the `-k` argument: `minecraftd print -k SERVER_ROOT`.

### Server does not start

Expand All @@ -83,31 +89,13 @@ systemctl start minecraftd@server-name # (it should start this time)

To facilitate the use of a unique environment variable file per instance, the file `/etc/minecraft/<instance name>` is read in addition to the global environment file, `/etc/conf.d/minecraft`. Note that `minecraft` is the value of the `@GAME@` macro in this case, so change this to `/etc/spigot/<instance name>`, etc. Values in the instance-specific environment file take precedence over values in the global configuration.

Behind the scenes, the systemd instantiated unit achieves instantiation by changing several environment variables as follows (where `%i` is the instance name, and `@SERVER_ROOT` defaults to `/srv/@GAME@`):

* `SERVER_ROOT=@SERVER_ROOT@/servers/%i`
* `BACKUP_DEST=@SERVER_ROOT@/servers/%i/backup`
* `SESSION_NAME=@GAME@-%i`

This means that attempting to override these environment variables in the per-instance environment file may result in the server not starting or files being in the wrong location.

If you need to start an instantiated server manually for some reason, you can do:

```sh
env SERVER_ROOT=/srv/minecraft/servers/creative \
SESSION_NAME=minecraft-creative \
minecraftd start
```

To attach to the console, just override the `SESSION_NAME` variable with `minecraft-` followed by the instance name:

```sh
$ SESSION_NAME=minecraft-creative minecraftd console
```
The configuration variables whose defaults change in instantiated mode are:

In the above example, `minecraft` is the value of the `@GAME@` macro, so you might need `spigot-creative`, `papermc-creative`, etc.
* `SERVER_ROOT` changes from `/srv/minecraft` to `/srv/minecraft/servers/<instance name>`
* `SESSION_NAME` changes from `minecraft` to `minecraft-<instance name>`
* `BACKUP_DEST` changes from `/srv/minecraft/backup` to `/srv/minecraft/servers/<instance name>/backup`

To take a backup, use instantiated variants of the service and timer, or set `BACKUP_DEST` alongside `SERVER_ROOT` and `SESSION_NAME` when running `minecraftd backup`.
These can also be overridden in the instance-specific configuration file, `/etc/minecraft/<instance name>`.

## License

Expand Down
189 changes: 189 additions & 0 deletions argparse.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#!/bin/bash

# shell script library to parse and validate command line arguments, and generate the --help text.

set -u

re_subcmd_vn='^[a-z0-9]+:[A-Za-z0-9_]+$'
COMMAND=

declare -A args args_help subcmds

add_arg() {
local short="$1"
local long="$2"
local varname="$3"
local help="$4"
local required="${5:-false}"
[ -v 6 ] && local default="$6"
if [[ "$varname" =~ $re_subcmd_vn ]]; then
local subc _vn
IFS=":" read subc _vn <<< "$varname"
[[ -n "${subcmds[$subc]}" ]]
fi
args[$varname]="$short $long $required"
args_help[$varname]="$help"
# init the global
[[ -v default ]] && declare -g ${varname#*:}="${default}"
}

add_subcommand() {
local subc="$1"
local help="$2"

subcmds[$subc]="$help"
}

usage() {
# help function. generates usage instructions
local short long required help
local n_subcmds=${#subcmds[@]}
local ofd=1
[[ -v 1 ]] && ofd=2
local cols=80
[[ -t $ofd ]] && cols=$(tput cols)
(
if [[ -v 1 ]]; then
echo "ERROR: $1"
echo ""
fi
if [[ -v DESCRIPTION ]]; then
echo -e "$DESCRIPTION"
echo ""
fi
if [[ $n_subcmds < 1 ]]; then
echo "Usage: $0 [options]"
echo ""
echo "Valid options are:"
else
echo "Usage: $0 [global options] COMMAND [command-specific options]"
echo ""
echo "Global options:"
fi
for varname in ${!args[@]}; do
[[ $varname =~ $re_subcmd_vn ]] && continue
IFS=" " read short long required <<< "${args[$varname]}"
help="${args_help[$varname]}"
printf " -%s, --%-16s %s (required: %s)\n" "$short" "$long" "$help" "$required"
done

if [[ $n_subcmds > 0 ]]; then
echo ""
echo "Valid commands:"
for subcmd in ${!subcmds[@]}; do
printf " %-12s %s\n" "$subcmd" "${subcmds[$subcmd]}"
done

for subcmd in ${!subcmds[@]}; do
echo ""
echo "Options for command \"$subcmd\":"
local n=0
for varname in ${!args[@]}; do
[[ "${varname%:*}" == "$subcmd" ]] || continue
((n++))
IFS=" " read short long required <<< "${args[$varname]}"
help="${args_help[$varname]}"
printf " -%s, --%-16s %s (required: %s)\n" "$short" "$long" "$help" "$required"
done
[[ $n == 0 ]] && echo " (None)"
done
echo ""

[[ -v COPYRIGHT ]] && echo -e "$COPYRIGHT"
fi
) | fold -w "$cols" -s >&$ofd
exit 1
}

parse_args() {
local short long required longprefix found
local n_subcmds=${#subcmds[@]}
declare -a bareargs reqargs
while [[ -v 1 ]]; do
case "$1" in
--help|-h)
usage ;;
-*)
# parse as named option
found=false
for varname in ${!args[@]}; do
IFS=" " read short long required <<< "${args[$varname]}"
if [[ $varname =~ $re_subcmd_vn ]]; then
# subcommand option
local vsubc _vn
IFS=":" read vsubc _vn <<< "$varname"
[[ "$vsubc" = "$COMMAND" ]] || continue
varname=$_vn
fi
longprefix="--$long="
case "$1" in
-$short|--$long)
[[ -v 2 ]] || usage "value for argument $1 may not be omitted"
declare -g $varname="$2"
found=true
shift
;;
$longprefix*)
found=true
declare -g $varname=${1:${#longprefix}}
;;
esac
done
[[ "$found" == "true" ]] || usage "Unknown option: $1"
;;
*)
if [[ $n_subcmds > 0 && $COMMAND == "" ]]; then
COMMAND="$1"
[[ -v subcmds[$COMMAND] ]] || usage "Undefined command: $COMMAND"
else
bareargs+=("$1")
fi
;;
esac
shift
done

[[ $n_subcmds > 0 && $COMMAND == "" ]] && usage "No command was given."

# build list of missing required args
for varname in ${!args[@]}; do
IFS=" " read short long required <<< "${args[$varname]}"
[[ "$required" == "true" ]] || continue
if [[ $varname =~ $re_subcmd_vn ]]; then
# subcommand option
local vsubc _vn
IFS=":" read vsubc _vn <<< "$varname"
[[ "$vsubc" = "$COMMAND" ]] || continue
[[ -v $_vn ]] && continue
reqargs+=("$varname")
else
[[ -v $varname ]] && continue
reqargs+=("$varname")
fi
done
# process bareword args
while [[ -v bareargs[0] && -v reqargs[0] ]]; do
declare -g ${reqargs[0]#*:}=${bareargs[0]}
bareargs=("${bareargs[@]:1}")
reqargs=("${reqargs[@]:1}")
done

# if there's any bareword arguments left, we are out of ideas for what to do with them, so fail.
[[ -v bareargs[0] ]] && usage "Unknown argument: ${bareargs[0]}"

# enforce required args
for varname in ${!args[@]}; do
IFS=" " read short long required <<< "${args[$varname]}"
[[ "$required" == "true" ]] || continue
if [[ $varname =~ $re_subcmd_vn ]]; then
# subcommand arg
local vsubc _vn
IFS=":" read vsubc _vn <<< "$varname"
[[ "$vsubc" == "$COMMAND" ]] || continue
[[ ! -v $_vn ]] && usage "Option is required for command $vsubc but not set: $long"
else
# global arg
[[ ! -v $varname ]] && usage "Required option or positional argument not set: $long"
fi
done
}
12 changes: 12 additions & 0 deletions [email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[Unit]
Description=Create server directories for @GAME@ %i instance
After=local-fs.target network.target multi-user.target

[Service]
Type=oneshot
RemainAfterExit=no
# create the "servers" directory followed by the instance directory
ExecStart=/usr/bin/@INAME@ -i %i init

[Install]
WantedBy=multi-user.target
Loading

0 comments on commit 88a9376

Please sign in to comment.