#!/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 "$@"