From b566bfc11d4bc37195d6fb7f8c4d238716bee056 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Sat, 25 Dec 2021 21:30:42 -0600 Subject: [PATCH] Added two core files, management and completion from Bash-Scripts --- 7D2D-Manage-Completion.bash | 9 + 7D2D-Manage.bash | 701 ++++++++++++++++++++++++++++++++++++ 2 files changed, 710 insertions(+) create mode 100644 7D2D-Manage-Completion.bash create mode 100644 7D2D-Manage.bash diff --git a/7D2D-Manage-Completion.bash b/7D2D-Manage-Completion.bash new file mode 100644 index 0000000..1c8a9c1 --- /dev/null +++ b/7D2D-Manage-Completion.bash @@ -0,0 +1,9 @@ +#!/bin/bash + +_7D2D_manage_completions() +{ + COMPREPLY=($(compgen -W "backup install kill list start update" "${COMP_WORDS[1]}")) +} + +complete -F _7D2D_manage_completions 7D2D-Manage + diff --git a/7D2D-Manage.bash b/7D2D-Manage.bash new file mode 100644 index 0000000..cae0a0f --- /dev/null +++ b/7D2D-Manage.bash @@ -0,0 +1,701 @@ +#!/bin/bash + + +echo_rgb() { + # Echo a colored string to the terminal based on rgb values + # + # Positional Arguments: + # + # message + # - The message to be printed to stdout + # red + # - The red value from 0 to 255 + # green + # - The green value from 0 to 255 + # blue + # - The blue value from 0 to 255 + # + # Usage: + # echo_rgb "Yep" 10 8 30 + # + # POSIX Compliant: + # N/A + # + + local red + local green + local blue + local input + + input="${1}" + red="${2}" + green="${3}" + blue="${4}" + + printf "\e[0;38;2;%s;%s;%sm%s\e[m\n" "${red}" "${green}" "${blue}" "${input}" +} + +log() { + # Print a message and send it to stdout or stderr depending upon log level, also configurable with debug etc. + # + # Arguments: + # level + # - The log level, defined within a case check in this function + # message + # - The info message + # line_number + # - The line number of the calling function (${LINNO}) + # + # Usage: + # log "info" "Could not find that directory" + # + # POSIX Compliant: + # Yes + # + + # Set debug status depending if a global debug variable has been set to either 1 or 0 + local debug + if [ ${DEBUG} ]; then + debug=${DEBUG} + else + debug=0 + fi + + local FORMAT + FORMAT="[$(echo_rgb "$(date +%Y-%m-%dT%H:%M:%S)" 180 140 255)]" + + # Convert the level to uppercase + local level + level=$(echo "${1}" | tr '[:lower:]' '[:upper:]') + + local message + message="${2}" + + case "${level}" in + INFO) + # Output all info log levels to stdout + printf "${FORMAT}[$(echo_rgb "INFO" 0 140 255)] %s\n" "${message}" >&1 + return 0 + ;; + WARN | WARNING) + # Output all warning log levels to stdout + printf "${FORMAT}[$(echo_rgb "WARNING" 255 255 0)] %s\n" "${message}" >&1 + return 0 + ;; + DEBUG) + # Output all debug log levels to stdout + if [ "${DEBUG}" ]; then + printf "${FORMAT}[$(echo_rgb "DEBUG" 0 160 110)] %s\n" "${message}" >&1 + fi + return 0 + ;; + ERROR) + # Output all error log levels to stderr + printf "${FORMAT}[$(echo_rgb "ERROR" 255 0 0)] %s\n" "${message}" >&2 + return 0 + ;; + # Further log levels can be added by extending this switch statement with more comparisons + + *) # Default case, no matches + # Returns non-zero code as an improper log option was passed, this helps with using `set -e` + printf "${FORMAT}[ERROR] %s\n" "Invalid log level passed, received level \"${level}\" with message \"${message}\"" >&2 + return 1 + ;; + esac +} + +confirmation() { + # Receive confirmation from user as y, Y, n, or N + # returns 0 when answer is yes and 1 when answer is no + # + # Arguments: + # message + # - The confirmation prompt sent to the user, for example: + # Would you like to overwrite foobar.txt (y/N)? + # + # Usage: + # confirmation "Some prompt" + # - Sends "Some prompt" to the user and gets their input + # + # POSIX Compliant: + # Yes + # + + local message + message="${1}" + + local choice + + while true; do + read -p "${message} " -n 1 -r choice + case "$choice" in + y | Y) + echo "" + return 0 + ;; + n | N) + echo "" + return 1 + ;; + *) echo -e "\nInput must be either y, Y, n, or N" ;; + esac + done +} + +### IMPORTS ### + +### CONSTANTS ### +START_PORT_RANGE=50000 +BASE_DIR="${HOME}/7-Days-To-Die" +### CONSTANTS ### + +important() { + echo_rgb "${1}" 135 195 255 +} + +start_server() { + local server_id + local can_kill + + server_id="" + can_kill=1 + + while :; do + case ${1} in + -h | -\? | --help) + printf "Usage: %s\n" \ + "start [options] + --server | -s + Starts the given server id + + Example: + --server 3 + + --can-kill | -c + Automatically kills the server if it is running without prompting + + Example: + --can-kill" + exit + ;; + --) # End of all options. + break + ;; + --server | -s) + shift + server_id="${1}" + [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 + ;; + --can-kill | -c) + can_kill=0 + ;; + -?*) + printf 'Unknown option: %s\n' "$1" >&2 + ;; + *) # Default case: No more options, so break out of the loop. + break ;; + esac + shift + done + + [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 + + local server_directory + local server_config + local server_save_dir + local server_name + local prefix + local server_session_name + + server_name="Server-${server_id}" + server_directory="${BASE_DIR}/${server_name}" + server_config="${server_directory}/serverconfig.xml" + server_save_dir="${server_directory}/Saves/" + server_userdata_dir="${server_directory}/UserData" + prefix="7D2D" + server_session_name="${prefix}-${server_name}" + + [[ ! -d "${server_directory}" ]] && + log "error" "Unable to find the server directory for $(important "${server_name}")" && + return 1 + + backup_configs "${server_directory}" + + + local server_port + server_port="$(( START_PORT_RANGE + server_id ))" + + log "info" "Generating a few required directories in $(important "${server_directory}")" + mkdir -p "${server_save_dir}" + mkdir -p "${server_userdata_dir}" + + log "info" "Configuring serverconfig.xml" + + # Overwrite values that we want to control, e.g. server port + while IFS='' read -r; do + if [[ "${REPLY}" = *"property name=\"ServerPort\""* ]]; then + printf "\t%s\n" "" + + # Override SaveGameFolder, opiniated in that it should exist in the Server Directory + elif [[ "${REPLY}" = *"property name=\"SaveGameFolder\""* ]]; then + printf "\t%s\n" "" + # Override UserDataFolder, opiniated in that it should exist in the Server Directory + elif [[ "${REPLY}" = *"property name=\"UserDataFolder\""* ]]; then + printf "\t%s\n" " "temp-serverconfig.xml" + + # Occasionally the closing tag in serverconfig.xml will be missing, this ensures it is there in that scenario + if [[ ! "$(tail "temp-serverconfig.xml" -n 1 )" = *"ServerSettings"* ]]; then + printf "\n" >> "temp-serverconfig.xml" + fi + + local backups_dir + local backup_file + backups_dir="${server_directory}/Config-Backups" + backup_file="${backups_dir}/serverconfig.xml-$(date +%s)" + mkdir -p "${backups_dir}" + cp "${server_config}" "${backup_file}" + mv "temp-serverconfig.xml" "${server_config}" + + log "info" "Starting $(important "${server_name}") located at $(important "${server_directory}") on port $(important "${server_port}")" + + if tmux has-session -t "${server_session_name}" >/dev/null 2>&1; then + log "warning" "$(important "${server_name}") is currently running" + + if (( can_kill == 0 )); then + log "info" "Been explicitly permitted to kill, bypassing confirmation" + kill_server -s "${server_id}" + else + if confirmation "Would you like to kill $(important "${server_name}")? (Y/n)" ; then + log "info" "Given answer $(important "yes") to kill the server, killing $(important "${server_name}")" + kill_server -s "${server_id}" + else + log "info" "Given answer $(important "no") to kill the server, exiting" + exit 0 + fi + fi + fi + + log "info" "Creating new session for $(important "${server_name}") as session $(important "${server_session_name}")" + tmux new-session -d -s "${server_session_name}" + tmux send-keys \ +"until ${server_directory}/startserver.sh -configfile=${server_directory}/serverconfig.xml; do + echo 'Server died with code $?, restarting in 60 seconds...' >&2 + sleep 60; +done" C-m + log "info" "Finished starting $(important "${server_name}") on port $(important "${server_port}") as tmux session $(important "${server_session_name}")" +} + +kill_server() { + local prefix + local server_id + + server_id="" + prefix="7D2D" + + while :; do + case ${1} in + -h | -\? | --help) + printf "Usage: %s\n" \ + "kill [options] + --server | -s + Forcefully kills the server with the given id + + Example: + --server 3" + exit + ;; + --) # End of all options. + break + ;; + --server | -s) + shift + server_id="${1}" + [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 + ;; + -?*) + printf 'Unknown option: %s\n' "$1" >&2 + ;; + *) # Default case: No more options, so break out of the loop. + break ;; + esac + shift + done + + [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 + + if tmux kill-session -t "${prefix}-Server-${server_id}" >/dev/null 2>&1; then + log "info" "Stopped $(important "${prefix}-Server-${server_id}")" + return 0 + else + log "error" "$(important "${prefix}-Server-${server_id}") is not running" + return 1 + fi +} + +install() { + local server_id + server_id="" + + while :; do + case ${1} in + -h | -\? | --help) + printf "Usage: %s\n" \ + "install [OPTIONS] + --server | -s + Installs the server to the given id if it doesn't exist + + Example: + --server 3" + exit + ;; + --) # End of all options. + break + ;; + --server | -s) + shift + server_id="${1}" + [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 + ;; + -?*) + printf 'Unknown option: %s\n' "$1" >&2 + ;; + *) # Default case: No more options, so break out of the loop. + break ;; + esac + shift + done + + [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 + + local prefix + + prefix="7D2D" + + local server_name + local server_directory + local server_admin_xml + server_name="Server-${server_id}" + server_directory="${BASE_DIR}/${server_name}" + server_admin_xml="${HOME}/.local/share/7DaysToDie/Saves/serveradmin.xml" + + # Ensure that the global mods directory exists + [[ -d "${server_directory}" ]] && + log "error" "A server already exists at $(important "${server_directory}"), delete it and try again" && + exit 1 + + log "info" "Installing $(important "${server_name}") to $(important "${server_directory}")" + steamcmd +force_install_dir "${server_directory}" +login anonymous +app_update 294420 validate +quit + mkdir -p "${server_directory}/Mods" + + log "info" "Running $(important "${server_name}") to get a valid $(important "serveradmin.xml")" + start_server -s "${server_id}" -c + log "info" "Waiting to kill $(important "${server_name}")" + sleep 5 + kill_server -s "${server_id}" + + cp "${server_admin_xml}" "${server_directory}/" + log "info" "Successfully installed $(important "${server_name}") to $(important "${server_directory}")" +} + +update() { + + local server_id + server_id="" + + while :; do + case ${1} in + -h | -\? | --help) + printf "Usage: %s\n" \ +"update [OPTIONS] + --server | -s + Updates the given server id + + Example: + --server 3" + + exit + ;; + --) # End of all options. + break + ;; + --server | -s) + shift + server_id="${1}" + [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 + ;; + -?*) + printf 'Unknown option: %s\n' "$1" >&2 + ;; + *) # Default case: No more options, so break out of the loop. + break ;; + esac + shift + done + + [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 + + # Kill the server to ensure a smooth update + kill_server -s "${server_id}" >/dev/null 2>&1 + + local server_directory + local server_name + local wait_time + server_name="Server-${server_id}" + server_directory="${BASE_DIR}/${server_name}" + wait_time="5" + + log "info" "Recommend running a $(important "backup") operation before running this, waiting $(important "${wait_time}") seconds before continuing..." + sleep "${wait_time}" + + backup_configs "${server_directory}" + log "info" "Updating server $(important "${server_name}") located at $(important "${server_directory}")..." + steamcmd +force_install_dir "${server_directory}" +login anonymous +app_update 294420 validate +quit + + log "info" "Finished updating $(important "${server_name}") located at $(important "${server_directory}")" + +} + +backup_configs() { + local server_directory + local config_backup_directory + + server_directory="${1}" + config_backup_directory="${server_directory}/config-backups/$(date +s)" + log "info" "Backing up $(important ".xml configurations") in $(important "${server_directory}") to $(important "${config_backup_directory}")" + + mkdir -p "${config_backup_directory}" + cp "${server_directory}/"*.xml "${config_backup_directory}/" +} + +backup() { + + local server_id + server_id="" + + while :; do + case ${1} in + -h | -\? | --help) + printf "Usage: %s\n" \ +"backup [OPTIONS] + --server | -s + Backups the given server id + + Example: + --server 3" + + exit + ;; + --) # End of all options. + break + ;; + --server | -s) + shift + server_id="${1}" + [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 + ;; + -?*) + printf 'Unknown option: %s\n' "$1" >&2 + ;; + *) # Default case: No more options, so break out of the loop. + break ;; + esac + shift + done + + [[ -z "${server_id}" ]] && log "error" "No server id passed" && exit 1 + + + # Kill the server to ensure a smooth backup + kill_server -s "${server_id}" >/dev/null 2>&1 + + local backup_dir + local backup_full_path + local server_directory + local server_name + server_name="Server-${server_id}" + server_directory="${BASE_DIR}/${server_name}" + backup_dir="${HOME}/7D2D-Server-Backups/${server_name}" + backup_full_path="${backup_dir}/$(date +%s).tar.gz" + + [[ ! -d "${server_directory}" ]] && log "info" "The server $(important "${server_name}") had no directory located at $(important "${server_directory}")" && exit 1 + + log "info" "Backing up server $(important "${server_name}") to $(important "${backup_full_path}"), this may take a while" + mkdir -p "${backup_dir}" + + # Do a check if pv is there, pv is used for showing progress + if which pv > /dev/null 2>&1; then + tar cf - "${server_directory}" -P 2> /dev/null | pv -s "$(du -sb "${server_directory}" | awk '{print $1}')" | gzip > "${backup_full_path}" + else + log "info" "$(important "pv") not installed, not showing progress..." + tar czf "${server_directory}" "${backup_full_path}" 2> /dev/null + fi +} + +list_servers() { + local picked_option + picked_option="" + while :; do + case ${1} in + -h | -\? | --help) + printf "Usage: %s\n" \ +"list [OPTIONS] + --running | -r + Lists the currently running 7 Days To Die Servers + + Example: + --running + --installed | -i + Lists the currently installed 7 Days To Die Servers + + Example: + --installed" + exit + ;; + --) # End of all options. + break + ;; + --running | -r) + picked_option=0 + ;; + --installed | -i) + picked_option=1 + ;; + -?*) + printf 'Unknown option: %s\n' "$1" >&2 + return 1 + ;; + *) # Default case: No more options, so break out of the loop. + break ;; + esac + shift + done + + [[ -z "${picked_option}" ]] && + log "error" "An option must be passed for list, check list -h" && + return 1 + + if (( picked_option == 0 )); then + log "debug" "Listing running servers" + local tmux_sessions + tmux_sessions="$(tmux list-sessions)" >/dev/null 2>&1 + if [[ ! "${?}" -eq "0" ]]; then + important "No servers currently running." + fi + while read -r; do + if [[ "${REPLY}" = *"-Server-"* ]]; then + local running_server + running_server="$(echo "${REPLY}" | cut -d ":" -f1)" + important "${running_server}" + fi + done <<< "${tmux_sessions}" + elif (( picked_option == 1 )); then + log "debug" "Listing installed servers" + while read -r; do + important "${BASE_DIR}/${REPLY}" + done <<< "$(find "${BASE_DIR}" -name "startserver.sh" | cut -d "/" -f5)" + fi +} + +usage() { + # Print out usage instructions for the local script + # + # Arguments: + # None + # + # Usage: + # usage + # + # POSIX Compliant: + # Yes + # + printf "Usage: %s\n" \ + "$(basename "${0}") -h + start + Exposes options to start 7 Days To Die Servers, pass -h to it for details + kill + Exposes options to kill 7 Days To Die Servers, pass -h to it for details + install + Exposes options to install 7 Days To Die Servers, pass -h to it for details + update + Exposes options to update 7 Days To Die Servers, pass -h to it for details + list + Exposes options to list 7 Days To Die Servers, pass -h to it for details + backup + Exposes options to backup 7 Days To Die Servers, pass -h to it for details" +} + + +main() { + # Parse input arguments + # + # Arguments: + # Consult the `usage` function + # + # Usage: + # parse_args "$@" + # - All arguments should be ingested by parse_args first for variable setting + # + # POSIX Compliant: + # Yes + # + + while :; do + case ${1} in + -h | -\? | --help) + usage # Display a usage synopsis. + exit + ;; + --) # End of all options. + break + ;; + start | s) + shift + start_server "$@" + break + ;; + kill | k) + shift + kill_server "$@" + break + ;; + install | i) + shift + install "$@" + break + ;; + update | u) + shift + update "$@" + break + ;; + list | l) + shift + list_servers "$@" + break + ;; + backup | b) + shift + backup "$@" + break + ;; + -?*) + printf "Unknown option: %s\n" "$1" >&2 + usage + exit 1 + ;; + *) # Default case: No more options, so break out of the loop. + break ;; + esac + shift + done +} + +main "$@"