diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..ddb3e480 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,4 @@ +changelog: + exclude: + authors: + - dependabot diff --git a/Dockerfile b/Dockerfile index b348c08f..856aa60f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,7 +76,8 @@ ENV UID=1000 GID=1000 \ TYPE=VANILLA VERSION=LATEST \ ENABLE_RCON=true RCON_PORT=25575 RCON_PASSWORD=minecraft \ ENABLE_AUTOPAUSE=false AUTOPAUSE_TIMEOUT_EST=3600 AUTOPAUSE_TIMEOUT_KN=120 AUTOPAUSE_TIMEOUT_INIT=600 \ - AUTOPAUSE_PERIOD=10 AUTOPAUSE_KNOCK_INTERFACE=eth0 + AUTOPAUSE_PERIOD=10 AUTOPAUSE_KNOCK_INTERFACE=eth0 \ + ENABLE_AUTOSTOP=false AUTOSTOP_TIMEOUT_EST=3600 AUTOSTOP_TIMEOUT_INIT=1800 AUTOSTOP_PERIOD=10 COPY --chmod=755 scripts/start* / COPY --chmod=755 bin/ /usr/local/bin/ @@ -84,8 +85,9 @@ COPY --chmod=755 bin/mc-health /health.sh COPY --chmod=644 files/server.properties /tmp/server.properties COPY --chmod=644 files/log4j2.xml /tmp/log4j2.xml COPY --chmod=755 files/autopause /autopause +COPY --chmod=755 files/autostop /autostop -RUN dos2unix /start* /autopause/* +RUN dos2unix /start* /autopause/* /autostop/* ENTRYPOINT [ "/start" ] HEALTHCHECK --start-period=1m CMD mc-health diff --git a/README.md b/README.md index ad18af7f..c1fe70d6 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ By default, the container will download the latest version of the "vanilla" [Min * [Downloadable world](#downloadable-world) * [Cloning world from a container path](#cloning-world-from-a-container-path) * [Overwrite world on start](#overwrite-world-on-start) + * [Datapacks](#datapacks) * [Server configuration](#server-configuration) * [Message of the Day](#message-of-the-day) * [Difficulty](#difficulty) @@ -131,10 +132,11 @@ By default, the container will download the latest version of the "vanilla" [Min * [Autopause](#autopause) * [Description](#description) * [Enabling Autopause](#enabling-autopause) + * [Autostop](#autostop) * [Running on RaspberryPi](#running-on-raspberrypi) * [Contributing](#contributing) - + @@ -792,6 +794,16 @@ The following diagram shows how this option can be used in a compose deployment ### Overwrite world on start The world will only be downloaded or copied if it doesn't exist already. Set `FORCE_WORLD_COPY=TRUE` to force overwrite the world on every server start. +### Datapacks +Datapacks can be installed in a similar manner to mods/plugins. There are many environment variables which function in the same way they do for [mods](#working-with-mods-and-plugins): +* `DATAPACKS` +* `DATAPACKS_FILE` +* `REMOVE_OLD_DATAPACKS` +* `REMOVE_OLD_DATAPACKS_DEPTH` +* `REMOVE_OLD_DATAPACKS_INCLUDE` +* `REMOVE_OLD_DATAPACKS_EXCLUDE` +Datapacks will be placed in `/data/$LEVEL/datapacks` + ## Server configuration By default, the server configuration will be created and set based on the following environment variables, but only the first time the server is started. If the `server.properties` file already exists, the values in them will not be changed. @@ -1459,6 +1471,28 @@ The following environment variables define the behaviour of auto-pausing: * `AUTOPAUSE_KNOCK_INTERFACE`, default `eth0`
Describes the interface passed to the `knockd` daemon. If the default interface does not work, run the `ifconfig` command inside the container and derive the interface receiving the incoming connection from its output. The passed interface must exist inside the container. Using the loopback interface (`lo`) does likely not yield the desired results. +## Autostop + +An option to stop the server after a specified time has been added for niche applications (e.g. billing saving on AWS Fargate). The function is incompatible with the Autopause functionality, as they basically cancel out each other. + +Note that the docker container variables have to be set accordingly (restart policy set to "no") and that the container has to be manually restarted. + +A starting, example compose file has been provided in [examples/docker-compose-autostop.yml](examples/docker-compose-autostop.yml). + +Enable the Autostop functionality by setting: + +``` +-e ENABLE_AUTOSTOP=TRUE +``` + +The following environment variables define the behaviour of auto-stopping: +* `AUTOSTOP_TIMEOUT_EST`, default `3600` (seconds) + describes the time between the last client disconnect and the stopping of the server (read as timeout established) +* `AUTOSTOP_TIMEOUT_INIT`, default `1800` (seconds) + describes the time between server start and the stopping of the server, when no client connects inbetween (read as timeout initialized) +* `AUTOSTOP_PERIOD`, default `10` (seconds) + describes period of the daemonized state machine, that handles the stopping of the server + ## Running on RaspberryPi To run this image on a RaspberryPi 3 B+, 4, or newer, use any of the image tags [list in the Java version section](#running-minecraft-server-on-different-java-version) that specify `armv7` for the architecture, which includes `itzg/minecraft-server:latest`. diff --git a/bin/mc-send-to-console b/bin/mc-send-to-console index a83f0e81..7615c241 100755 --- a/bin/mc-send-to-console +++ b/bin/mc-send-to-console @@ -12,4 +12,4 @@ if [ ! -p "${CONSOLE_IN_NAMED_PIPE}" ]; then exit 1 fi -echo "$@" > "${CONSOLE_IN_NAMED_PIPE:-/tmp/minecraft-console-in}" \ No newline at end of file +gosu minecraft bash -c "echo $* > '${CONSOLE_IN_NAMED_PIPE:-/tmp/minecraft-console-in}'" \ No newline at end of file diff --git a/examples/docker-compose-autostop.yml b/examples/docker-compose-autostop.yml new file mode 100644 index 00000000..5d45421a --- /dev/null +++ b/examples/docker-compose-autostop.yml @@ -0,0 +1,20 @@ +version: '3.8' + +services: + minecraft: + image: itzg/minecraft-server + ports: + - "25565:25565" + volumes: + - "mc:/data" + environment: + EULA: "TRUE" + ENABLE_AUTOSTOP: "TRUE" + # More aggressive settings for demo purposes + AUTOSTOP_TIMEOUT_INIT: "30" + AUTOSTOP_TIMEOUT_EST: "20" + # Important not to auto-restart the server!!! + restart: "no" + +volumes: + mc: {} diff --git a/files/autostop/autostop-daemon.sh b/files/autostop/autostop-daemon.sh new file mode 100755 index 00000000..0d6aa2ad --- /dev/null +++ b/files/autostop/autostop-daemon.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# needed for the clients connected function residing in autopause +. /autopause/autopause-fcns.sh + +. ${SCRIPTS:-/}start-utils + +# wait for java process to be started +while : +do + if java_process_exists ; then + break + fi + sleep 0.1 +done + +STATE=INIT + +while : +do + case X$STATE in + XINIT) + # Server startup + if mc_server_listening ; then + TIME_THRESH=$(($(current_uptime)+$AUTOSTOP_TIMEOUT_INIT)) + logAutostop "MC Server listening for connections - stopping in $AUTOSTOP_TIMEOUT_INIT seconds" + STATE=II + fi + ;; + XII) + # Initial idle + if java_clients_connected ; then + logAutostop "Client connected - waiting for disconnect" + STATE=E + else + if [[ $(current_uptime) -ge $TIME_THRESH ]] ; then + logAutostop "No client connected since startup - stopping server" + /autostop/stop.sh + exit 0 + fi + fi + ;; + XE) + # Established + if ! java_clients_connected ; then + TIME_THRESH=$(($(current_uptime)+$AUTOSTOP_TIMEOUT_EST)) + logAutostop "All clients disconnected - stopping in $AUTOSTOP_TIMEOUT_EST seconds" + STATE=I + fi + ;; + XI) + # Idle + if java_clients_connected ; then + logAutostop "Client reconnected - waiting for disconnect" + STATE=E + else + if [[ $(current_uptime) -ge $TIME_THRESH ]] ; then + logAutostop "No client reconnected - stopping" + /autostop/stop.sh + exit 0 + fi + fi + ;; + *) + logAutostop "Error: invalid state: $STATE" + ;; + esac + sleep $AUTOSTOP_PERIOD +done diff --git a/files/autostop/stop.sh b/files/autostop/stop.sh new file mode 100755 index 00000000..1a9ed2f4 --- /dev/null +++ b/files/autostop/stop.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +. /start-utils + +logAutostopAction "Stopping Java process" +kill -SIGTERM 1 diff --git a/scripts/start b/scripts/start index 9e252956..95c2736e 100755 --- a/scripts/start +++ b/scripts/start @@ -40,7 +40,7 @@ if ! isTrue "${SKIP_SUDO:-false}" && [ $(id -u) = 0 ]; then echo 'hosts: files dns' > /etc/nsswitch.conf fi - exec gosu ${runAsUser}:${runAsGroup} ${SCRIPTS:-/}start-configuration $@ + exec gosu ${runAsUser}:${runAsGroup} ${SCRIPTS:-/}start-configuration "$@" else - exec ${SCRIPTS:-/}start-configuration $@ + exec ${SCRIPTS:-/}start-configuration "$@" fi diff --git a/scripts/start-autostop b/scripts/start-autostop new file mode 100755 index 00000000..b71bf395 --- /dev/null +++ b/scripts/start-autostop @@ -0,0 +1,34 @@ +#!/bin/bash + +# shellcheck source=start-utils +. "${SCRIPTS:-/}start-utils" + +: "${SERVER_PORT:=25565}" +export SERVER_PORT + +log "Autostop functionality enabled" + +isDebugging && set -x + +if ! [[ $AUTOSTOP_PERIOD =~ ^[0-9]+$ ]]; then + AUTOSTOP_PERIOD=10 + export AUTOSTOP_PERIOD + log "Warning: AUTOSTOP_PERIOD is not numeric, set to 10 (seconds)" +fi +if [ "$AUTOSTOP_PERIOD" -eq "0" ] ; then + AUTOSTOP_PERIOD=10 + export AUTOSTOP_PERIOD + log "Warning: AUTOSTOP_PERIOD must not be 0, set to 10 (seconds)" +fi +if ! [[ $AUTOSTOP_TIMEOUT_EST =~ ^[0-9]+$ ]] ; then + AUTOSTOP_TIMEOUT_EST=3600 + export AUTOSTOP_TIMEOUT_EST + log "Warning: AUTOSTOP_TIMEOUT_EST is not numeric, set to 3600 (seconds)" +fi +if ! [[ $AUTOSTOP_TIMEOUT_INIT =~ ^[0-9]+$ ]] ; then + AUTOSTOP_TIMEOUT_INIT=1800 + export AUTOSTOP_TIMEOUT_INIT + log "Warning: AUTOSTOP_TIMEOUT_INIT is not numeric, set to 1800 (seconds)" +fi + +/autostop/autostop-daemon.sh & diff --git a/scripts/start-configuration b/scripts/start-configuration index 450bea0f..67aba9e4 100755 --- a/scripts/start-configuration +++ b/scripts/start-configuration @@ -37,6 +37,11 @@ if isTrue "${ENABLE_AUTOPAUSE}" && isTrue "${EXEC_DIRECTLY:-false}"; then exit 1 fi +if isTrue "${ENABLE_AUTOPAUSE}" && isTrue "${ENABLE_AUTOSTOP}"; then + log "ENABLE_AUTOPAUSE=true is incompatible with ENABLE_AUTOSTOP=true" + exit 1 +fi + if [[ $PROXY ]]; then export http_proxy="$PROXY" export https_proxy="$PROXY" @@ -96,6 +101,10 @@ if isTrue "${ENABLE_AUTOPAUSE}"; then ${SCRIPTS:-/}start-autopause fi +if isTrue "${ENABLE_AUTOSTOP}"; then + ${SCRIPTS:-/}start-autostop +fi + if versionLessThan 1.7; then echo " MC_HEALTH_EXTRA_ARGS=( diff --git a/scripts/start-setupDatapack b/scripts/start-setupDatapack new file mode 100755 index 00000000..b8134191 --- /dev/null +++ b/scripts/start-setupDatapack @@ -0,0 +1,77 @@ +#!/bin/bash + +set -e -o pipefail + +: "${REMOVE_OLD_DATAPACKS:=false}" +: "${DATAPACKS_FILE:=}" +: "${REMOVE_OLD_DATAPACKS_DEPTH:=1} " +: "${REMOVE_OLD_DATAPACKS_INCLUDE:=*.zip}" + +# shellcheck source=start-utils +. "${SCRIPTS:-/}start-utils" +isDebugging && set -x + +out_dir=/data/${LEVEL:-world}/datapacks + +# Remove old datapacks +if isTrue "${REMOVE_OLD_DATAPACKS}" && [ -z "${DATAPACKS_FILE}" ]; then + if [ -d "$out_dir" ]; then + find "$out_dir" -mindepth 1 -maxdepth ${REMOVE_OLD_DATAPACKS_DEPTH:-16} -wholename "${REMOVE_OLD_DATAPACKS_INCLUDE:-*}" -not -wholename "${REMOVE_OLD_DATAPACKS_EXCLUDE:-}" -delete + fi +fi + +if [[ "$DATAPACKS" ]]; then + mkdir -p "$out_dir" + + for i in ${DATAPACKS//,/ } + do + if isURL "$i"; then + log "Downloading datapack $i ..." + if ! get -o "${out_dir}" "$i"; then + log "ERROR: failed to download from $i into $out_dir" + exit 2 + fi + elif [[ -f "$i" && "$i" =~ .*\.zip ]]; then + log "Copying datapack located at $i ..." + out_file=$(basename "$i") + if ! cp "$i" "${out_dir}/$out_file"; then + log "ERROR: failed to copy from $i into $out_dir" + exit 2 + fi + elif [[ -d "$i" ]]; then + log "Copying datapacks from $i ..." + cp "$i"/*.zip "${out_dir}" + else + log "ERROR Invalid URL or path given in DATAPACKS: $i" + exit 2 + fi + done + +elif [[ "$DATAPACKS_FILE" ]]; then + if [ ! -f "$DATAPACKS_FILE" ]; then + log "ERROR: given DATAPACKS_FILE file does not exist" + exit 2 + fi + + mkdir -p "$out_dir" + + args=( + -o "${out_dir}" + --log-progress-each + --skip-existing + --uris-file "${DATAPACKS_FILE}" + ) + if isTrue "${REMOVE_OLD_DATAPACKS}"; then + args+=( + --prune-others "${REMOVE_OLD_DATAPACKS_INCLUDE}" + --prune-depth "${REMOVE_OLD_DATAPACKS_DEPTH}" + ) + fi + + if ! get "${args[@]}" ; then + log "ERROR: failed to retrieve one or more datapacks" + exit 1 + fi +fi + +exec "${SCRIPTS:-/}start-setupModpack" "$@" diff --git a/scripts/start-setupWorld b/scripts/start-setupWorld index d2803f7c..3484b46d 100755 --- a/scripts/start-setupWorld +++ b/scripts/start-setupWorld @@ -69,4 +69,4 @@ if [[ "$WORLD" ]] && ( isTrue "${FORCE_WORLD_COPY}" || [ ! -d "$worldDest" ] ); fi fi -exec "${SCRIPTS:-/}start-setupModpack" "$@" +exec "${SCRIPTS:-/}start-setupDatapack" "$@" diff --git a/scripts/start-spiget b/scripts/start-spiget index 451312ab..4dfa7575 100755 --- a/scripts/start-spiget +++ b/scripts/start-spiget @@ -22,6 +22,20 @@ containsJars() { return 1 } +containsPlugin() { + file=${1?} + + pat='plugin.yml$' + + while read -r line; do + if [[ $line =~ $pat ]]; then + return 0 + fi + done <<<$(unzip -l "$file") + + return 1 +} + getResourceFromSpiget() { resource=${1?} @@ -81,9 +95,12 @@ downloadResourceFromSpiget() { log "Extracting contents of resource ${resource} into plugins" unzip -o -q -d /data/plugins "${tmpfile}" rm "${tmpfile}" - else + elif containsPlugin "${tmpfile}"; then log "Moving resource ${resource} into plugins" mv "${tmpfile}" "/data/plugins/${resource}.jar" + else + log "ERROR downloaded resource '${resource}' seems to be not a valid plugin" + exit 2 fi } diff --git a/scripts/start-utils b/scripts/start-utils index 1a2ed977..8c77cbca 100755 --- a/scripts/start-utils +++ b/scripts/start-utils @@ -93,6 +93,14 @@ function logAutopauseAction() { echo "[$(date -Iseconds)] [Autopause] $*" } +function logAutostop() { + echo "[Autostop loop] $*" +} + +function logAutostopAction() { + echo "[$(date -Iseconds)] [Autostop] $*" +} + function normalizeMemSize() { local scale=1 case ${1,,} in