#!/bin/bash # shellcheck disable=SC2031,SC2155 # ARK: survival evolved manager # # Original author: LeXaT # Original Maintainer: FezVrasta # Current Maintainer: klightspeed # Contributors: Sispheor, Atriusftw, klightspeed, lexat, puseidr, chriscpritchard #disable exportall so options are no exported under Debian et al. set +o allexport # Script version arkstVersion='1.6' ### The following variables are changed by an installer or package manager, and are excluded from git blob hash calculation ### If an override is added, do not put anything after arkstSomeOverride='' in a commit # Set by installer to git tag (i.e. release version) arkstTag='' # Set by installer to git commit arkstCommit='' # Set this if you are a distribution package maintainer otherwise leave empty arkstUsePkgManager='' # Set this to allow variable expansion in variables sourced from arkmanager.cfg arkstRootUseEnv='' # Set this to override the main fork arkstGithubRepoOverride='' # Set this to override the global config file path arkstGlobalCfgFileOverride='' # Set this to override the user config file path arkstUserCfgFileOverride='' ### Do not modify the following settings in an installer or package manager - use the above overrides instead arkstGithubRepo="${arkstGithubRepoOverride:-arkmanager/ark-server-tools}" arkstGlobalCfgFile="${arkstGlobalCfgFileOverride:-/etc/arkmanager/arkmanager.cfg}" arkstUserCfgFile="${arkstUserCfgFileOverride:-.arkmanager.cfg}" arkstScriptPath="${0}" if [[ "${arkstScriptPath}" != /* ]]; then arkstScriptPath="$(realpath "$0")" fi doUpgradeTools() { if [ -n "$arkstUsePkgManager" ]; then echo "arkmanager v${arkstVersion}: Please check for, and install, updates using your system's package manager" else local sudo=sudo if [ "$(id -u)" == 0 ] || [ "$steamcmd_user" == "--me" ]; then sudo= fi local reinstall_args=() if [ -n "$install_bindir" ]; then reinstall_args=( "${reinstall_args[@]}" "--bindir" "$install_bindir" ) fi if [ -n "$install_libexecdir" ]; then reinstall_args=( "${reinstall_args[@]}" "--libexecdir" "$install_libexecdir" ) fi if [ -n "$install_datadir" ]; then reinstall_args=( "${reinstall_args[@]}" "--datadir" "$install_datadir" ) fi echo "arkmanager v${arkstVersion}: Checking for updates..." if [ -n "$arkstUnstable" ] || [ "$arkstChannel" != "master" ]; then doUpgradeToolsFromBranch else doUpgradeToolsFromRelease fi fi } doUpgradeToolsFromCommit(){ local sudo=sudo if [ "$(id -u)" == 0 ] || [ "$steamcmd_user" == "--me" ]; then sudo= fi local commit="$1" tmpdir="$(mktemp -d "ark-server-tools-XXXXXXXX")" if [ -z "$tmpdir" ]; then echo "Unable to create temporary directory"; exit 1; fi cd "$tmpdir" echo "Downloading installer" curl -s -L "https://github.com/${arkstGithubRepo}/archive/${commit}.tar.gz" | tar -xz cd "ark-server-tools-${commit}/tools" if [ ! -f "install.sh" ]; then echo "install.sh not found in $PWD"; exit 1; fi sed -i -e "s|^arkstCommit='.*'|arkstCommit='${commit}'|" \ -e "s|^arkstTag='.*'|arkstTag='${tagname}'|" \ -e "s|^arkstRootUseEnv='.*'|arkstRootUseEnv='${arkstRootUseEnv}'|" \ -e "s|^arkstGithubRepoOverride='.*'|arkstGithubRepoOverride='${arkstGithubRepoOverride}'|" \ -e "s|^arkstGlobalCfgFileOverride='.*'|arkstGlobalCfgFileOverride='${arkstGlobalCfgFileOverride}'|" \ -e "s|^arkstUserCfgFileOverride='.*'|arkstUserCfgFileOverride='${arkstUserCfgFileOverride}'|" \ arkmanager echo "Running install.sh" $sudo bash install.sh "$steamcmd_user" "${reinstall_args[@]}" result=$? cd / rm -rf "$tmpdir" if [ "$result" = 0 ] || [ "$result" = 2 ]; then echo "ARK Server Tools successfully upgraded" "${arkstScriptPath}" --version else echo "ARK Server Tools upgrade failed" fi exit $result } doUpgradeToolsFromBranch(){ arkstLatestVersion="$(curl -s "https://raw.githubusercontent.com/${arkstGithubRepo}/${arkstChannel}/.version")" arkstLatestCommit="$(curl -s "https://api.github.com/repos/${arkstGithubRepo}/git/refs/heads/${arkstChannel}" | sed -n 's/^ *"sha": "\(.*\)",.*/\1/p')" if [[ "$arkstLatestVersion" == "404: Not Found" ]]; then echo "Channel '${arkstChannel}' does not exist" echo echo "Available channels:" curl -s "https://api.github.com/repos/${arkstGithubRepo}/git/refs/heads" | sed -n 's|^ *"ref": "refs/heads/\(.*\)",|\1|p' echo return fi REPLY= if [[ "$arkstLatestVersion" > "$arkstVersion" ]]; then read -p "A new version was found! Do you want to upgrade ARK Server Tools to v${arkstLatestVersion}?" -n 1 -r echo elif [[ "$arkstLatestVersion" == "$arkstVersion" && "$arkstLatestCommit" != "$arkstCommit" ]]; then read -p "A hotfix is available for v${arkstLatestVersion}. Do you wish to install it?" -n 1 -r echo else echo "Your ARK server tools are already up to date" fi if [[ "$REPLY" =~ ^[Yy]$ ]]; then doUpgradeToolsFromCommit "$arkstLatestCommit" fi } doUpgradeToolsFromRelease(){ local tagname= local desc= echo "Getting latest release..." # Read the variables from github while IFS=$'\t' read -r n v; do case "${n}" in tag_name) tagname="${v}"; ;; body) desc="${v}" esac done < <(curl -s "https://api.github.com/repos/${arkstGithubRepo}/releases/latest" | sed -n 's/^ "\([^"]*\)": "*\([^"]*\)"*,*/\1\t\2/p') if [ -n "$tagname" ]; then if [ "$tagname" != "$arkstTag" ]; then echo "A new version has been released: ${tagname}" echo -e "$desc" read -p "Do you want to upgrade to ${tagname}? [Y/N] " -n 1 -r echo if [[ "$REPLY" =~ ^[Yy]$ ]]; then echo "Getting commit for latest release..." local commit="$(curl -s "https://api.github.com/repos/${arkstGithubRepo}/git/refs/tags/${tagname}" | sed -n 's/^ *"sha": "\(.*\)",.*/\1/p')" doUpgradeToolsFromCommit "$commit" fi else echo "Your ARK server tools are already up to date" fi else echo "Unable to get latest release" fi } doUninstallTools() { if [ -n "$arkstUsePkgManager" ]; then echo "arkmanager v${arkstVersion}: Please uninstall using your system's package manager" else local sudo=sudo if [ "$(id -u)" == 0 ] || [ "$steamcmd_user" == "--me" ]; then sudo= fi read -p "Are you sure you want to uninstall the ARK Server Tools? [y/N]" -n 1 -r if [[ "$REPLY" =~ ^[Yy]$ ]]; then if [ -n "${install_datadir}" ] && [ -x "${install_datadir}/arkmanager-uninstall.sh" ]; then $sudo "${install_datadir}/arkmanager-uninstall.sh" exit 0 elif [ -n "${install_libexecdir}" ] && [ -x "${install_libexecdir}/arkmanager-uninstall.sh" ]; then $sudo "${install_libexecdir}/arkmanager-uninstall.sh" exit 0 fi fi fi } runAsRoot(){ getConfigVar(){ val="$(echo -ne "$(sed -n "/^$1=/{s|^[^=]*=||;s|[[:space:]]*\\(#.*\\)*\$||;s|^\"\\(.*\\)\"\$|\\1|;s|^'\\(.*\\)'\$|\\1|;p}" <"${arkstGlobalCfgFile}" | tail -n1)")" if [ -n "$arkstRootUseEnv" ]; then val="$(eval printf "%s" "$(printf "%q" "${val}" | sed 's|\\[$]\\[{]\([A-Za-z][A-Za-z0-9_]*\)\\[}]|${\1}|g;s|\\[$]\([A-Za-z][A-Za-z0-9_]*\)|${\1}|g')")" fi if [ -n "$val" ]; then echo "$val" else echo "$2" fi } cd / arkstChannel="$(getConfigVar arkstChannel "master")" arkstUnstable="$(getConfigVar arkstUnstable "")" install_bindir="$(getConfigVar install_bindir "${0%/*}")" install_libexecdir="$(getConfigVar install_libexecdir "${install_bindir%/*}/libexec/arkmanager")" install_datadir="$(getConfigVar install_datadir "${install_bindir%/*}/share/arkmanager")" steamcmd_user="$(getConfigVar steamcmd_user "steam")" steamcmd_user_shellexec="$(getConfigVar steamcmd_user_shellexec "${BASH:-/bin/bash}")" if ! getent passwd "$steamcmd_user" >/dev/null 2>&1; then echo "Invalid steamcmd_user in config file" exit 1 fi if [ "$1" == "upgrade-tools" ]; then doUpgradeTools elif [ "$1" == "uninstall-tools" ]; then doUninstallTools elif [ "$steamcmd_user" = "$USERNAME" ]; then echo "Error: steamcmd_user is root; aborting" >&2 exit 1 else echo "Attempting to drop privileges; re-running as $steamcmd_user" >&2 exec /sbin/runuser "$steamcmd_user" -s "$steamcmd_user_shellexec" -c "$(printf "%q" "${arkstScriptPath}") --runfromroot$(printf " %q" "$@")" exit 1 fi } # Check the user is not currently running this script as root if [ "$(id -u)" == "0" ]; then if [ "$1" = "--run-as-root-i-know-what-im-doing" ]; then echo "Warning: running as root is dangerous" >&2 elif [ "$1" = "--runfromroot" ]; then echo "Error: still running as root after attempting to drop privileges; aborting" >&2 exit 1 else runAsRoot "$@" exit 0 fi fi #--------------------- # Variables #--------------------- # Global variables if [ -f "${arkstGlobalCfgFile}" ]; then # shellcheck source=arkmanager.cfg.shellcheck source "${arkstGlobalCfgFile}" fi if [ -f "${HOME}/${arkstUserCfgFile}" ]; then # shellcheck source=arkmanager.cfg.shellcheck source "${HOME}/${arkstUserCfgFile}" fi if [[ -n "${steamcmd_user}" && "${steamcmd_user}" != "--me" && "$USERNAME" != "${steamcmd_user}" && -n "$usesudo" ]]; then echo "Attempting to run as $steamcmd_user" >&2 exec sudo --user="${steamcmd_user}" "${arkstScriptPath}" "$@" fi if [[ -n "${serverbasedir}" && -d "${serverbasedir}" ]]; then cd "${serverbasedir}" elif [ -d "${HOME}" ]; then cd "${HOME}" elif [ -d "${steamcmdroot}" ]; then cd "${steamcmdroot}" elif [[ ! -d "${PWD}" || ! -r "${PWD}" || ! -x "${PWD}" ]]; then cd / fi lsof=lsof if [ -x /usr/sbin/lsof ]; then lsof=/usr/sbin/lsof fi # Local variables instver="" bnumber="" GREEN="\\033[1;32m" RED="\\033[1;31m" YELLOW="\\e[0;33m" NORMAL="\\033[0;39m" # Set TERM to "dumb" if TERM is not set export TERM=${TERM:-dumb} arkmanagerLog="arkmanager.log" # here are logged the actions performed by arkmanager arkserverLog="arkserver.log" # here is logged the output of ShooterGameServer appid="${appid:-376030}" mod_appid="${mod_appid:-346110}" install_bindir="${install_bindir:-${0%/*}}" install_libexecdir="${install_libexecdir:-${install_bindir%/*}/libexec/arkmanager}" if [ "$steamcmd_user" == "--me" ]; then install_datadir="${install_datadir:-${HOME}/.share/local/arkmanager}" else install_datadir="${install_datadir:-${install_bindir%/*}/share/arkmanager}" fi declare -A modsrcdirs #--------------------- # functions #--------------------- # # timestamp # timestamp() { date +"%Y-%m-%d %H:%M:%S" } # # Log a message to arkmanager.log, and exho it to the console # logprint(){ printf "%s\n" "$*" printf "%s: [%s] %s\n" "$(timestamp)" "${instance}" "$*" >>"${logdir}/${arkmanagerLog}" } # # check configuration and report errors # checkConfig() { # SteamCMD configuration # steamcmdroot if [ ! -d "$steamcmdroot" ] ; then echo -e "[" "$RED" "ERROR" "$NORMAL" "]" "\tYour SteamCMD root seems not valid." >&2 fi # steamcmdexec if [ ! -f "$steamcmdroot/$steamcmdexec" ] ; then echo -e "[" "$RED" "ERROR" "$NORMAL" "]" "\tYour SteamCMD exec could not be found." >&2 fi # steamcmd_user if [ "$steamcmd_user" != "--me" ]; then if ! getent passwd "$steamcmd_user" > /dev/null 2>&1 ; then echo -e "[" "$RED" "ERROR" "$NORMAL" "]" "\tYour SteamCMD user is not valid." >&2 fi fi # Environment configuration if [ "$1" != "install" ] && [ -n "$instance" ]; then # arkserverexec if [ -n "$arkserverroot" ] && [ ! -f "$arkserverroot/$arkserverexec" ] ; then echo -e "[" "$YELLOW" "WARN" "$NORMAL" "]" "\tYour ARK server exec could not be found." >&2 fi # SavedArks directory if [ -n "$arkserverroot" ]; then local savedarksdir="${arkserverroot}/${arkserverdir:-ShooterGame}/Saved/${ark_AltSaveDirectoryName:-SavedArks}" mkdir -p "${savedarksdir}" if [ ! -w "${savedarksdir}" ]; then echo -e "[" "$RED" "ERROR" "$NORMAL" "]" "\tThe ARK SavedArks directory is not writable, and saveworld will fail" >&2 fi mkdir -p "${arkserverroot}/${arkserverdir:-ShooterGame}/Content/Mods" if [ ! -w "${arkserverroot}/${arkserverdir:-ShooterGame}/Content/Mods" ]; then echo -e "[" "$RED" "ERROR" "$NORMAL" "]" "\tThe ARK Mods directory is not writable, and installmod will fail" >&2 fi fi if [ "$1" != "installmod" ] && [ "$1" != "installmods" ]; then # Warn if any mods are requested but not installed if [ -n "$arkserverroot" ] && [ -d "${arkserverroot}/${arkserverdir:-ShooterGame}/Content/Mods" ]; then for modid in $(getModIds); do if [ ! -f "${arkserverroot}/${arkserverdir:-ShooterGame}/Content/Mods/${modid}/mod.info" ]; then echo -e "[" "$RED" "ERROR" "$NORMAL" "]" "\tMod ${modid} is requested but not installed. Run 'arkmanager installmod ${modid}' to install this mod." >&2 fi done fi fi fi # Warn if mod_branch=Linux if [ "$mod_branch" == "Linux" ] && [ -z "$nowarnmodbranch" ]; then echo -e "[" "$YELLOW" "WARN" "$NORMAL" "]" "\tmod_branch is set to Linux. Linux mods are known to cause the server to crash. It is suggested you set mod_branch to Windows." >&2 fi # Service configuration # logdir if [ ! -w "$logdir" ] ; then echo -e "[" "$RED" "ERROR" "$NORMAL" "]" "\tYou do not have permission to write to the log directory." >&2 fi } # # Get setting from config or from ini file # $1 is the setting name # $2 is the default # getArkServerSetting() { # shellcheck disable=SC2018,SC2019 local lcname="$(tr 'A-Z' 'a-z' <<<"${1}")" local varname # shellcheck disable=SC2154 for varname in "${!ark_@}"; do # shellcheck disable=SC2018,SC2019 if [ "$(tr 'A-Z' 'a-z' <<<"$varname")" == "ark_${lcname}" ]; then echo "${!varname}" return fi done # shellcheck disable=SC2154 for varname in "${!arkopt_@}"; do # shellcheck disable=SC2018,SC2019 if [ "$(tr 'A-Z' 'a-z' <<<"$varname")" == "arkopt_${lcname}" ]; then echo "${!varname}" return fi done local val="$(tr -d '\0\376\377' <"${arkserverroot}/${arkserverdir}/Saved/Config/LinuxServer/GameUserSettings.ini" | sed -n '/^\[ServerSettings\]/,/^\[.*\]/{s/^'"$1"'[[:space:]]*=[[:space:]]*//ip;}' )" if [ -n "$val" ]; then echo "$val" else echo "$2" fi } # # Get server admin password # getAdminPassword() { getArkServerSetting "ServerAdminPassword" "" } # # Get server RCON Port # getRconPort() { getArkServerSetting "RCONPort" "32330" } # # Gets server bind IP # # shellcheck disable=SC2120 getMultiHome() { getArkServerSetting "MultiHome" "${1}" } # shellcheck disable=SC2120 getMultiHomeIP() { getArkServerSetting "MultiHome" "${1:-127.0.0.1}" } # # Get server Game Port # getGamePort() { echo "${ark_Port:-7778}" } # # Get server Query Port # getQueryPort(){ echo "${ark_QueryPort:-27015}" } # # Determine SteamCMD data directory # getSteamWorkshopDir(){ if [[ -d "${steamworkshopdir}" && -f "${steamworkshopdir}/appworkshop_${mod_appid}.acf" ]]; then echo "${steamworkshopdir}" return fi if [[ -z "${steamcmdhome}" || ! -d "${steamcmdhome}" ]]; then steamcmdhome="${HOME}" fi local wsfile="$( for d in "$steamcmdhome/.steam/steam" "$steamcmdhome/.steam" "$steamcmdhome/Steam"; do for d2 in "${d}/steamapps/workshop" "${d}/SteamApps/workshop"; do if [[ -d "${d2}" && -f "${d2}/appworkshop_${mod_appid}.acf" ]]; then stat -c "%Y %n" "${d2}/appworkshop_${mod_appid}.acf" fi done done | sort -n | tail -n1 | cut -d' ' -f2- )" if [[ -n "$wsfile" && -f "$wsfile" ]]; then echo "${wsfile%/*}" else echo "$steamcmdhome/Steam/steamapps/workshop" fi } # # Determine SteamCMD data directory # getSteamAppInfoCache(){ if [[ -n "${steamcmd_appinfocache}" && -f "${steamcmd_appinfocache}" ]]; then echo "${steamcmd_appinfocache}" return fi if [[ -z "${steamcmdhome}" || ! -d "${steamcmdhome}" ]]; then steamcmdhome="${HOME}" fi local appcachefile="$( for d in "$steamcmdhome/.steam/steam" "$steamcmdhome/.steam" "$steamcmdhome/Steam"; do if [[ -d "${d}" && -f "${d}/appcache/appinfo.vdf" ]]; then stat -c "%Y %n" "${d}/appcache/appinfo.vdf" fi done | sort -n | tail -n1 | cut -d' ' -f2- )" if [[ -n "$appcachefile" && -f "$appcachefile" ]]; then echo "${appcachefile}" else echo "${steamcmd_appinfocache:-$steamcmdhome/Steam/appcache/appinfo.vdf}" fi } # # Determine SteamCMD data directory # getSteamWorkshopLog(){ if [[ -n "${steamcmd_workshoplog}" && -f "${steamcmd_workshoplog}" ]]; then echo "${steamcmd_workshoplog}" fi if [[ -z "${steamcmdhome}" || ! -d "${steamcmdhome}" ]]; then steamcmdhome="${HOME}" fi local wslogfile="$( for d in "$steamcmdhome/.steam/steam" "$steamcmdhome/.steam" "$steamcmdhome/Steam"; do if [[ -d "${d}" && -f "${d}/logs/workshop_log.txt" ]]; then stat -c "%Y %n" "${d}/logs/workshop_log.txt" fi done | sort -n | tail -n1 | cut -d' ' -f2- )" if [[ -n "$wslogfile" && -f "$wslogfile" ]]; then echo "${wslogfile}" else echo "${steamcmd_workshoplog:-$steamcmdhome/Steam/logs/workshop_log.txt}" fi } # # Execute RCON command # rconcmd() { local adminpass="$(getAdminPassword)" if [ -z "$adminpass" ]; then echo "ServerAdminPassword is empty - unable to execute RCON command" return 1 elif [[ "$adminpass" =~ [?\177-\377] ]]; then echo "ServerAdminPassword contains invalid characters" return 1 fi perl -MSocket -e ' sub sendpkt { my ($sock, $reqid, $reqtype, $body) = @_; my $packet = pack("VVV", length($body) + 10, $reqid, $reqtype) . $body . "\0\0"; send($sock, $packet, 0) or die "Error sending command to server: $!"; } sub recvpkt { my ($sock) = @_; my $data = ""; recv($sock, $data, 12, 0); die "Empty response" if length($data) == 0; my ($pktlen, $resid, $restype) = unpack("VVV", $data); recv($sock, $data, $pktlen - 8, 0); return ($resid, $restype, substr($data, 0, $pktlen - 10)); } sub auth { my ($sock, $password) = @_; my $reqid = 1; sendpkt($sock, $reqid, 3, $password); my ($resid, $restype, $rcvbody) = recvpkt($sock); die "Authentication failed" if $resid == -1 or $resid == 0xFFFFFFFF; } my $port = $ARGV[0]; my $ipaddr = $ARGV[1]; my $password = $ARGV[2]; my $command = $ARGV[3]; socket(my $socket, PF_INET, SOCK_STREAM, 0); setsockopt($socket, SOL_SOCKET, SO_RCVTIMEO, pack("i4", 30, 0, 0, 0)); my $sockaddr = pack_sockaddr_in($port, inet_aton($ipaddr)); connect($socket, $sockaddr) or die "Error connecting to server: $!"; auth($socket, $password); sendpkt($socket, 2, 2, $command); my ($resid, $restype, $rcvbody) = recvpkt($socket); if ($rcvbody eq "Server received, But no response!! \n ") { print "Command processed\n"; } else { print "\"", $rcvbody, "\"\n"; } ' "$(getRconPort)" "$(getMultiHomeIP)" "$adminpass" "$1" } # # Save world # doSaveWorld() { rconcmd saveworld } # # Exit cleanly # doExitServer() { rconcmd doexit } # # Broadcast message # doBroadcast(){ rconcmd "${broadcastcmd:-broadcast} $1" } # # Broadcast message with echo # doBroadcastWithEcho(){ echo "$1" doBroadcast "$1" } # # Discord Webhook notifier # function notifyDiscord(){ if [ -n "$discordWebhookURL" ]; then local msg="$(echo -n "${msg}" | tr '\n' '\001' | sed 's/\001/\\n/g;s/["\\]/\\\0/g')" local json="{\"content\":\"${msg}\"}" curl -s -H "Content-Type: application/json" -d "${json}" "$discordWebhookURL" >/dev/null fi } # # Overridable notifier # function notifyOverride(){ : } # # Notification wrapper # function notify(){ if [[ -n "$1" && "$1" != "-" ]]; then local msg local notifymsg="$1" msg="${notifyTemplate:-Message from instance \`{instance\}\` on server \`{server\}\`: {msg\}}" msg="${msg//\{msg\}/${notifymsg}}" msg="${msg//\{instance\}/${instance}}" msg="${msg//\{server\}/${HOSTNAME}}" #shellcheck disable=SC2154 for v in "${!notifyvar_@}"; do vn="${v#notifyvar_}" msg="${msg//\{${vn}\}/${!v}}" done notifyDiscord "$msg" notifyOverride "$msg" if [ -n "${notifyCommand}" ]; then eval " ${notifyCommand}" fi fi } # # Download SteamCMD # function doDownloadSteamCMD(){ if [ ! -f "${steamcmdroot}/${steamcmdexec}" ]; then mkdir -p "${steamcmdroot}" curl -s "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" -o "${steamcmdroot}/steamcmd_linux.tar.gz" tar -xzf "${steamcmdroot}/steamcmd_linux.tar.gz" -C "${steamcmdroot}" fi } # # SteamCMD helper function # function runSteamCMD(){ if [[ -z "${steamcmdhome}" || ! -d "${steamcmdhome}" ]]; then steamcmdhome="${HOME}" fi # shellcheck disable=SC2086 HOME="${steamcmdhome}" "$steamcmdroot/$steamcmdexec" +@NoPromptForPassword 1 ${steamcmd_cmds_prelogin} "${steamcmd_force_install_dir[@]}" +login "${steamlogin:-anonymous}" ${steamcmd_cmds_postlogin} "$@" +quit } function runSteamCMDspinner(){ if [ -n "$verbose" ]; then printf "Executing" # shellcheck disable=SC2086 printf " %q" "$steamcmdroot/$steamcmdexec" +@NoPromptForPassword 1 ${steamcmd_cmds_prelogin} "${steamcmd_force_install_dir[@]}" +login "${steamlogin:-anonymous}" ${steamcmd_cmds_postlogin} "$@" +quit printf "\n" if (command >&3) 2>/dev/null; then runSteamCMD "$@" > >(tee /dev/fd/3) else runSteamCMD "$@" fi return $? else if [ -z "$progressDisplayType" ]; then if stty <&2 >/dev/null 2>&1; then progressDisplayType=spinner else progressDisplayType=dots fi fi if (command >&3) 2>/dev/null; then runSteamCMD "$@" >&3 & else runSteamCMD "$@" >/dev/null & fi local scpid=$! local pos=0 # shellcheck disable=SC1003 local spinner=( '\b-' '\b/' '\b|' '\b\\' ) if [ "$progressDisplayType" == "dots" ]; then spinner=( '.' ) fi echo -n ' ... ' while kill -0 $scpid 2>/dev/null; do echo -ne "${spinner[$pos]}" (( pos = (pos + 1) % ${#spinner[*]} )) sleep 0.5 done echo -ne '\b \b' wait $scpid return $? fi } function runSteamCMDspinnerSubst(){ local fd="$1" shift runSteamCMDspinner "$@" 3>&1 >"/dev/fd/${fd}" } # # Check if a new version is available but not apply it # function checkForUpdate(){ tput sc echo "Querying Steam database for latest version..." if isUpdateNeeded; then tput rc; tput ed; echo -e "Current version:" "$RED" "$instver" "$NORMAL" echo -e "Available version:" "$GREEN" "$bnumber" "$NORMAL" echo -e "Your server needs to be restarted in order to receive the latest update." echo -e "Run \"arkmanager update\" to do so" return 1 else tput rc; tput ed; echo -e "Current version:" "$GREEN" "$instver" "$NORMAL" echo -e "Available version:" "$GREEN" "$bnumber" "$NORMAL" echo "Your server is up to date!" return 0 fi } # # Check if the server need to be updated # Return 0 if update is needed, else return 1 # function isUpdateNeeded(){ instver="$(getCurrentVersion)" bnumber="$(getAvailableVersion)" if [[ -z "$bnumber" || "$bnumber" -eq "$instver" ]]; then return 1 # no update needed elif checkUpdateManifests; then echo "Build ID changed but manifests have not changed" return 1 else return 0 # update needed fi } # # Parse an ACF structure # $1 is the desired path # $2 is the desired property # $3 is the current path # function parseSteamACF(){ local sname while read -r name val; do name="${name#\"}" name="${name%\"}" val="${val#\"}" val="${val%\"}" if [ "$name" = "}" ]; then break elif [ "$name" == "{" ]; then parseSteamACF "$1" "$2" "${3}.${sname}" else if [ "$3" == "$1" ] && [ "$name" == "$2" ]; then echo "$val" break fi sname="${name}" fi done } # # Return the current version number # function getCurrentVersion(){ if [ -f "${arkserverroot}/steamapps/appmanifest_${appid}.acf" ]; then while read -r name val; do if [ "${name}" == "{" ]; then parseSteamACF "" "buildid"; break; fi; done <"${arkserverroot}/steamapps/appmanifest_${appid}.acf" fi } # # Return the version from the staging directory # function getStagingVersion(){ if [ -f "${arkStagingDir}/steamapps/appmanifest_${appid}.acf" ]; then while read -r name val; do if [ "${name}" == "{" ]; then parseSteamACF "" "buildid"; break; fi; done <"${arkStagingDir}/steamapps/appmanifest_${appid}.acf" fi } # # Return the installed beta / branch # function getCurrentBranch(){ if [ -f "${arkserverroot}/steamapps/appmanifest_${appid}.acf" ]; then while read -r name val; do if [ "${name}" == "{" ]; then parseSteamACF ".UserConfig" "betakey"; break; fi; done <"${arkserverroot}/steamapps/appmanifest_${appid}.acf" fi } # # Return the installed beta / branch in staging directory # function getCurrentBranch(){ if [ -f "${arkStagingDir}/steamapps/appmanifest_${appid}.acf" ]; then while read -r name val; do if [ "${name}" == "{" ]; then parseSteamACF ".UserConfig" "betakey"; break; fi; done <"${arkStagingDir}/steamapps/appmanifest_${appid}.acf" fi } # # Get the current available server version on steamdb # function getAvailableVersion(){ rm -f "$(getSteamAppInfoCache)" if [ -z "$appbranch" ]; then appbranch="$(getCurrentBranch)" fi runSteamCMD +app_info_update 1 +app_info_print "$appid" | while read -r name val; do if [ "${name}" == "{" ]; then parseSteamACF ".depots.branches.${appbranch:-public}" "buildid"; break; fi; done } # # Gets the server map name # function getServerMapName(){ local mapname="${serverMap}" # extract the map name from the active map mod if [[ ( -z "$mapname" || "$mapname" == "TheIsland" ) && -n "$serverMapModId" ]]; then mapname="$(perl -e ' my $data; { local $/; $data = <>; } my $mapnamelen = unpack("@0 L<", $data); my $mapname = substr($data, 4, $mapnamelen - 1); $mapnamelen += 4; my $mapfilelen = unpack("@" . ($mapnamelen + 4) . " L<", $data); my $mapfile = substr($data, $mapnamelen + 8, $mapfilelen - 1); print $mapfile; ' <"${arkserverroot}/${arkserverdir}/Content/Mods/${serverMapModId}/mod.info")" fi echo "${mapname##*/}" } # # Gets the saved worlds directory # function getSavedArksDirectory(){ local savedir="SavedArks" local saverootdir="$1" # Get save directory name if [ -n "${ark_AltSaveDirectoryName}" ]; then savedir="${ark_AltSaveDirectoryName}" fi savedir="${saverootdir}/${savedir}" # Check for the (unlikely) case that the case of the # saved ark directory is screwed up if [ ! -d "${savedir}" ]; then cisavedir="$(find "${arkserverroot}" -ipath "${savedir}" | head -n1)" if [ -n "$cisavedir" ]; then echo -e " ${NORMAL}[ ${YELLOW}WARN${NORMAL} ] Saved arks directory capitalization is inconsistent" >&2 savedir="${cisavedir}" else echo -e " ${NORMAL}[ ${RED}ERROR${NORMAL} ] Saved arks directory does not exist" >&2 echo "" return 1 fi fi echo "${savedir}" } # # Check if the update manifest matches the current manifest # function checkUpdateManifests(){ local appinfo="$(runSteamCMD +app_info_print "$appid")" if [ -z "$appbranch" ]; then appbranch="$(getCurrentBranch)" fi local hasmanifest= while read -r depot manifest <&3; do hasmanifest=1 depot="${depot//\"/}" manifest="${manifest//\"/}" newmanifest="$(echo "${appinfo}" | while read -r name val; do if [ "${name}" == "{" ]; then parseSteamACF ".depots.${depot}.manifests" "${appbranch:-public}"; break; fi; done)" if [[ -z "${newmanifest}" && "${appbranch:-public}" != "public" ]]; then newmanifest="$(echo "${appinfo}" | while read -r name val; do if [ "${name}" == "{" ]; then parseSteamACF ".depots.${depot}.manifests" "public"; break; fi; done)" fi if [ "${newmanifest}" != "${manifest}" ]; then return 1 fi done 3< <(sed -n '/^[{]$/,/^[}]$/{/^\t"MountedDepots"$/,/^\t[}]$/{/^\t\t/p}}' "${arkserverroot}/steamapps/appmanifest_${appid}.acf") if [ -z "$hasmanifest" ]; then return 1 else return 0 fi } # # Get the PID of the server process # function getServerPID(){ if [ -f "${arkserverroot}/${arkserverpidfile}" ]; then serverpid="$(<"${arkserverroot}/${arkserverpidfile}")" if kill -0 "$serverpid" >/dev/null 2>&1; then echo "$serverpid" return elif kill -0 "-$serverpid" >/dev/null 2>&1; then pgrep -g "$serverpid" && return fi fi if [ -f "${arkserverroot}/${arkserveroldpidfile}" ]; then serverpid="$(<"${arkserverroot}/${arkserveroldpidfile}")" if kill -0 "$serverpid" >/dev/null 2>&1; then echo "$serverpid" return elif kill -0 "-$serverpid" >/dev/null 2>&1; then pgrep -g "$serverpid" && return fi fi } # # Check id the server process is alive # function isTheServerRunning(){ if [ -n "$(getServerPID)" ]; then return 0 else return 1 fi } # # Check if the server is up # # function isTheServerUp(){ local ip="$(getMultiHome)" result=1 if [ -x "$( which "$lsof" )" ]; then "$lsof" -w -i "${ip:+udp@}${ip}:$(getGamePort)" > /dev/null result=$? fi if [ $result -ne 0 ]; then perl -MSocket -MFcntl -e ' my $port = int($ARGV[0]); socket(my $socket, PF_INET, SOCK_DGRAM, 0); setsockopt($socket, SOL_SOCKET, SO_RCVTIMEO, pack("i4", 1, 0, 0, 0)); my $sockaddr = pack_sockaddr_in($port, inet_aton($ARGV[1])); send($socket, "\xff\xff\xff\xffTSource Engine Query\x00", 0, $sockaddr); my $flags = fcntl($socket, F_GETFL, 0) or exit(1); fcntl($socket, F_SETFL, $flags | O_NONBLOCK) or exit(1); my $data = ""; my $rin = ""; vec($rin, fileno($socket), 1) = 1; if (select($rin, undef, undef, 0.25) >= 0) { recv($socket, $data, 1400, 0) or exit(1); my ($servername, $mapname, $game, $fullname, $rest) = split(/\x00/, substr($data, 6), 5); my $maxplayers = ord(substr($rest, 3, 1)); if ($maxplayers == 0) { exit(1); } exit(0); } else { exit(1); } ' "$(getQueryPort)" "$(getMultiHomeIP)" result=$? fi # In this case, the result is: # 1 if the command fail. The port is not listenning # 0 if the command succeed. The port is listenning if [ $result -eq 0 ];then return 1 else return 0 fi } # # Check if the server is visible in the steam server list # function isTheServerOnline(){ if [ -n "$(getMultiHome)" ]; then publicip="$(curl --interface "$(getMultiHomeIP)" -s https://api.ipify.org/)" else publicip="$(curl -s https://api.ipify.org/)" fi local serverresp if [[ "$publicip" =~ [1-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]* ]]; then serverresp="$(curl -s "http://api.steampowered.com/ISteamApps/GetServersAtAddress/v0001?addr=${publicip}:$(getQueryPort)")" fi # If the Steam server response contains "addr": "$ip:$port", # then the server has registered with the Steam master server if [[ "$serverresp" =~ \"addr\":[[:space:]]*\"([^\"]*):([0-9]*)\" ]]; then return 0 else return 1 fi } # # Check if anybody is connected to the server # function numPlayersConnected(){ if [[ -n "$arkflag_crossplay" || -n "$arkflag_epiconly" ]]; then players="$(rconcmd listplayers 2>/dev/null)" if [[ "$players" == '"'?*'"' ]]; then echo "$players" | grep -c '[0-9]\.' return fi fi if [ -n "$arkUsePlayerList" ]; then perl -MSocket -e ' my $port = int($ARGV[0]); socket(my $socket, PF_INET, SOCK_DGRAM, 0); setsockopt($socket, SOL_SOCKET, SO_RCVTIMEO, pack("i4", 1, 0, 0, 0)); my $sockaddr = pack_sockaddr_in($port, inet_aton($ARGV[1])); send($socket, "\xff\xff\xff\xff\x55\xff\xff\xff\xff", 0, $sockaddr); my $data = ""; recv($socket, $data, 1400, 0) or (print "-1" and exit(1)); if (ord(substr($data, 4, 1)) == 0x41) { my $chal = substr($data, 5); send($socket, "\xff\xff\xff\xff\x55" . $chal, 0, $sockaddr); $data = ""; recv($socket, $data, 1400, 0) or (print "-1" and exit(1)); } ord(substr($data, 4, 1)) != 0x44 and (print "-1" and exit(1)); my $players = ord(substr($data, 5, 1)); my $active = 0; my $pdata = substr($data, 6); for my $i (0 .. $players) { my $idx = ord(substr($pdata, 0, 1)); my ($name, $rest) = split(/\x00/, substr($pdata, 1), 2); $pdata = substr($rest, 8); if ($name ne "") { $active = $active + 1; } } print "$active\n"; ' "$(getQueryPort)" "$(getMultiHomeIP)" else perl -MSocket -e ' my $port = int($ARGV[0]); socket(my $socket, PF_INET, SOCK_DGRAM, 0); setsockopt($socket, SOL_SOCKET, SO_RCVTIMEO, pack("i4", 1, 0, 0, 0)); my $sockaddr = pack_sockaddr_in($port, inet_aton($ARGV[1])); send($socket, "\xff\xff\xff\xffTSource Engine Query\x00", 0, $sockaddr); my $data = ""; recv($socket, $data, 1400, 0) or (print "-1" and exit(1)); my ($servername, $mapname, $game, $fullname, $rest) = split(/\x00/, substr($data, 6), 5); my $players = ord(substr($rest, 2, 1)); print "$players\n"; ' "$(getQueryPort)" "$(getMultiHomeIP)" fi } # # run function # doRun() { cd "${arkserverroot}/${arkserverexec%/*}" if isTheServerRunning; then echo "Error: another server instance is running from the same directory" echo "Aborting - two servers MUST NOT run from the same directory" exit 1 fi # $$ returns the main process, $BASHPID returns the current process echo "$BASHPID" >"${arkserverroot}/${arkmanagerpidfile}" if [ -f "${arkserverroot}/${arkupdatelockfile}" ]; then local updatepid="$(<"${arkserverroot}/${arkupdatelockfile}")" if kill -0 "$updatepid" >/dev/null 2>&1; then echo "An update is currently in progress. Start aborted" return 1 fi fi if [[ " $* " = *" --wait "* ]]; then # This requires bash 4+ # $$ returns the main process, $BASHPID returns the current process kill -STOP $BASHPID # wait for caller to renice us fi arkserveropts="$serverMap" declare -A usedoptions while read -r varname; do val="${!varname}" modid="${varname#arkmod_}" case "$val" in game*|enabled) ark_GameModIds="${ark_GameModIds}${ark_GameModIds:+,}${modid}" ;; map*) serverMapModId="${modid}" ;; tc|total*) ark_TotalConversionMod="${modid}" ;; esac usedoptions[${varname}]="${val}" done < <(sed -n 's/^\(arkmod_[^= ]*\)=.*/\1/p' <"$configfile") if [ -n "$arkModCollections" ]; then while read -r modid; do if [[ ",${ark_GameModIds}," != *",$modid,"* ]]; then ark_GameModIds="${ark_GameModIds}${ark_GameModIds:+,}${modid}" fi done fi if [[ ( -z "$serverMap" || "$serverMap" == "TheIsland" ) && -n "$serverMapModId" ]]; then serverMap="$(perl -e ' my $data; { local $/; $data = <>; } my $mapnamelen = unpack("@0 L<", $data); my $mapname = substr($data, 4, $mapnamelen - 1); $mapnamelen += 4; my $mapfilelen = unpack("@" . ($mapnamelen + 4) . " L<", $data); my $mapfile = substr($data, $mapnamelen + 8, $mapfilelen - 1); print $mapfile; ' <"${arkserverroot}/${arkserverdir}/Content/Mods/${serverMapModId}/mod.info")" arkserveropts="${serverMap}?MapModID=${serverMapModId}" fi if [ -z "$arkserveropts" ]; then arkserveropts="TheIsland" fi arkextraopts=( ) while read -r varname; do val="${!varname}" case "$varname" in ark_*) name="${varname#ark_}" # Port is actually one higher than specified # i.e. specifying port 7777 will have the server # use port 7778 if [[ "$name" == "Port" && ( -z "$arkNoPortDecrement" || "$arkNoPortDecrement" == "false" ) ]]; then (( val = val - 1 )) fi if [ -n "$val" ]; then arkserveropts="${arkserveropts}?${name}=${val}" else arkserveropts="${arkserveropts}?${name}" fi ;; arkopt_*) name="${varname#arkopt_}" val="${!varname}" if [ -n "$val" ]; then arkextraopts=( "${arkextraopts[@]}" "-${name}=${val}" ) fi ;; arkflag_*) name="${varname#arkflag_}" arkextraopts=( "${arkextraopts[@]}" "-${name}" ) ;; esac usedoptions[$varname]="${val}" done < <(sed -n 's/^\(ark\(\|opt\|flag\)_[^= ]*\)=.*/\1/p' <"$configfile") # bring in ark_... options # shellcheck disable=SC2154 for varname in "${!ark_@}"; do if [ -z "${usedoptions[${varname}]}" ]; then name="${varname#ark_}" val="${!varname}" # Port is actually one higher than specified # i.e. specifying port 7777 will have the server # use port 7778 if [[ "$name" == "Port" && ( -z "$arkNoPortDecrement" || "$arkNoPortDecrement" == "false" ) ]]; then (( val = val - 1 )) fi if [ -n "$val" ]; then arkserveropts="${arkserveropts}?${name}=${val}" else arkserveropts="${arkserveropts}?${name}" fi usedoptions[${varname}]="${val}" fi done # bring in arkflag_... flags # shellcheck disable=SC2154 for varname in "${!arkflag_@}"; do if [ -z "${usedoptions[${varname}]}" ]; then name="${varname#arkflag_}" arkextraopts=( "${arkextraopts[@]}" "-${name}" ) usedoptions[${varname}]="${val}" fi done # bring in arkopt_... options # shellcheck disable=SC2154 for varname in "${!arkopt_@}"; do if [ -z "${usedoptions[${varname}]}" ]; then name="${varname#arkopt_}" val="${!varname}" if [ -n "$val" ]; then arkextraopts=( "${arkextraopts[@]}" "-${name}=${val}" ) fi usedoptions[${varname}]="${val}" fi done if [[ " ${arkextraopts[*]} " =~ " -automanagedmods " ]]; then steamcmdroot="${arkserverroot}/Engine/Binaries/ThirdParty/SteamCMD/Linux" steamcmdexec="steamcmd.sh" doDownloadSteamCMD fi arkserveropts="${arkserveropts}?listen" if [ -n "${arkCustomDynamicConfigURL}" ]; then arkserveropts="${arkserveropts}?customdynamicconfigurl=${arkCustomDynamicConfigURL}" if [[ -z "${usedoptions[arkflag_UseDynamicConfig]}" && -z "${usedoptions[arkopt_UseDynamicConfig]}" ]]; then arkextraopts=( "${arkextraopts[@]}" "-UseDynamicConfig" ) fi fi # run the server in background echo "$(timestamp): start" serverpid=0 restartserver=1 # Shutdown the server when we are terminated shutdown_server(){ restartserver=0 notify "${notifyMsgShuttingDown:-Shutting down}" rm -f "$arkserverroot/$arkautorestartfile" if [ "$serverpid" -ne 0 ]; then kill -INT -$serverpid >/dev/null 2>&1 fi exit 0 } trap shutdown_server INT TERM # Enable job control so server gets its own process group set -m # Auto-restart loop while [ $restartserver -ne 0 ]; do echo -n "$(timestamp): Running" notify "${notifyMsgStarting:-Starting}" printf " %q" "$arkserverroot/$arkserverexec" "$arkserveropts" "${arkextraopts[@]}" echo # Put the server process into the background so we can monitor it "$arkserverroot/$arkserverexec" "$arkserveropts" "${arkextraopts[@]}" & # Grab the server PID serverpid=$! echo "$serverpid" >"${arkserverroot}/${arkserverpidfile}" echo "$(timestamp): Server PID: $serverpid" # Disable auto-restart so we don't get caught in a restart loop rm -f "$arkserverroot/$arkautorestartfile" restartserver=0 serveronline=0 if [ -n "$arkAlwaysRestartOnCrash" ]; then restartserver=1 touch "$arkserverroot/$arkautorestartfile" fi # Retries for checking it the server comes back up after going down serverdowntries=0 sleep 5 while true; do # Grab the current server PID local pid="$(getServerPID)" if [ "$pid" == "$serverpid" ]; then serverdowntries=0 if [ "$serveronline" -eq 0 ]; then # Check if the server has fully started if ! isTheServerUp; then # Enable auto-restart if the server is up echo "$(timestamp): server is up" notify "${notifyMsgServerUp:-Server is up}" if [ "$restartserver" -eq 0 ]; then touch "$arkserverroot/$arkautorestartfile" restartserver=1 fi serveronline=1 fi elif isTheServerUp; then (( serverdowntries++ )) if (( serverdowntries > 12 )); then # Server has not been listening for 60 seconds, so restart it. echo "$(timestamp): The server has stopped listening" echo "$(timestamp): Restarting server" notify "${notifyMsgStoppedListening:-Server has stopped listening - restarting}" for (( i = 0; i < 5; i++ )); do if ! kill -0 "-$serverpid"; then break fi kill -INT "-$serverpid" sleep 5 done if kill -0 "-$serverpid"; then echo "$(timestamp): Graceful restart failed - killing server" kill -KILL "-$serverpid" fi # Exit the server check loop break fi fi elif [ "$pid" == "$(pgrep --pgroup $serverpid)" ]; then restartserver=1 echo "$(timestamp): Server has double-forked and cannot be effectively monitored" echo "$(timestamp): Restarting server" notify "${notifyMsgDoubleForked:-Server has daemonized itself - restarting}" for (( i = 0; i < 5; i++ )); do if ! kill -0 "-$serverpid"; then break fi kill -INT "-$serverpid" sleep 5 done if kill -0 "-$serverpid"; then echo "$(timestamp): Graceful restart failed - killing server" kill -KILL "-$serverpid" fi # Exit the server check loop break else echo "$(timestamp): Bad PID '$pid'; expected '$serverpid'" if [ "$pid" != "" ]; then # Another instance must be running - disable autorestart restartserver=0 fi break fi sleep 5 done # Wait on the now-dead process to reap it and get its return status wait $serverpid echo "$(timestamp): exited with status $?" # doStop will remove the autorestart file if [ ! -f "$arkserverroot/$arkautorestartfile" ]; then restartserver=0 fi if [ "$restartserver" -ne 0 ]; then notify "${notifyMsgServerTerminated:-Server exited - restarting}" echo "$(timestamp): restarting server" fi done } doRunBG(){ for fd in "/proc/$BASHPID/fd/"*; do fd="${fd##*/}" [[ $fd -gt 2 && $fd != 255 ]] && exec {fd}<&- done doRun "$@" > >(while read -r l; do printf "%s: [%s] %s\n" "$(timestamp)" "${instance}" "${l}"; done) 2>&1 } # # start function # doStart() { touch "${arkserverroot}/.startAfterUpdate-${instance}" if [ -f "${arkserverroot}/${arkupdatelockfile}" ]; then local updatepid="$(<"${arkserverroot}/${arkupdatelockfile}")" if kill -0 "$updatepid" >/dev/null 2>&1; then logprint "Start aborted due to running update - pid: $updatepid" return 1 fi fi serverpid="$(getServerPID)" if [ -n "$serverpid" ] && kill -0 "$serverpid"; then logprint "Start aborted due to server already running - pid: $serverpid" else local saverootdir="${arkserverroot}/${arkserverdir}/Saved" local savedcfgdir="${saverootdir}/Config/LinuxServer" local prestart="${configfile%.cfg}.start" local gusini="${configfile%.cfg}.GameUserSettings.ini" local gameini="${configfile%.cfg}.Game.ini" if [ -n "${arkGameUserSettingsIniFile}" ]; then gusini="${arkGameUserSettingsIniFile}" fi if [ -n "${arkGameIniFile}" ]; then gameini="${arkGameIniFile}" fi if [ -n "${arkPreStart}" ]; then prestart="${arkPreStart}" fi if [[ -n "${gusini}" && -f "${gusini}" ]]; then cp "${gusini}" "${savedcfgdir}/GameUserSettings.ini" fi if [[ -n "${gameini}" && -f "${gameini}" ]]; then cp "${gameini}" "${savedcfgdir}/Game.ini" fi if [[ -n "${prestart}" && -f "${prestart}" ]]; then if [ -x "${prestart}" ]; then "${prestart}" "$@" else /bin/bash "${prestart}" "$@" fi fi if [ "$arkAutoUpdateOnStart" == "true" ]; then if ! [[ " $* " =~ " --noautoupdate " ]]; then logprint "Checking for updates before starting" doUpdate --update-mods --no-autostart fi fi if [[ " $* " =~ " --alwaysrestart " ]]; then arkAlwaysRestartOnCrash=true fi nobackground= if [[ " $* " =~ " --no-background " ]]; then nobackground=1 fi tput sc logprint "The server is starting..." local pid=$! if [[ -n "$nobackground" ]]; then echo doRun "$@" return elif [[ -n "$arkPriorityBoost" || -n "$arkCpuAffinity" ]]; then doRunBG --wait >"$logdir/$arkserverLog" 2>&1 & # output of this command is logged local pid="$!" # Wait for monitor process to suspend itself sleep 1 if [ -n "$arkPriorityBoost" ]; then logprint "Boosting priority of ark server" sudo renice -n "$arkPriorityBoost" "$pid" fi if [ -n "$arkCpuAffinity" ]; then echo "Setting CPU affinity for ark server" taskset -pc "$arkCpuAffinity" "$pid" fi kill -CONT "$pid" else doRunBG >"$logdir/$arkserverLog" 2>&1 & # output of this command is logged fi tput rc; tput ed; logprint "The server is now running, and should be up within 10 minutes" if [[ -n "$arkStartDelay" || -n "$defaultinstance_max" ]]; then local delay="${arkStartDelay:-${defaultinstance_max}}" if (( "${#instances[@]}" > 1 )); then echo -n "Waiting up to ${delay} seconds before starting next instance " while (( delay > 0 )); do echo -n "." if isTheServerOnline; then break fi (( delay-- )) done echo fi fi fi } # # starts all servers specified by configfile_xxxxx in config file # doStartAll(){ doStart # shellcheck disable=SC2154 for cfg in "${!configfile_@}"; do if [ -f "${!cfg}" ]; then ( # shellcheck source=arkmanager.cfg.shellcheck source "${!cfg}" doStart ) fi done } # # stop the ARK server # doStop() { if [ "$1" != "update" ]; then rm -f "${arkserverroot}/.startAfterUpdate-${instance}" fi if isTheServerRunning; then local stopreason="$1" local dowarn= local warnreason= local dosave= shift for arg in "$@"; do case "$arg" in --warn) dowarn=1; ;; --warnreason=*) warnreason="${arg#--warnreason=}"; dowarn=1; ;; --saveworld) dosave=1; ;; esac done if [[ -n "$dowarn" ]]; then if ! doWarn "$stopreason" "$warnreason"; then return 1 fi fi if [[ -n "$dosave" ]]; then doSaveWorld fi tput sc logprint "Stopping server; reason: $stopreason" rm -f "$arkserverroot/$arkautorestartfile" rm -f "$arkserverroot/$arkoldautorestartfile" # kill the server with the PID PID="$(getServerPID)" if [ -n "$PID" ]; then kill -INT "$PID" >/dev/null 2>&1 fi for (( i = 0; i < 20; i++ )); do sleep 1 if ! isTheServerRunning; then break fi done if isTheServerRunning && [ -n "$PID" ]; then tput rc logprint "Killing server" kill -KILL "$PID" >/dev/null 2>&1 fi if [ -f "${arkserverroot}/${arkmanagerpidfile}" ]; then PID="$(<"${arkserverroot}/${arkmanagerpidfile}")" if [ -n "$PID" ]; then kill "$PID" >/dev/null 2>&1 fi fi rm -f "${arkserverroot}/${arkserverpidfile}" rm -f "${arkserverroot}/${arkserveroldpidfile}" rm -f "${arkserverroot}/${arkmanagerpidfile}" tput rc; tput ed; logprint "The server has been stopped" else echo "The server is already stopped" fi } # # stops all servers specified by configfile_xxxxx in config file # doStopAll(){ doStop for cfg in "${!configfile_@}"; do if [ -f "${!cfg}" ]; then ( # shellcheck source=arkmanager.cfg.shellcheck source "${!cfg}" doStop ) fi done } # # install / update / download update # runSteamCMDAppUpdate(){ local installdir="$1" shift local steamcmd_force_install_dir=( +force_install_dir "$installdir" ) # shellcheck disable=SC2086 runSteamCMDspinner +app_update "$appid" $steamcmd_appextraopts "$@" } # # install of ARK server # doInstall() { # Check if arkserverroot already exists if [ ! -d "$arkserverroot" ]; then # If it does not exist, try create it echo -e "Creating the ARK server root directory ($arkserverroot)" mkdir -p "$arkserverroot" mkdir -p "${arkserverroot}/steamapps" if [ ! $? ] ; then echo -e "[" "$RED" "ERROR" "$NORMAL" "]" "\tFailed to create the defined ARK server root directory ($arkserverroot)" exit 1 fi fi cd "$steamcmdroot" echo -n "Installing ARK server" # install the server doDownloadSteamCMD runSteamCMDAppUpdate "$arkserverroot" validate if [ $? -eq 5 ]; then echo "User ${steamlogin:-anonymous} login failed - please login to steamcmd manually" elif [ $? -eq 8 ]; then echo "Insufficient disk space to install the ARK server" fi # the current version should be the last version. We set our version instver="$(getCurrentVersion)" } # # Cancels a pending shutdown # doCancelShutdown(){ if [ -f "${arkserverroot}/${arkwarnlockfile}" ]; then local lockpid="$(<"${arkserverroot}/${arkwarnlockfile}")" if [ -n "$lockpid" ]; then kill "$lockpid" rm -f "${arkserverroot}/${arkwarnlockfile}" fi fi } # # Formats a warning message based on replacement strings # printWarnMessage(){ local msg local usenotify="$5" if [ -n "$msgWarnReason" ]; then local reason local msgtime if [ "$3" == "minutes" ]; then if [ -n "$msgTimeMinutes" ]; then msgtime="${msgTimeMinutes//\{minutes\}/$4}" else msgtime="$4 minutes" fi else if [ -n "$msgTimeSeconds" ]; then msgtime="${msgTimeSeconds//\{seconds\}/$4}" else msgtime="$4 seconds" fi fi msg="${msgWarnReason//\{time\}/$msgtime}" if [ -n "$warnreason" ]; then local v="warnreason_$warnreason" reason="${!v}" if [ -z "$reason" ]; then reason="$warnreason" fi elif [ "$1" == "update" ]; then if [ -n "$appupdate" ]; then if [ -n "$modupdate" ]; then if [ -n "$msgReasonUpdateAppMod" ]; then reason="$msgReasonUpdateMod" else reason="an update to the game and an update to mod(s) {modnamesupdated}" fi else if [ -n "$msgReasonUpdateApp" ]; then reason="$msgReasonUpdateApp" else reason="an update to the game" fi fi elif [ -n "$modupdate" ]; then if [ -n "$msgReasonUpdateMod" ]; then reason="$msgReasonUpdateMod" else reason="an update to mod(s) {modnamesupdated}" fi fi elif [ -n "$2" ]; then reason="$2" elif [ "$1" == "restart" ]; then if [ -n "$msgReasonRestart" ]; then reason="$msgReasonRestart" else reason="a restart" fi else if [ -n "$msgReasonShutdown" ]; then reason="$msgReasonShutdown" else reason="maintenance" fi fi reason="${reason//\{time\}/${msgtime}}" reason="${reason//\{modnamesupdated\}/${modnamesupdated}}" reason="${reason//\{version\}/${arkversion}}" msg="${msg//\{reason\}/${reason}}" else if [ "$1" == "update" ]; then if [ "$3" == "minutes" ]; then if [ -n "$msgWarnUpdateMinutes" ]; then msg="${msgWarnUpdateMinutes//%d/$4}" else msg="This ARK server will shutdown for an update in $4 minutes" fi else if [ -n "$msgWarnUpdateSeconds" ]; then msg="${msgWarnUpdateSeconds//%d/$4}" else msg="This ARK server will shutdown for an update in $4 seconds" fi fi elif [ "$1" == "restart" ]; then if [ "$3" == "minutes" ]; then if [ -n "$msgWarnRestartMinutes" ]; then msg="${msgWarnRestartMinutes//%d/$4}" else msg="This ARK server will shutdown for a restart in $4 minutes" fi else if [ -n "$msgWarnRestartSeconds" ]; then msg="${msgWarnRestartSeconds//%d/$4}" else msg="This ARK server will shutdown for a restart in $4 seconds" fi fi else if [ "$3" == "minutes" ]; then if [ -n "$msgWarnShutdownMinutes" ]; then msg="${msgWarnShutdownMinutes//%d/$4}" else msg="This ARK server will shutdown in $4 minutes" fi else if [ -n "$msgWarnShutdownSeconds" ]; then msg="${msgWarnShutdownSeconds//%d/$4}" else msg="This ARK server will shutdown in $4 seconds" fi fi fi fi doBroadcastWithEcho "$msg" if [ -n "$usenotify" ]; then notify "$msg" fi } # # Checks if a player has requested an update cancel in the last 5 minutes # isUpdateCancelRequested(){ if [ -n "$chatCommandRestartCancel" ]; then local canceltime="$( find "${arkserverroot}/${arkserverdir}/Saved/Logs" -name 'ServerGame.*.log' -mmin -5 -print0 | xargs -0 grep -F -e "${chatCommandRestartCancel}" | sed 's@^[[]\(....\)\.\(..\)\.\(..\)-\(..\)\.\(..\)\.\(..\):.*@\1-\2-\3 \4:\5:\6 UTC@' | head -n1)" if [ -n "${canceltime}" ]; then canceltime="$(date +%s --date="${canceltime}")" local timenow="$(date +%s --date="now - 5 minutes")" if (( canceltime > timenow )); then return 0 fi fi fi return 1 } # # Waits for a configurable number of minutes before updating the server # doWarn(){ cd "$arkserverroot" ( echo "${BASHPID}" >"${arkserverroot}/${arkwarnlockfile}.${BASHPID}" 2>/dev/null while true; do if ! ln "${arkserverroot}/${arkwarnlockfile}.${BASHPID}" "${arkserverroot}/${arkwarnlockfile}" 2>/dev/null; then local lockpid="$(<"${arkserverroot}/${arkwarnlockfile}")" if [ -n "$lockpid" ] && [ "$lockpid" != "${BASHPID}" ] && kill -0 "$lockpid" 2>/dev/null; then echo "Shutdown warning already in progress (PID: $lockpid)" rm -f "${arkserverroot}/${arkwarnlockfile}.${BASHPID}" 2>/dev/null exit 1 fi rm -f "${arkserverroot}/${arkwarnlockfile}" else break fi done rm -f "${arkserverroot}/${arkwarnlockfile}.${BASHPID}" update_cancelled(){ if [ -n "$msgUpdateCancelled" ]; then msg="${msgUpdateCancelled//%s/$1}" else msg="Shutdown cancelled by operator ($1)" fi doBroadcastWithEcho "${msg}" notify "${msg}" exit 1 } trap "update_cancelled 'Ctrl+C'" SIGINT trap "update_cancelled 'Terminated'" SIGTERM trap "update_cancelled 'Connection Closed'" SIGHUP trap "update_cancelled 'Quit'" SIGQUIT local pid="$(getServerPID)" local sleeppid local usenotify=1 if [ -n "$noNotifyWarn" ]; then usenotify= fi if [ -n "$pid" ]; then local warnminutes=$(( arkwarnminutes )) if (( warnminutes == 0 )); then warnminutes=60 fi local warnintervals=( 90 60 45 30 20 15 10 5 4 3 2 ) if (( warnminutes > 2 )); then for warninterval in "${warnintervals[@]}"; do if [ "$(getServerPID)" != "$pid" ]; then echo "Server has stopped. Aborting $1" rm -f "${arkserverroot}/${arkwarnlockfile}" return 1 fi if (( warnminutes > warninterval )); then sleep 1m & sleeppid=$! printWarnMessage "$1" "$2" "minutes" "$warnminutes" "$usenotify" usenotify= for (( min = warnminutes - 1; min >= warninterval; min-- )); do if [ "$arkprecisewarn" != "true" ]; then numplayers=$(numPlayersConnected) echo "There are ${numplayers} players connected" if [[ "${numplayers}" == "-1" ]]; then echo "Server is not running. Shutting down immediately" notify "${notifyMsgServerNotRunning:-Server is not running. Shutting down immediately}" return 0 elif (( (numplayers + 0) == 0 )); then doBroadcastWithEcho "Nobody is connected. Shutting down immediately" notify "${notifyMsgNobodyConnected:-Nobody is connected. Shutting down immediately}" rm -f "${arkserverroot}/${arkwarnlockfile}" return 0 fi fi if isUpdateCancelRequested; then doBroadcastWithEcho "Restart cancelled by player request" notify "${notifyMsgRestartCancelled:-Restart cancelled by player request}" return 1 fi wait $sleeppid if (( min > warninterval )); then sleep 1m & sleeppid=$! fi done warnminutes=$warninterval fi done fi local warnseconds=120 warnintervals=( 90 60 45 30 20 15 10 5 0 ) if (( warnminutes == 1 )); then warnseconds=60 warnintervals=( 45 30 20 15 10 5 0 ) fi for warninterval in "${warnintervals[@]}"; do sleep $(( warnseconds - warninterval ))s & sleeppid=$! if [ "$(getServerPID)" != "$pid" ]; then echo "Server has stopped. Aborting update" rm -f "${arkserverroot}/${arkwarnlockfile}" return 1 fi printWarnMessage "$1" "$2" "seconds" "$warnseconds" "$usenotify" usenotify= if (( warnseconds >= 20 )); then if [ "$arkprecisewarn" != "true" ]; then numplayers=$(numPlayersConnected) echo "There are ${numplayers} players connected" if [[ "${numplayers}" == "-1" ]]; then echo "Server is not running. Shutting down immediately" notify "${notifyMsgServerNotRunning:-Server is not running. Shutting down immediately}" return 0 elif (( (numplayers + 0) == 0 )); then doBroadcastWithEcho "Nobody is connected. Shutting down immediately" notify "${notifyMsgNobodyConnected:-Nobody is connected. Shutting down immediately}" rm -f "${arkserverroot}/${arkwarnlockfile}" return 0 fi fi if isUpdateCancelRequested; then doBroadcastWithEcho "Restart cancelled by player request" notify "${notifyMsgRestartCancelled:-Restart cancelled by player request}" return 1 fi fi wait $sleeppid warnseconds=$warninterval done fi rm -f "${arkserverroot}/${arkwarnlockfile}" if [ "$(getServerPID)" != "$pid" ]; then echo "Server has stopped. Aborting $1" return 1 fi return 0 ) return $? } # # Stop the server, update it and then start it back. # doUpdate() { local appupdate= local bgupdate= local updatetype=normal local validate= local modupdate= local saveworld= local downloadonly= local nodownload= local noautostart= local use_systemd= local use_service= local use_upstart= local force= local safeupdate= local appbeta= local appbetapass= local mapfile= for arg in "$@"; do case "$arg" in --force) appupdate=1; force=1; ;; --safe) safeupdate=1; ;; --warn) updatetype=warn; ;; --ifempty) updatetype=ifempty; ;; --warnreason=*) warnreason="${arg#--warnreason=}"; updatetype=warn; ;; --validate) validate=validate; appupdate=1; force=1; ;; --saveworld) saveworld=1; ;; --update-mods) modupdate=1; ;; --backup) arkBackupPreUpdate=true; ;; --no-autostart) noautostart=1; ;; --stagingdir=*) arkStagingDir="${arg#--stagingdir=}"; ;; --downloadonly) downloadonly=1; ;; --no-download) nodownload=1; ;; --systemd) use_systemd=1; ;; --service) use_service=1; ;; --upstart) use_upstart=1; ;; --beta=*) appbeta="${arg#--beta=}"; ;; --betapassword=*) appbetapass="${arg#--betapassword=}"; ;; *) echo "Unrecognized option $arg" echo "Try 'arkmanager -h' or 'arkmanager --help' for more information." exit 1 esac done # check if the server was alive before the update so we can launch it back after the update serverWasAlive=0 if isTheServerRunning ;then serverWasAlive=1 fi echo "${BASHPID}" >"${arkserverroot}/${arkupdatelockfile}.${BASHPID}" 2>/dev/null while true; do if ! ln "${arkserverroot}/${arkupdatelockfile}.${BASHPID}" "${arkserverroot}/${arkupdatelockfile}" 2>/dev/null; then local lockpid="$(<"${arkserverroot}/${arkupdatelockfile}")" if [ -n "$lockpid" ] && [ "$lockpid" != "${BASHPID}" ] && kill -0 "$lockpid" 2>/dev/null; then logprint "Update already in progress (PID: $lockpid)" rm -f "${arkserverroot}/${arkupdatelockfile}.${BASHPID}" 2>/dev/null return 1 fi rm -f "${arkserverroot}/${arkupdatelockfile}" else break fi done rm -f "${arkserverroot}/${arkupdatelockfile}.${BASHPID}" logprint "Checking for update; PID: ${BASHPID}" if [ -n "$modupdate" ]; then if [ -z "$nodownload" ]; then if ! doDownloadAllMods; then modupdate= fi fi if ! isAnyModUpdateNeeded; then modupdate= fi fi cd "$arkserverroot" if [ -n "$appupdate" ] || isUpdateNeeded; then appupdate=1 if [ -n "${arkStagingDir}" ] && [ "${arkStagingDir}" != "${arkserverroot}" ]; then if [ ! -d "$arkStagingDir/${arkserverdir}" ]; then logprint "Copying to staging directory" mkdir -p "$arkStagingDir" if [ "$(stat -c "%d" "$arkserverroot")" == "$(stat -c "%d" "$arkStagingDir")" ]; then if [ -n "$useRefLinks" ]; then cp -a --reflink=always "$arkserverroot/${arkserverdir}/." "$arkStagingDir/${arkserverdir}" cp -a --reflink=always "$arkserverroot/Engine/." "$arkStagingDir/Engine" cp -a --reflink=always "$arkserverroot/linux64/." "$arkStagingDir/linux64" cp -a --reflink=always "$arkserverroot/${arkserverdir}/Content/Mods/111111111/." "$arkStagingDir/${arkserverdir}/Content/Mods/111111111" cp --reflink=always "$arkserverroot/${arkserverdir}/Content/Mods/111111111.mod" "$arkStagingDir/${arkserverdir}/Content/Mods/111111111.mod" cp --reflink=always "$arkserverroot/"* "$arkStagingDir" >/dev/null 2>&1 cp -a --reflink=always "$arkserverroot/steamapps/." "$arkStagingDir/steamapps" else cp -al "$arkserverroot/${arkserverdir}/." "$arkStagingDir/${arkserverdir}" cp -al "$arkserverroot/Engine/." "$arkStagingDir/Engine" cp -al "$arkserverroot/linux64/." "$arkStagingDir/linux64" cp -al "$arkserverroot/${arkserverdir}/Content/Mods/111111111/." "$arkStagingDir/${arkserverdir}/Content/Mods/111111111" cp -l "$arkserverroot/${arkserverdir}/Content/Mods/111111111.mod" "$arkStagingDir/${arkserverdir}/Content/Mods/111111111.mod" cp -l "$arkserverroot/"* "$arkStagingDir" >/dev/null 2>&1 cp -a "$arkserverroot/steamapps/." "$arkStagingDir/steamapps" fi else rsync -a "$arkserverroot/." "$arkStagingDir/." fi rm -rf "$arkStagingDir/${arkserverdir}/Content/Mods/"* rm -rf "$arkStagingDir/${arkserverdir}/Saved/"* rm -rf "$arkStagingDir/Engine/Binaries/ThirdParty/SteamCMD/Linux/steamapps" fi rm -f "$arkStagingDir/${arkserverdir}/Saved/".[^.]* rm -f "$arkStagingDir/${arkserverdir}/Binaries/Linux/"*.txt if [ -z "$nodownload" ]; then echo -n "Downloading ARK update" logprint "Downloading ARK update" >/dev/null doDownloadSteamCMD cd "$steamcmdroot" runSteamCMDAppUpdate "$arkStagingDir" ${appbeta:+-beta} $appbeta ${appbetapass:+-betapassword} $appbetapass $validate result=$? # shellcheck disable=SC2181 if [ $result -eq 0 ]; then rm -rf "${arkStagingDir}/steamapps/downloading/${appid}" elif [ $result -eq 5 ]; then echo "User ${steamlogin:-anonymous} login failed - please login to steamcmd manually" fi if [ -d "${arkStagingDir}/steamapps/downloading/${appid}" ]; then logprint "Update download interrupted" return 1 fi local curver="$(getCurrentVersion)" local newver="$(getStagingVersion)" local nextver="$(getAvailableVersion)" if [[ -z "${newver}" || "$curver" == "$newver" ]]; then if [ -z "$force" ]; then logprint "Update download unsuccessful" return 1 elif [ "${newver}" != "${nextver}" ]; then logprint "Warning: staging directory update was unsuccessful" fi fi fi fi fi if [[ -f "$arkserverroot/$arkmanagerpidfile" && "$arkserverroot/$arkmanagerpidfile" -ot "${arkserverroot}/${arkupdatetimefile}" ]]; then local mgrpid="$(<"$arkserverroot/$arkmanagerpidfile")" if (( mgrpid != 0 )); then logprint "Server was updated while it was running" bgupdate=1 fi fi if [ -n "$downloadonly" ]; then if [ -n "$appupdate" ] && [ -n "$arkStagingDir" ] && [ "$arkStagingDir" != "$arkserverroot" ]; then logprint "Server update downloaded" fi if [ -n "$modupdate" ]; then logprint "Mod update downloaded" fi logprint "Not applying update - download-only requested" elif [ -n "$appupdate" ] || [ -n "$modupdate" ] || [ -n "$bgupdate" ]; then # version.txt is frequently missing, empty, or out of date if false && [ -f "$arkserverroot/version.txt" ]; then arkversion="$(<"$arkserverroot/version.txt")" else arkversion="$(getCurrentVersion)" fi if isTheServerRunning; then if [ "$updatetype" == "warn" ]; then if ! doWarn update; then return 1 fi elif [ "$updatetype" == "ifempty" ]; then numplayers=$(( $(numPlayersConnected) + 0 )) if (( numplayers != 0 )); then logprint "${numplayers} players are still connected" return 1 fi fi if [ -n "$safeupdate" ]; then logprint "Saving world" doSaveWorld saverootdir="${arkserverroot}/${arkserverdir}/Saved" savedir="$(getSavedArksDirectory "${saverootdir}")" mapname="$(getServerMapName)" maxwait=30 if [ -z "$savedir" ]; then logprint "Unable to find saved arks directory" else mapfile="${savedir}/${mapname}.ark" if [ ! -f "${mapfile}" ]; then sleep 2 fi if [ ! -f "${mapfile}" ]; then if [ -f "${mapfile%.ark}.tmp" ]; then logprint "Saved ark file doesn't exist, but temporary file does" else logprint "Unable to find saved ark file" fi else for (( i = 0; i < maxwait; i++ )); do if [ "$(find "${mapfile}" -mmin -1)" ]; then break fi logprint "Save file older than 1 minute. Delaying update." sleep 30s done logprint "Save file newer than 1 minute. Performing an update." fi fi fi fi if [ -n "$saveworld" ]; then logprint "Saving world" doSaveWorld fi doStop update # If user wants to back-up, we do it here. if [ "$arkBackupPreUpdate" == "true" ]; then doBackup fi if [ -n "$appupdate" ]; then if [ -d "${arkStagingDir}" ] && [ "${arkStagingDir}" != "${arkserverroot}" ]; then logprint "Applying update from staging directory" if [ "$(stat -c "%d" "$arkserverroot")" == "$(stat -c "%d" "$arkStagingDir")" ]; then if [ -n "$useRefLinks" ]; then cp -au --reflink=always --remove-destination "$arkStagingDir/${arkserverdir}/." "$arkserverroot/${arkserverdir}" cp -au --reflink=always --remove-destination "$arkStagingDir/Engine/." "$arkserverroot/Engine" cp -au --reflink=always --remove-destination "$arkStagingDir/linux64/." "$arkserverroot/linux64" cp -u --reflink=always --remove-destination "$arkStagingDir/"* "$arkserverroot" >/dev/null 2>&1 cp -au --reflink=always --remove-destination "$arkStagingDir/steamapps/." "$arkserverroot/steamapps" else cp -alu --remove-destination "$arkStagingDir/${arkserverdir}/." "$arkserverroot/${arkserverdir}" cp -alu --remove-destination "$arkStagingDir/Engine/." "$arkserverroot/Engine" cp -alu --remove-destination "$arkStagingDir/linux64/." "$arkserverroot/linux64" cp -lu --remove-destination "$arkStagingDir/"* "$arkserverroot" >/dev/null 2>&1 cp -au --remove-destination "$arkStagingDir/steamapps/." "$arkserverroot/steamapps" fi else rsync -a "$arkStagingDir/." "$arkserverroot" fi cd "$arkserverroot" find Engine "${arkserverdir}" linux64 -depth -print | grep -v '^\('"${arkserverdir}"'/\(Saved\|Content/Mods\|Binaries/Linux/.*\.txt\)\|Engine/Binaries/ThirdParty/SteamCMD/Linux/steamapps\)' | while read -r f; do if [ ! -e "${arkStagingDir}/${f}" ]; then if [ -f "$f" ]; then rm "${f}" else rmdir "${f}" fi fi done for f in *; do if [[ -f "${f}" && ! -e "${arkStagingDir}/${f}" ]]; then rm "${f}" fi done else echo -n "Performing ARK update" logprint "Performing ARK update" >/dev/null doDownloadSteamCMD cd "$steamcmdroot" runSteamCMDAppUpdate "$arkserverroot" ${appbeta:+-beta} $appbeta ${appbetapass:+-betapassword} $appbetapass $validate if [ $? -eq 5 ]; then echo "User ${steamlogin:-anonymous} login failed - please login to steamcmd manually" fi fi # the current version should be the last version. We set our version instver="$(getCurrentVersion)" logprint "Update to $instver complete" fi if [ -n "$modupdate" ] && [ -z "$arkflag_automanagedmods" ]; then for modid in $(getModIds); do if isModUpdateNeeded "$modid"; then logprint "Updating mod $modid" doExtractMod "$modid" logprint "Mod $modid updated" fi done fi if [ -z "$bgupdate" ]; then touch "${arkserverroot}/${arkupdatetimefile}" fi else logprint "Your server is already up to date! The most recent version is ${bnumber}." fi; rm -f "${arkserverroot}/${arkupdatelockfile}" if ! isTheServerRunning; then # we restart the server only if it was started before the update if [ -z "$noautostart" ]; then if [ $serverWasAlive -eq 1 ] || [ -f "${arkserverroot}/.startAfterUpdate-${instance}" ]; then rm -f "${arkserverroot}/.startAfterUpdate-${instance}" if [ -n "$use_systemd" ]; then sudo systemctl start "arkmanager@$instance" elif [ -n "$use_service" ]; then if [ -f "/etc/init.d/arkmanager" ]; then sudo "/etc/init.d/arkmanager" start "$instance" elif [ -f "/etc/rc.d/init.d/arkmanager" ]; then sudo "/etc/rc.d/init.d/arkmanager" start "$instance" fi elif [ -n "$use_upstart" ]; then sudo start arkmanager "service=$instance" else doStart --noautoupdate fi fi fi fi } # # Check if any mod update is available # checkForModUpdate(){ local updateavail= local modname= local steamworkshopdir="$(getSteamWorkshopDir)" local cancheckmodavail=1 local modmissing= local revstatcode= local alluptodate=1 if [[ " $* " =~ " --revstatus " ]]; then revstatcode=1 fi if [[ " $* " =~ " --skip-workshop-dir " ]]; then cancheckmodavail= else if [ ! -d "${steamworkshopdir}" ]; then echo "Error: ${steamworkshopdir} does not exist" if [ -n "$revstatcode" ]; then return 4; else return 0; fi fi if [ ! -f "${steamworkshopdir}/appworkshop_${mod_appid}.acf" ]; then echo "Error: appworkshop_${mod_appid}.acf not found at ${steamworkshopdir}" cancheckmodavail= fi fi for modid in $(getModIds); do availupd="$(getAvailModLastUpdated "$modid")" instupd="$(getInstalledModLastUpdated "$modid")" modname="$(getModName "$modid")" if [ -z "$availupd" ]; then printf "Mod %d doesn't exist in the steam workshop\n" "$modid" modmissing=1 elif [ "$instupd" != "$availupd" ]; then alluptodate= if [ -n "$cancheckmodavail" ]; then localupd="$(getLocalModLastUpdated "$modid")" if [ -z "$localupd" ]; then printf "Mod %d [%s] has not been downloaded\n" "$modid" "$modname" updateavail=1 elif [ "$availupd" != "$localupd" ]; then printf "Mod %d [%s] has been updated on the Steam workshop\n" "$modid" "$modname" printf "Local last updated: %s\nSteam last updated: %s\n" "$(date --date="@$localupd")" "$(date --date="@$availupd")" updateavail=1 fi fi fi if [ ! -d "$arkserverroot/${arkserverdir}/Content/Mods/$modid" ]; then printf "Mod %d [%s] is not installed\n" "$modid" "$modname" updateavail=1 elif [ "$instupd" != "$availupd" ]; then if isModUpdateNeeded "$modid"; then printf "Mod %d [%s] update needs to be applied\n" "$modid" "$modname" updateavail=1 elif [ -z "$cancheckmodavail" ]; then printf "Mod %d [%s] update needs to be downloaded and applied\n" "$modid" "$modname" updateavail=1 fi fi done if [ -n "$updateavail" ]; then echo "One or more updates are available" if [ -n "$revstatcode" ]; then return 1; else return 0; fi elif [ -n "$alluptodate" ]; then echo "All mods are up to date" if [ -n "$revstatcode" ]; then return 0; else return 1; fi elif [ -z "$cancheckmodavail" ]; then echo "Cannot check if mods are up to date" if [ -n "$revstatcode" ]; then return 3; else return 0; fi elif [ -n "$modmissing" ]; then echo "One or more mods are unavailable" return 2 else echo "All mods are up to date" if [ -n "$revstatcode" ]; then return 0; else return 1; fi fi } # # Get the Mod IDs of the installed mods and the requested mods # getModIds(){ ( echo "${serverMapModId}" echo "${ark_TotalConversionMod}" echo "${ark_GameModIds}" | tr ',' '\n' # shellcheck disable=SC2154 for v in "${!arkmod_@}"; do if [ "${!v}" != "disabled" ]; then echo "${v#arkmod_}" fi done if [ -z "$ignoreInstalledMods" ]; then find "${arkserverroot}/${arkserverdir}/Content/Mods" -maxdepth 1 -type d -printf "%P\n" fi getCollectionMods ) | sort | uniq | grep '^[1-9][0-9]*$' | grep -v '^111111111$' } # # Get Mod IDs included in collections in rkModCollections # getCollectionMods(){ ( for m in ${arkModCollections//,/\ }; do curl \ -s \ -d \ "collectioncount=1&publishedfileids[0]=${m}" \ "http://api.steampowered.com/ISteamRemoteStorage/GetCollectionDetails/v1" | perl -e ' use JSON; my $js = decode_json <>; my $coll = ${$js}{response}{collectiondetails}[0]; if (exists ${$coll}{children}) { foreach my $mod (@{${$coll}{children}}) { print "${$mod}{publishedfileid}\n"; } } else { print "${$coll}{publishedfileid}\n"; } ' 2>/dev/null done ) | sort | uniq | grep '^[1-9][0-9]*$' } # # Get the Mod details of the installed mods and the requested mods # listMods(){ local modlist local modid moddir declare -A modlist if [ -n "${serverMapModId}" ]; then modlist[${serverMapModId}]="serverMapModId" fi if [ -n "${ark_TotalConversionMod}" ]; then modlist[${ark_TotalConversionMod}]="ark_TotalConversionMod" fi if [ -n "${ark_GameModIds}" ]; then for modid in ${ark_GameModIds//,/ }; do modlist[${modid}]="ark_GameModIds" done fi for modid in "${!arkmod_@}"; do if [ "${!modid}" != "disabled" ]; then modlist[${modid#arkmod_}]="${modid}" fi done if [ -z "$ignoreInstalledMods" ]; then for moddir in "${arkserverroot}/${arkserverdir}/Content/Mods"/*; do modid="${moddir##*/}" if [[ "${modid}" =~ ^[1-9][0-9]*$ && "${modid}" != "111111111" ]]; then modlist[${moddir##*/}]="Content/Mods" fi done fi for modid in "${!modlist[@]}"; do printf " %10d: %-20s %s\n" "$modid" "[${modlist[$modid]}]" "$(getModName $modid 2>/dev/null)" done } # # Gets installed mod last update timestamp # getInstalledModLastUpdated(){ local modid="$1" if [ ! -f "${arkserverroot}/${arkserverdir}/Content/Mods/${modid}.mod" ]; then return 0; fi if [ -f "${arkserverroot}/${arkserverdir}/Content/Mods/${modid}/__modversion__.info" ]; then cat "${arkserverroot}/${arkserverdir}/Content/Mods/${modid}/__modversion__.info" else stat -c "%Y" "${arkserverroot}/${arkserverdir}/Content/Mods/${modid}.mod" fi } # # Gets local mod last update timestamp # getLocalModLastUpdated(){ local modid="$1" local steamworkshopdir="$(getSteamWorkshopDir)" if [ ! -f "${steamworkshopdir}/appworkshop_${mod_appid}.acf" ]; then return 0; fi local instupd="$(sed -n '/^\t"WorkshopItemsInstalled"$/,/^\t[}]$/{/^\t\t"'"${modid}"'"$/,/^\t\t[}]$/{s|^\t\t\t"timeupdated"\t\t"\(.*\)"$|\1|p}}' <"${steamworkshopdir}/appworkshop_${mod_appid}.acf")" echo "$instupd" } # # Gets available mod last update timestamp # getAvailModLastUpdated(){ local modid="$1" local serverresp="$(curl -s -d "itemcount=1&publishedfileids[0]=${modid}" "http://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1")" local remupd= if [[ "$serverresp" =~ \"time_updated\":[[:space:]]*([^,]*) ]]; then remupd="${BASH_REMATCH[1]}" fi echo "$remupd" } # # Checks if a mod update is available before trying to download it isModUpdateAvailable(){ local modid="$1" local remupd="$(getAvailModLastUpdated "$modid")" local instupd="$(getInstalledModLastUpdated "$modid")" if [[ -n "$remupd" && "$instupd" == "$remupd" ]]; then return 1 fi local steamworkshopdir="$(getSteamWorkshopDir)" if [ ! -f "${steamworkshopdir}/appworkshop_${mod_appid}.acf" ]; then return 0; fi local localupd="$(getLocalModLastUpdated "$modid")" if [[ -n "${remupd}" && "${localupd}" != "${remupd}" ]]; then return 0 # true fi return 1 # false } # # Downloads a mod from the Steam workshop # doDownloadMod(){ local modid=$1 local steamcmdroot="$steamcmdroot" if [ -n "$arkflag_automanagedmods" ]; then steamcmdroot="$arkserverroot/Engine/Binaries/ThirdParty/SteamCMD/Linux" fi doDownloadSteamCMD local steamworkshopdir="$(getSteamWorkshopDir)" local steamcmd_workshoplog="$(getSteamWorkshopLog)" local modsrcdir="${steamworkshopdir}/content/$mod_appid/$modid" local moddldir="${steamworkshopdir}/downloads/$mod_appid" cd "$steamcmdroot" retries=10 # Bypass the 111111111 modid used by Primitive+ if [ "$modid" = "111111111" ]; then return 0 fi while true; do echo -n "Downloading mod $modid" local output output=$(runSteamCMDspinnerSubst 5 +workshop_download_item "$mod_appid" "$modid") result=$? if [ $result -eq 0 ]; then modsrcdir="$(echo "$output" | sed -n 's@^Success. Downloaded item [0-9][0-9]* to "\([^"]*\)" .*@\1@p')" break elif [ $result -eq 5 ]; then echo "User ${steamlogin:-anonymous} login failed - please login to steamcmd manually" return 1 else echo failedmod="$(tail -n 20 "${steamcmd_workshoplog}" | sed -n 's|.* Download item \([0-9]*\) result : \(.*\)|\1\t\2|p' | grep -v $'\tOK' | tail -n1 | cut -f1)" if [[ -n "$failedmod" && "$failedmod" != "$modid" ]]; then echo "Mod $failedmod prevented mod update - removing failed mod" doRemoveMods "$failedmod" elif [ ! -d "$moddldir" ]; then echo "Mod $modid download failed" break fi (( retries = retries - 1 )) if (( retries <= 0 )); then echo "Retries exhausted" fi echo "Mod $modid not fully downloaded - retrying" fi done 5> >(cat) if [ -f "$modsrcdir/mod.info" ]; then echo "Mod $modid downloaded" modsrcdirs[$modid]="$modsrcdir" return 0 else echo "Mod $modid was not successfully downloaded" return 1 fi } # # Downloads all installed and requested mods from the Steam workshop # doDownloadAllMods(){ local fail=0 local success=0 for modid in $(getModIds); do if isModUpdateAvailable "$modid"; then if doDownloadMod "$modid"; then success=1 else fail=1 fi fi done [[ $success == 1 || $fail == 0 ]] && return 0 || return 1 } # # Checks if the files a mod owns need to be updated # isModUpdateNeeded(){ local modid=$1 local steamworkshopdir="$(getSteamWorkshopDir)" local modsrcdir="${steamworkshopdir}/content/$mod_appid/$modid" local moddestdir="$arkserverroot/${arkserverdir}/Content/Mods/$modid" local modbranch="${mod_branch:-Windows}" # Bypass the 111111111 modid used by Primitive+ if [ "$modid" = "111111111" ]; then return 1 fi if [ -n "${modsrcdirs[$modid]}" ]; then modsrcdir="${modsrcdirs[$modid]}" fi # shellcheck disable=SC2154 for varname in "${!mod_branch_@}"; do if [ "mod_branch_$modid" == "$varname" ]; then modbranch="${!varname}" fi done if [ -f "$moddestdir/.modbranch" ]; then mv "$moddestdir/.modbranch" "$moddestdir/__arkmanager_modbranch__.info" fi if [ ! -f "$moddestdir/__arkmanager_modbranch__.info" ] || [ "$(<"$moddestdir/__arkmanager_modbranch__.info")" != "$modbranch" ]; then return 0 fi if [ -f "$modsrcdir/mod.info" ]; then if [ -f "$modsrcdir/${modbranch}NoEditor/mod.info" ]; then modsrcdir="$modsrcdir/${modbranch}NoEditor" fi while read -r f; do if [ ! -f "$moddestdir/${f%.z}" ] || [ "$modsrcdir/$f" -nt "$moddestdir/${f%.z}" ]; then return 0 fi done < <(find "$modsrcdir" -type f ! -name "*.z.uncompressed_size" -printf "%P\n") fi return 1 } # # Get the name of the specified mod # getModName(){ local modid=$1 local steamworkshopdir="$(getSteamWorkshopDir)" local modsrcdir="${steamworkshopdir}/content/$mod_appid/$modid" if [ -n "${modsrcdirs[$modid]}" ]; then modsrcdir="${modsrcdirs[$modid]}" fi modname="$(curl -s "https://steamcommunity.com/sharedfiles/filedetails/?id=${modid}" | sed -n 's|^.*
\([^<]*\)
.*|\1|p')" if [ -n "$modname" ]; then echo "$modname" else perl -e ' my $data; { local $/; $data = ; } my $mapnamelen = unpack("@0 L<", $data); my $mapname = substr($data, 4, $mapnamelen - 1); print $mapname ' <"${modsrcdir}/mod.info" fi } # # Checks if any installed or requested mods need to be updated # isAnyModUpdateNeeded(){ modnamesupdated="" local ismodupdateneeded=1 for modid in $(getModIds); do if isModUpdateNeeded "$modid"; then ismodupdateneeded=0 if [ -n "$modnamesupdated" ]; then modnamesupdated="${modnamesupdated}, " fi modnamesupdated="${modnamesupdated}$(getModName "$modid")" fi done return $ismodupdateneeded } # # Extracts a mod into the ARK Mods directory # doExtractMod(){ local modid=$1 local steamworkshopdir="$(getSteamWorkshopDir)" local modsrcdir="${steamworkshopdir}/content/$mod_appid/$modid" local moddestdir="$arkserverroot/${arkserverdir}/Content/Mods/$modid" local modextractdir="$moddestdir" local modbranch="${mod_branch:-Windows}" if [[ -n "$arkStagingDir" && -d "$arkStagingDir" ]]; then modextractdir="$arkStagingDir/Mods/$modid" fi # Bypass the 111111111 modid used by Primitive+ if [ "$modid" = "111111111" ]; then return 0 fi if [ -n "${modsrcdirs[$modid]}" ]; then modsrcdir="${modsrcdirs[$modid]}" fi for varname in "${!mod_branch_@}"; do if [ "mod_branch_$modid" == "$varname" ]; then modbranch="${!varname}" fi done if [ -f "$modextractdir/.modbranch" ]; then mv "$modextractdir/.modbranch" "$modextractdir/__arkmanager_modbranch__.info" fi if [ ! -f "$modextractdir/__arkmanager_modbranch__.info" ] || [ "$(<"$modextractdir/__arkmanager_modbranch__.info")" != "$modbranch" ]; then rm -rf "$modextractdir" fi if [ -f "$modsrcdir/mod.info" ]; then local modupdatetime="$(getLocalModLastUpdated "$modid")" echo "Copying files to $modextractdir" if [ -f "$modsrcdir/${modbranch}NoEditor/mod.info" ]; then modsrcdir="$modsrcdir/${modbranch}NoEditor" fi find "$modsrcdir" -type d -printf "$modextractdir/%P\0" | xargs -0 -r mkdir -p find "$modextractdir" -type f ! -name '.*' -printf "%P\n" | while read -r f; do if [ ! -f "$modsrcdir/$f" ] && [ ! -f "$modsrcdir/${f}.z" ]; then rm "$modextractdir/$f" fi done find "$modextractdir" -depth -type d -printf "%P\n" | while read -r d; do if [ ! -d "$modsrcdir/$d" ]; then rmdir "$modextractdir/$d" fi done find "$modsrcdir" -type f ! \( -name '*.z' -or -name '*.z.uncompressed_size' \) -printf "%P\n" | while read -r f; do if [ ! -f "$modextractdir/$f" ] || [ "$modsrcdir/$f" -nt "$modextractdir/$f" ]; then printf "%10d %s " "$(stat -c '%s' "$modsrcdir/$f")" "$f" if [[ -n "$useRefLinks" && "$(stat -c "%d" "$modsrcdir")" == "$(stat -c "%d" "$modextractdir")" ]]; then cp --reflink=auto "$modsrcdir/$f" "$modextractdir/$f" else cp "$modsrcdir/$f" "$modextractdir/$f" fi echo -ne "\r\\033[K" fi done find "$modsrcdir" -type f -name '*.z' -printf "%P\n" | while read -r f; do if [ ! -f "$modextractdir/${f%.z}" ] || [ "$modsrcdir/$f" -nt "$modextractdir/${f%.z}" ]; then printf "%10d %s " "$(stat -c '%s' "$modsrcdir/$f")" "${f%.z}" perl -M'Compress::Raw::Zlib' -e ' my $sig; read(STDIN, $sig, 8) or die "Unable to read compressed file: $!"; if ($sig != "\xC1\x83\x2A\x9E\x00\x00\x00\x00"){ die "Bad file magic"; } my $data; read(STDIN, $data, 24) or die "Unable to read compressed file: $!"; my ($chunksizelo, $chunksizehi, $comprtotlo, $comprtothi, $uncomtotlo, $uncomtothi) = unpack("(LLLLLL)<", $data); my @chunks = (); my $comprused = 0; while ($comprused < $comprtotlo) { read(STDIN, $data, 16) or die "Unable to read compressed file: $!"; my ($comprsizelo, $comprsizehi, $uncomsizelo, $uncomsizehi) = unpack("(LLLL)<", $data); push @chunks, $comprsizelo; $comprused += $comprsizelo; } foreach my $comprsize (@chunks) { read(STDIN, $data, $comprsize) or die "File read failed: $!"; my ($inflate, $status) = new Compress::Raw::Zlib::Inflate(); my $output; $status = $inflate->inflate($data, $output, 1); if ($status != Z_STREAM_END) { die "Bad compressed stream; status: " . ($status); } if (length($data) != 0) { die "Unconsumed data in input" } print $output; } ' <"$modsrcdir/$f" >"$modextractdir/${f%.z}" touch -c -r "$modsrcdir/$f" "$modextractdir/${f%.z}" echo -ne "\r\\033[K" fi done modname="$(curl -s "http://steamcommunity.com/sharedfiles/filedetails/?id=${modid}" | sed -n 's|^.*
\([^<]*\)
.*|\1|p')" if [ -f "${modextractdir}/.mod" ]; then rm "${modextractdir}/.mod" fi perl -e ' my $data; { local $/; $data = ; } my $mapnamelen = unpack("@0 L<", $data); my $mapname = substr($data, 4, $mapnamelen - 1); my $nummaps = unpack("@" . ($mapnamelen + 4) . " L<", $data); my $pos = $mapnamelen + 8; my $modname = ($ARGV[2] || $mapname) . "\x00"; my $modnamelen = length($modname); my $modpath = "../../../" . $ARGV[0] . "/Content/Mods/" . $ARGV[1] . "\x00"; my $modpathlen = length($modpath); print pack("L< L< L< Z$modnamelen L< Z$modpathlen L<", $ARGV[1], 0, $modnamelen, $modname, $modpathlen, $modpath, $nummaps); for (my $mapnum = 0; $mapnum < $nummaps; $mapnum++){ my $mapfilelen = unpack("@" . ($pos) . " L<", $data); my $mapfile = substr($data, $mapnamelen + 12, $mapfilelen); print pack("L< Z$mapfilelen", $mapfilelen, $mapfile); $pos = $pos + 4 + $mapfilelen; } print "\x33\xFF\x22\xFF\x02\x00\x00\x00\x01"; ' "$arkserverdir" "$modid" "$modname" <"$modextractdir/mod.info" >"${modextractdir}.mod" if [ -f "$modextractdir/modmeta.info" ]; then cat "$modextractdir/modmeta.info" >>"${modextractdir}.mod" else echo -ne '\x01\x00\x00\x00\x08\x00\x00\x00ModType\x00\x02\x00\x00\x001\x00' >>"${modextractdir}.mod" fi if [ -n "${modupdatetime}" ]; then touch -c --date="@${modupdatetime}" -- "${modextractdir}.mod" echo "${modupdatetime}" >"${modextractdir}/__modversion__.info" fi echo "$modbranch" >"$modextractdir/__arkmanager_modbranch__.info" if [[ "$modextractdir" != "$moddestdir" ]]; then if [ ! -d "${moddestdir}" ]; then mkdir -p "${moddestdir}" fi if [ "$(stat -c "%d" "$modextractdir")" == "$(stat -c "%d" "$moddestdir")" ]; then if [ -n "$useRefLinks" ]; then cp -au --reflink=always --remove-destination "${modextractdir}/." "${moddestdir}" else cp -alu --remove-destination "${modextractdir}/." "${moddestdir}" fi else cp -au --remove-destination "${modextractdir}/." "${moddestdir}" fi find "${moddestdir}" -type f ! -name '.*' -printf "%P\n" | while read -r f; do if [ ! -f "${modextractdir}/${f}" ]; then rm "${moddestdir}/${f}" fi done find "$modextractdir" -depth -type d -printf "%P\n" | while read -r d; do if [ ! -d "$modsrcdir/$d" ]; then rmdir "$modextractdir/$d" fi done if [[ -n "$useRefLinks" && "$(stat -c "%d" "$modextractdir")" == "$(stat -c "%d" "$moddestdir")" ]]; then cp -u --reflink=always "${modextractdir}.mod" "${moddestdir}.mod" else cp -u "${modextractdir}.mod" "${moddestdir}.mod" fi fi fi } # # Downloads mod and installs it into mods directory # doInstallMod(){ local modid local steamworkshopdir="$(getSteamWorkshopDir)" for modid in ${1//,/ }; do if [[ " $* " = *" --validate "* ]]; then if [ -f "${steamworkshopdir}/appworkshop_${mod_appid}.acf" ]; then sed -i "/^\\t\\t\"${modid}\"/,/^\\t\\t}/d" "${steamworkshopdir}/appworkshop_${mod_appid}.acf" fi fi if doDownloadMod "$modid"; then doExtractMod "$modid" echo "Mod $modid installed" fi done } # # Downloads and installs all requested mods # doInstallAllMods(){ for modid in $(getModIds); do doInstallMod "$modid" "$@" done } # # Removes all mods from the mods directory # doUninstallAllMods(){ for modid in $(getModIds); do if [[ "$modid" != "111111111" && "$modid" != "TheCenter" ]]; then doUninstallMod "$modid" fi done } # # Removes mod from mods directory # doUninstallMod(){ local modid for modid in ${1//,/ }; do local moddir="$arkserverroot/${arkserverdir}/Content/Mods/$modid" local modfile="$arkserverroot/${arkserverdir}/Content/Mods/${modid}.mod" if [ -d "${moddir}" ]; then rm -rf "${moddir}" fi if [ -f "${modfile}" ]; then rm -f "$modfile" fi done } # # Enables a mod in the config # doEnableMod(){ local modid="${1%=*}" local modtype="${1#*=}" if [ "$modtype" = "$1" ]; then modtype=game fi local modvar="arkmod_${modid}" if [ -n "${!modvar}" ]; then sed -i "s|^\(${modvar}\)=[^ ]*|\1=${modtype}|" "$configfile" else echo "${modvar}=${modtype}" >>"$configfile" fi } # # Disable a mod in the config # doDisableMod(){ local modid="$1" local modvar="arkmod_$modid" if [[ "$ark_GameModIds" = *"$modid"* ]]; then sed -i "s|^\(ark_GameModIds=\(\|[\"']\)\(\|[^\"' ]*,\)\)${modid},*|\1|" "$configfile" fi if [ -n "$modvar" ]; then sed -i "s|^\(arkmod_${modid}\)=[^ ]*|\1=disabled|" "$configfile" fi } # # Removes mod from steamcmd workshop directory # doRemoveMods(){ local modid local steamworkshopdir="$(getSteamWorkshopDir)" for modid in ${1//,/ }; do if [ -f "${steamworkshopdir}/appworkshop_${mod_appid}.acf" ]; then sed -i "/^\\t\\t\"${modid}\"/,/^\\t\\t}/d" "${steamworkshopdir}/appworkshop_${mod_appid}.acf" fi if [ -d "${steamworkshopdir}/content/${mod_appid}/${modid}" ]; then rm -rf "${steamworkshopdir}/content/${mod_appid}/${modid}" fi if [ -d "${steamworkshopdir}/downloads/${mod_appid}/${modid}" ]; then rm -rf "${steamworkshopdir}/downloads/${mod_appid}/${modid}" fi done } # # Copies server state to a backup directory # doBackup(){ local datestamp="$(date +"%Y-%m-%d_%H.%M.%S")" local daystamp="$(date +"%Y-%m-%d")" local backupdir="${arkbackupdir}/${datestamp}" local backupdirdaily="${arkbackupdir}/${daystamp}" local saverootdir="${arkserverroot}/${arkserverdir}/Saved" local savedcfgdir="${saverootdir}/Config/LinuxServer" local savedir="$(getSavedArksDirectory "${saverootdir}")" local clusterdir="$(getArkServerSetting "ClusterDirOverride" "${saverootdir}/clusters/$(getArkServerSetting "clusterid")")" local mapname="$(getServerMapName)" local backupfile mkdir -p "$backupdir" mkdir -p "$backupdirdaily" for arg in "$@"; do case "$arg" in --allmaps) backupAllMaps=true; ;; --no-allmaps) backupAllMaps=false; ;; --autobackups) includeAutoBackups=true; ;; --no-autobackups) includeAutoBackups=false; ;; --cluster) backupCluster=true; ;; --no-cluster) backupCluster=false; ;; *) echo "Unrecognized option $arg" echo "Try 'arkmanager -h' or 'arkmanager --help' for more information." exit 1 esac done if [[ -z "$savedir" ]]; then return 1 fi echo -e "${NORMAL} Saved arks directory is ${savedir}" # ARK server uses Write-Unlink-Rename echo -ne "${NORMAL} Copying ARK world file (${mapname}) " local mapfile="${savedir}/${mapname}.ark" cp -p "${mapfile}" "${backupdir}/${mapname}.ark" if [ ! -f "${backupdir}/${mapname}.ark" ]; then sleep 2 cp -p "${mapfile}" "${backupdir}/${mapname}.ark" fi if [ ! -f "${backupdir}/${mapname}.ark" ]; then cp -p "${mapfile%.ark}.tmp" "${backupdir}/${mapname}.ark" if [ -f "${backupdir}/${mapname}.ark" ]; then echo "${NORMAL}\e[68G[ ${YELLOW}WARN${NORMAL} ]" logprint "Saved ark file not found, but temporary file was" else cimapfile="$(find "${savedir}" -maxdepth 1 -iname "${mapname}.ark" -or -iname "${mapname}.tmp" | head -n1)" if [ -n "${cimapfile}" ]; then echo "${NORMAL}\e[68G[ ${YELLOW}WARN${NORMAL} ]" logprint "Inconsistent casing in map name - ${mapname}.ark does not exist, but ${cimapfile##*/} does" cp -p "${cimapfile}" "${backupdir}/${mapname}.ark" else echo "${NORMAL}\e[68G[ ${RED}FAILED${NORMAL} ]" logprint "Saved ark file could not be found" logprint "Candidates:" for f in "${savedir}"/*.ark; do if [ "${f}" == "${f%_??.??.????_??.??.??.ark}" ]; then # Exclude auto-backups logprint " Saved ARK ${f}" fi done fi fi else echo -e "${NORMAL}\e[68G[ ${GREEN}OK${NORMAL} ]" fi # Copy other world files if [ -n "$backupAllMaps" ] && [ "$backupAllMaps" != "false" ]; then echo -e "${NORMAL} Copying other ARK world files" for f in "${savedir}/"*.ark; do if [[ "${f}" != "${mapfile}" && "${f}" != "${savedir}/"*_??.??.????_??.??.??.ark ]]; then cp -p "${f}" "${backupdir}/${f##*/}" fi done fi # Copy map autobackups if [ -n "$includeAutoBackups" ] && [ "$includeAutoBackups" != "false" ]; then echo -e "${NORMAL} Copying all ARK world autobackup files" for f in "${savedir}/"*.ark; do if [[ "${f}" == "${savedir}/"*_??.??.????_??.??.??.ark ]]; then cp -p "${f}" "${backupdir}/${f##*/}" fi done fi # ARK server uses Lock-Truncate-Write-Unlock # Unfortunately we can't lock the file, as # ARK server uses a non-blocking lock and will # fail to update the file if the lock fails. echo -e "${NORMAL} Copying ARK profile files" for f in "${savedir}/"*.arkprofile; do if [ -f "${f}" ]; then echo -ne "${NORMAL} ${f##*/} " cp -p "${f}" "${backupdir}/${f##*/}" if [ ! -s "${backupdir}/${f##*/}" ]; then sleep 2 cp -p "${f}" "${backupdir}/${f##*/}" fi # If both attempts fail, server may have # crashed between truncate and write if [[ ! -s "${backupdir}/${f##*/}" && -f "${f%.arkprofile}.tmpprofile" ]]; then cp -p "${f%.arkprofile}.tmpprofile" "${backupdir}/${f##*/}" fi if [ -s "${backupdir}/${f##*/}" ]; then echo -e "${NORMAL}\e[68G[ ${GREEN}OK${NORMAL} ]" else echo -e "${NORMAL}\e[68G[ ${RED}FAILED${NORMAL} ]" fi fi done # ARK server uses Lock-Truncate-Write-Unlock echo -e "${NORMAL} Copying ARK tribe files " for f in "${savedir}/"*.arktribe; do if [ -f "${f}" ]; then echo -ne "${NORMAL} ${f##*/} " cp -p "${f}" "${backupdir}/${f##*/}" if [ ! -s "${backupdir}/${f##*/}" ]; then sleep 2 cp -p "${f}" "${backupdir}/${f##*/}" fi # If both attempts fail, server may have # crashed between truncate and write if [[ ! -s "${backupdir}/${f##*/}" && -f "${f%.arktribe}.tmptribe" ]]; then cp -p "${f%.arktribe}.tmptribe" "${backupdir}/${f##*/}" fi if [ -s "${backupdir}/${f##*/}" ]; then echo -e "${NORMAL}\e[68G[ ${GREEN}OK${NORMAL} ]" else echo -e "${NORMAL}\e[68G[ ${RED}FAILED${NORMAL} ]" fi fi done # ARK server uses Lock-Truncate-Write-Unlock echo -e "${NORMAL} Copying ARK tribute tribe files " for f in "${savedir}/"*.arktributetribe; do if [ -f "${f}" ]; then echo -ne "${NORMAL} ${f##*/} " cp -p "${f}" "${backupdir}/${f##*/}" if [ ! -s "${backupdir}/${f##*/}" ]; then sleep 2 cp -p "${f}" "${backupdir}/${f##*/}" fi # If both attempts fail, server may have # crashed between truncate and write if [[ ! -s "${backupdir}/${f##*/}" && -f "${f%.arktributetribe}.tmptributetribe" ]]; then cp -p "${f%.arktributetribe}.tmptributetribe" "${backupdir}/${f##*/}" fi if [ -s "${backupdir}/${f##*/}" ]; then echo -e "${NORMAL}\e[68G[ ${GREEN}OK${NORMAL} ]" else echo -e "${NORMAL}\e[68G[ ${RED}FAILED${NORMAL} ]" fi fi done if [ -d "${saverootdir}/SaveGames" ]; then echo -e "${NORMAL} Copying mod persistent info " cp -av "${saverootdir}/SaveGames" "${backupdir}/SaveGames" fi if [[ "$backupCluster" == "true" && -d "${clusterdir}" ]]; then echo -e "${NORMAL} Copying cluster files " cp -av "${clusterdir}" "${backupdir}/Cluster" fi # ARK server uses Lock-Truncate-Write-Unlock echo -ne "${NORMAL} Copying GameUserSettings.ini " cp -p "${savedcfgdir}/GameUserSettings.ini" "${backupdir}/GameUserSettings.ini" if [ ! -s "${backupdir}/GameUserSettings.ini" ]; then sleep 2 cp -p "${f}" "${backupdir}/${f##*/}" fi if [ -f "${backupdir}/GameUserSettings.ini" ]; then echo -e "${NORMAL}\e[68G[ ${GREEN}OK${NORMAL} ]" else echo -e "${NORMAL}\e[68G[ ${RED}FAILED${NORMAL} ]" fi echo -ne "${NORMAL} Copying Game.ini " cp -p "${savedcfgdir}/Game.ini" "${backupdir}/Game.ini" if [ ! -s "${backupdir}/Game.ini" ]; then sleep 2 cp -p "${f}" "${backupdir}/${f##*/}" fi if [ -f "${backupdir}/Game.ini" ]; then echo -e "${NORMAL}\e[68G[ ${GREEN}OK${NORMAL} ]" else echo -e "${NORMAL}\e[68G[ ${RED}FAILED${NORMAL} ]" fi #Tar the files and remove the original Backup Directory. Saves about 50MB of disk space per backup echo -ne "${NORMAL} Compressing Backup " backupfile="${arkbackupdir}/${daystamp}/${instance}.${datestamp}.tar" if [ "$arkbackupcompress" == "false" ]; then tar -cf "${backupfile}" -C "${arkbackupdir}" "${datestamp}" else backupfile="${backupfile}.bz2" tar -jcf "${backupfile}" -C "${arkbackupdir}" "${datestamp}" fi rm -rf "${backupdir}" if [ -f "${backupfile}" ]; then echo -e "${NORMAL}\e[68G[ ${GREEN}OK${NORMAL} ]" else echo -e "${NORMAL}\e[68G[ ${RED}FAILED${NORMAL} ]" fi echo -e "${NORMAL} Created Backup: ${GREEN} ${instance}.${datestamp}.tar.bz2${NORMAL} in ${arkbackupdir}/${daystamp}" if [ -n "$arkMaxBackupSizeGB" ] && (( arkMaxBackupSizeGB >= 1 )); then (( arkMaxBackupSizeMB = arkMaxBackupSizeGB * 1024 )) fi if [ -n "$arkMaxBackupSizeMB" ] && (( arkMaxBackupSizeMB > 64 )); then find "${arkbackupdir}" -type f -printf "%T@\t%s\t%p\n" | sort -n -r | cut -f2-3 | (sz=0; while read -r fsz f; do if (( sz / 1048576 > arkMaxBackupSizeMB )); then rm "$f" fi (( sz += fsz )) done) fi if [ -n "${arkBackupPostCommand}" ] && [ -f "${backupfile}" ]; then eval " ${arkBackupPostCommand}" fi } # # Copies server state from a backup # doRestore(){ local saverootdir="${arkserverroot}/${arkserverdir}/Saved" local savedcfgdir="${saverootdir}/Config/LinuxServer" local savedir="$(getSavedArksDirectory "${saverootdir}")" local savedir="$(getSavedArksDirectory "${saverootdir}")" local clusterdir="$(getArkServerSetting "ClusterDirOverride" "${saverootdir}/clusters/$(getArkServerSetting "clusterid")")" local restorePath= local stripComponents=1 if [[ $# -ne 0 ]] ; then backupFile=$1 else backupFile=$(find "${arkbackupdir}/" -type f -printf "%T@ %p\n" | sort -n | cut -d' ' -f 2- | tail -n 1) fi if [[ ! -f "$backupFile" ]] ; then if [[ -f "$arkbackupdir/$backupFile" ]] ; then backupFile="$arkbackupdir/$backupFile" else echo "File $backupFile not found." 1>&2 exit 1 fi fi echo "Restoring from ${backupFile}" for file in $(tar -tjf "${backupFile}") ; do restorePath= stripComponents=1 case "${file#*/}" in SaveGames/*) # mod persistent info restorePath="${saverootdir}" stripComponents=2 ;; Cluster/*) # cluster files restorePath="${clusterdir}" stripComponents=2 ;; *.ini) # ini are the config files restorePath="${savedcfgdir}" ;; *.ark|*.arkprofile|*.arktribe|*.arktributetribe) # ark file in savedir restorePath="${savedir}" ;; esac if [ -n "${restorePath}" ]; then echo "Restoring ${file#*/} to ${restorePath}" tar -xjvf "${backupFile}" -C "${restorePath}" --strip-components=${stripComponents} "${file}" # shellcheck disable=SC2181 if [ $? == 0 ] ; then echo -e "${NORMAL}\e[68G[ ${GREEN}OK${NORMAL} ]" else echo -e "${NORMAL}\e[68G[ ${RED}FAILED${NORMAL} ]" fi fi done echo -e "${NORMAL}Restore Complete \e[68G[ ${GREEN}OK${NORMAL} ]" } # # Install a cron job to execute a particular command # doInstallCronJob(){ hour='*' minute='0' cmdopts="${arkCronExtraOpts}" cmdargs="" instargs="" output=">/dev/null 2>&1" arkmanagerpath="${0}" command="$1" shift for opt in "$@"; do case "$opt" in --daily) ;; --hourly) hour='*' ;; --hour=*) hour="${opt#--hour=}" ;; --minute=*) minute="${opt#--minute=}" ;; --enable-output) output= ;; --arg=*) cmdargs="${cmdargs} $(printf "%q" "${opt#--arg=}")" ;; --*) cmdopts="${cmdopts} $(printf "%q" "${opt}")" ;; *) cmdargs="${cmdargs} $(printf "%q" "${opt}")" ;; esac done if [ "$allinstances" == "yes" ]; then instargs="@all" else for inst in "${instances[@]}"; do instargs="${instargs} $(printf "%q" "@${inst}")" done fi cronjob="${minute} ${hour} * * * ${arkmanagerpath} --cronjob ${command} ${instargs} ${cmdopts} --args ${cmdargs} -- ${output}" (crontab -l | \ sed -e "\\# [*] [*] [*] ${arkmanagerpath} --cronjob ${command} ${instargs} #d"; echo "${cronjob}" ) | \ crontab - } # # Removes an installed cron job # doRemoveCronJob(){ arkmanagerpath="${0}" command="$1" crontab -l | \ sed -e "\\# [*] [*] [*] ${arkmanagerpath} --cronjob ${command} @${instance} #d" | \ crontab - } # # Print the status of the server (running? online? version?) # printStatus(){ if isTheServerRunning ;then echo -e "$NORMAL" "Server running: " "$GREEN" "Yes" "$NORMAL" echo -e "$NORMAL" "Server PID: " "$GREEN" "$(getServerPID)" "$NORMAL" else echo -e "$NORMAL" "Server running: " "$RED" "No" "$NORMAL" fi if [ -n "$arkflag_epiconly" ]; then echo -e "$NORMAL" "Cannot check server status with -epiconly" elif isTheServerUp ;then echo -e "$NORMAL" "Server listening: " "$RED" "No" "$NORMAL" else echo -e "$NORMAL" "Server listening: " "$GREEN" "Yes" "$NORMAL" perl -MSocket -e ' my $port = int($ARGV[0]); socket(my $socket, PF_INET, SOCK_DGRAM, 0); setsockopt($socket, SOL_SOCKET, SO_RCVTIMEO, pack("i4", 1, 0, 0, 0)); my $sockaddr = pack_sockaddr_in($port, inet_aton($ARGV[1])); send($socket, "\xff\xff\xff\xffTSource Engine Query\x00", 0, $sockaddr); my $data = ""; recv($socket, $data, 1400, 0) or (print "Unable to query server\n" and exit(1)); my ($servername, $mapname, $game, $fullname, $rest) = split(/\x00/, substr($data, 6), 5); my $players = ord(substr($rest, 2, 1)); my $maxplayers = ord(substr($rest, 3, 1)); print "Server Name: $servername\n"; print "Steam Players: $players / $maxplayers\n"; send($socket, "\xff\xff\xff\xff\x55\xff\xff\xff\xff", 0, $sockaddr); $data = ""; recv($socket, $data, 1400, 0) or (print "Challenge request failed" and exit(1)); if (ord(substr($data, 4, 1)) == 0x41) { my $chal = substr($data, 5); send($socket, "\xff\xff\xff\xff\x55" . $chal, 0, $sockaddr); $data = ""; recv($socket, $data, 1400, 0) or (print "A2S_PLAYERS request failed" and exit(1)); } ord(substr($data, 4, 1)) != 0x44 and (print ("A2S_PLAYERS Response: : " . unpack("H*", $data)) and exit(1)); my $players = ord(substr($data, 5, 1)); my $active = 0; my $pdata = substr($data, 6); for my $i (0 .. $players) { my $idx = ord(substr($pdata, 0, 1)); my ($name, $rest) = split(/\x00/, substr($pdata, 1), 2); $pdata = substr($rest, 8); if ($name ne "") { $active = $active + 1; } } print "Active Steam Players: $active\n"; ' "$(getQueryPort)" "$(getMultiHomeIP)" if isTheServerOnline; then echo -e "$NORMAL" "Server online: " "$GREEN" "Yes" "$NORMAL" echo -e "$NORMAL" "ARKServers link: " "$GREEN" "http://arkservers.net/server/${publicip}:$(getQueryPort)" "$NORMAL" echo -e "$NORMAL" "Steam connect link: " "$GREEN" "steam://connect/${publicip}:$(getQueryPort)" "$NORMAL" else echo -e "$NORMAL" "Server online: " "$RED" "No" "$NORMAL" fi fi if [[ -n "$arkflag_crossplay" || -n "$arkflag_epiconly" ]]; then players="$(rconcmd listplayers 2>/dev/null)" if [[ "$players" == '"'?*'"' ]]; then echo "Active Players: $(echo "$players" | grep -c '[0-9]\.')" fi fi instver="$(getCurrentVersion)" echo -e "$NORMAL" "Server build ID: " "$GREEN" "$instver" "$NORMAL" instbeta="$(getCurrentBranch)" if [ -n "$instbeta" ]; then echo -e "$NORMAL" "Server branch: " "$GREEN" "$instbeta" "$NORMAL" fi if [ -f "$arkserverroot/version.txt" ]; then echo -e "$NORMAL" "Server version: " "$GREEN" "$(<"$arkserverroot/version.txt")" "$NORMAL" fi } doWait(){ local waitall= local waitany= local waitonline= local waitstopped= declare -A pidfiles for arg in "$@"; do case "$arg" in --all) waitall=1; ;; --any) waitany=1; ;; --online) waitonline=1; ;; --stopped) waitstopped=1; ;; esac done if [[ -z "${waitany}" && -z "${waitall}" ]]; then waitany=1 fi if [[ -z "${waitonline}" && -z "${waitstopped}" ]]; then waitstopped=1 fi for instance in "${instances[@]}"; do pidfile="$(useConfig "$instance"; echo "${arkserverroot}/${arkmanagerpidfile}")" pidfiles[$instance]="$pidfile" done ( trap exit INT trap exit TERM while sleep 5; do anyrunning=0 allrunning=1 anyonline=0 allonline=1 for instance in "${instances[@]}"; do pidfile="${pidfiles[$instance]}" if [ -f "${pidfile}" ]; then pid="$(<"${pidfile}")" if kill -0 "${pid}"; then anyrunning=1 if [ -n "$waitonline" ]; then if (useConfig "$instance"; isTheServerOnline); then anyonline=1 else allonline=0 fi fi else allrunning=0 allonline=0 fi fi done if [[ ( "${waitstopped}" == 1 && "${waitall}" == 1 && "${anyrunning}" == 0 ) || ( "${waitstopped}" == 1 && "${waitany}" == 1 && "${allrunning}" == 0 ) || ( "${waitonline}" == 1 && "${waitall}" == 1 && "${allonline}" == 1 ) || ( "${waitonline}" == 1 && "${waitany}" == 1 && "${anyonline}" == 1 ) ]]; then return fi done ) } getAllInstanceNames(){ declare -A instancenames if [ -n "${defaultinstance}" ]; then instancenames[${defaultinstance}]="${defaultinstance}" echo "${defaultinstance}" fi if [ -f "${HOME}/${arkstUserCfgFile}" ]; then while read -r l <&3; do v="${l%%=*}" if [[ "${v}" = configfile_* ]]; then if [ -n "${!v}" ]; then instancename="${v#configfile_}" if [ -z "${instancenames[${instancename}]}" ]; then instancenames[${instancename}]="${instancename}" echo "${instancename}" fi fi fi done 3<"${HOME}/${arkstUserCfgFile}" fi if [ -f "${arkstGlobalCfgFile}" ]; then while read -r l <&3; do v="${l%%=*}" if [[ "${v}" = configfile_* ]]; then if [ -n "${!v}" ]; then instancename="${v#configfile_}" if [ -z "${instancenames[${instancename}]}" ]; then instancenames[${instancename}]="${instancename}" echo "${instancename}" fi fi fi done 3<"${arkstGlobalCfgFile}" fi for f in "${HOME}"/.config/arkmanager/instances/*.cfg; do if [ -f "${f}" ]; then instancename="${f##*/}" instancename="${instancename%.cfg}" if [ -z "${instancenames[${instancename}]}" ]; then instancenames[${instancename}]="${instancename}" echo "${instancename}" fi fi done for f in /etc/arkmanager/instances/*.cfg; do if [ -f "${f}" ]; then instancename="${f##*/}" instancename="${instancename%.cfg}" if [ -z "${instancenames[${instancename}]}" ]; then instancenames[${instancename}]="${instancename}" echo "${instancename}" fi fi done } doListAllInstances(){ if [ "$1" == "--brief" ]; then getAllInstanceNames else echo "The following instances are available:" for n in $(getAllInstanceNames); do ( echo -n " @${n}: " useConfig "$n" echo "${configfile} => ${arkserverroot}" ) done fi } doPrintConfig(){ declare -A vars declare -A vals for v in $(eval echo \$\{\!{a..z}\*\} \$\{\!{A..Z}\*\} \$\{\!_\*\}); do vals["$v"]="${!v}" done for cfgfile in "$configfile" "${HOME}/${arkstUserCfgFile}" "${arkstGlobalCfgFile}"; do if [ -r "$cfgfile" ]; then while read -r v; do # shellcheck source=arkmanager.cfg.shellcheck val="$(source "$cfgfile"; echo "${!v}")" if [[ "$val" = "${vals[$v]}" && -z "${vars[$v]}" ]]; then vars["$v"]="$cfgfile" echo "${cfgfile} => ${v}" fi done < <(sed -n 's/^[[:space:]]*\([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p' <"$cfgfile") fi done } doTestConfig(){ set -e if [ -r "${arkstGlobalCfgFile}" ]; then # shellcheck source=arkmanager.cfg.shellcheck source "${arkstGlobalCfgFile}" fi if [ -r "${HOME}/${arkstUserCfgFile}" ]; then # shellcheck source=arkmanager.cfg.shellcheck source "${HOME}/${arkstUserCfgFile}" fi if [ -r "$configfile" ]; then # shellcheck source=arkmanager.cfg.shellcheck source "$configfile" fi test -n "$steamcmdroot" -a -d "$steamcmdroot" -a -r "$steamcmdroot" test -f "$steamcmdroot/$steamcmdexec" test -n "$arkserverroot" -a -d "$arkserverroot" -a -r "$arkserverroot" test -n "$arkserverexec" -a -x "$arkserverroot/$arkserverexec" test -n "$savedarksdir" -a -d "$savedarksdir" -a -w "$savedarksdir" test "$mod_branch" = "Linux" -a -z "$nowarnmodbranch" test -n "$logdir" -a -d "$logdir" -a -w "$logdir" set +e } useConfig() { configfile= if [ -f "/etc/arkmanager/instances/${1}.cfg" ]; then configfile="/etc/arkmanager/instances/${1}.cfg" fi if [[ -f "${HOME}/.config/arkmanager/instances/${1}.cfg" && "${HOME}/.config/arkmanager/instances/${1}.cfg" -nt "/etc/arkmanager/instances/${1}.cfg" ]]; then configfile="${HOME}/.config/arkmanager/instances/${1}.cfg" fi for varname in "${!configfile_@}"; do if [ "configfile_$1" == "$varname" ]; then configfile="${!varname}" break fi done if [[ -z "$configfile" && -n "$arkSingleInstance" ]]; then if [ -f "${HOME}/${arkstUserCfgFile}" ]; then configfile="${HOME}/${arkstUserCfgFile}" else configfile="${arkstGlobalCfgFile}" fi else if [ -z "$configfile" ]; then echo "Error: no config files for instance $1" exit 1 fi if [ ! -f "$configfile" ]; then echo "Error: config file $configfile does not exist" exit 1 fi # shellcheck source=arkmanager.cfg.shellcheck source "$configfile" fi if [ -z "$arkserverroot" ]; then echo "Error: arkserverroot not set" exit 1 fi arkserverdir="${arkserverdir:-ShooterGame}" arkautorestartfile="${arkautorestartfile:-${arkserverdir}/Saved/.autorestart-${1}}" arkoldautorestartfile="${arkserverdir}/Saved/.autorestart" arkserverpidfile="${arkserverpidfile:-${arkserverdir}/Saved/.arkserver-${1}.pid}" arkserveroldpidfile="${arkserverdir}/Saved/.arkserver.pid" arkmanagerpidfile="${arkmanagerpidfile:-${arkserverdir}/Saved/.arkmanager-${1}.pid}" arkwarnlockfile="${arkwarnlockfile:-${arkserverdir}/Saved/.ark-warn-${1}.lock}" # This is linked to the directory, not to the instance arkupdatelockfile="${arkupdatelockfile:-${arkserverdir}/Saved/.ark-update.lock}" arkupdatetimefile="${arkupdatetimefile:-${arkserverdir}/Saved/.ark-update.time}" broadcastcmd="${broadcastcmd:-broadcast}" } addArkOpt() { local optname="${1%%=*}" local optval="${1#*=}" local optdash="${optname:0:1}" if [[ "${optval}" == "${1}" ]]; then optval="" fi optname="${optname#-}" optname="${optname//[^A-Za-z0-9_]/_}" if [[ "${optdash}" == "-" ]]; then if [[ -z "${optval}" ]]; then IFS="" read -r "arkflag_${optname}" <<<"true" else IFS="" read -r "arkopt_${optname}" <<<"${optval}" fi else IFS="" read -r "ark_${optname}" <<<"${optval}" fi } showUsage() { echo -e "Usage: arkmanager [Commands]\n" cat <<-EOE Commands can be followed by one or more @instance arguments The special '@all' instance selects all instances Commands may also be followed by zero or more --options Commands that take no instances: Command Description upgrade-tools Check for a new ARK Server Tools version and upgrades it if needed uninstall-tools Uninstall the ARK Server Tools useconfig Sets the default instance for the commands that follow remove-mods Remove one or more mods from the steamcmd workshop directory list-instances Lists all available instances --help Show this help --version Show the version info of ARK Server Tools Commands that take one or more instances: Command Description backup Saves a backup of your server inside the backup directory restore Restore a Backup, If no file is specified, the latest backup is used broadcast Sends a message to all users connected to server saveworld Saves the game world to disk rconcmd Execute RCON command on server checkupdate Check for a new ARK server version checkmodupdate Checks for any mods needing updates install Install the ARK server files from steamcmd installmod Installs a mod from the Steam workshop uninstallmod Removes the mod from the Mods directory reinstallmod Removes and re-installs a mod in the Mods directory enablemod Enables a mod in the config disablemod Disables a mod in the config installmods Installs all enabled mods uninstallmods Removes all installed mods from the Mods directory install-cronjob Adds a cron job using the specified command remove-cronjob Removes a cron job that used the specified command restart Stops the server and then starts it run Runs the server without daemonizing start Starts the server stop Stops the server cancelshutdown Aborts a running stop --warn or update --warn status Returns the status of the current ARK server instance printconfig Displays which settings are sourced from which config file getpid Gets the running server PID update Check for a new ARK server version, if needed, stops the server, updates it, and starts it again Commands which invoke steamcmd take the following options: --verbose Show steamcmd output --spinner Use a spinner for progress (default) --dots Use dots for progress Update command takes the following options: --force Apply update without checking the current version --safe Wait for server to perform world save and update (deprecated, use --saveworld) --warn Warn players before updating server --ifempty Apply the update only if no players are connected --validate Validates all ARK server files --saveworld Saves world before update --update-mods Updates installed and requested mods --backup Takes a backup of the save files before updating --downloadonly Download the mod and/or server update without applying it Requires arkStagingDir be set to a staging directory on the same filesystem as the server --no-download Applies an already downloaded update from the staging directory --systemd Use the SystemD arkmanager@instance.service to restart the server --service Use the sysv-init arkmanager service to restart the server --upstart Use the Upstart arkmanager service to restart the server --no-autostart Don't start the server after updating --warnreason=... Specify an update reason other than the default --beta=... Specify a beta to install (use --beta=public to reset) Stop and restart commands take the following options: --saveworld Saves world before shutdown --warn Warn players before stopping server --warnreason=... Specify a shutdown reason other than the default Start and restart commands take the following options: --noautoupdate Disable any automatic update that may be enabled in the config --alwaysrestart Always restart server on crash, even if it did not finish starting --no-background Run the server without going into the background --arkopt,opt=val Add an ARK option: -opt=val Equivalent to adding arkopt_opt=val to config for this run -opt Equivalent to adding arkflag_opt=true to config for this run opt=val Equivalent to adding ark_opt=val to config for this run installmod command takes the following options: --validate Forcibly re-download the mod from the steam workshop install-cronjob command takes the following options: --daily Run once a day (default) --hourly Run once an hour --hour= Hour of the day to run command --minute= Minute of the hour to run command --enable-output Don't redirect the output of the command --arg=option Option to pass to command list-instances command takes the following options: --brief Only list instance names EOE } #--------------------- # Main program #--------------------- main(){ if [ -f "${arkstGlobalCfgFile}" ]; then # shellcheck source=arkmanager.cfg.shellcheck source "${arkstGlobalCfgFile}" fi if [ -f "${HOME}/${arkstUserCfgFile}" ]; then # shellcheck source=arkmanager.cfg.shellcheck source "${HOME}/${arkstUserCfgFile}" fi # check the configuration and throw errors or warnings if needed checkConfig status=0 while true; do options=( ) allinstances=no instances=( ) args=( ) command="$1" shift nrarg=0 # Handle global options case "$command" in --verbose) verbose=1 continue ;; --dots) progressDisplayType=dots continue ;; --spinner) progressDisplayType=spinner continue ;; --cronjob) # shellcheck disable=SC2034 inCronJob=true continue ;; --runfromroot) continue ;; --run-as-root-i-know-what-im-doing) continue ;; esac # get the number of arguments for commands that take arguments case "$command" in installmod) nrarg=1; ;; uninstallmod) nrarg=1; ;; reinstallmod) nrarg=1; ;; enablemod) nrarg=1; ;; disablemod) nrarg=1; ;; broadcast) nrarg=1; ;; rconcmd) nrarg=1; ;; notify) nrarg=1; ;; useconfig) nrarg=1; ;; install-cronjob) nrarg=1; ;; remove-cronjob) nrarg=1; ;; remove-mods) nrarg=1; ;; restore) nrarg=1; ;; esac # Enumerate the options and arguments while [ $# -ne 0 ]; do case "$1" in --) shift break ;; --args) nrarg=$# ;; --verbose) verbose=1 ;; --dots) progressDisplayType=dots ;; --spinner) progressDisplayType=spinner ;; --arkopt,*) addArkOpt "${1#--arkopt,}" ;; --*) options+=( "$1" ) ;; @all) allinstances=yes ;; @*) instances+=( "${1#@}" ) ;; *) if [ $nrarg -gt 0 ]; then args+=( "$1" ) (( nrarg-- )) else break fi ;; esac shift done # handle non-instance separately case "$command" in upgrade-tools) doUpgradeTools exit ;; uninstall-tools) doUninstallTools exit ;; useconfig) defaultinstance="${args[0]}" continue ;; remove-mods) doRemoveMods "${args[0]}" if [ $# -eq 0 ]; then exit 0 else continue fi ;; list-instances) doListAllInstances "${options[@]}" exit ;; --version) echo "Version: ${arkstVersion}" echo "Channel: ${arkstChannel}" if [ -n "${arkstUsePkgManager}" ]; then echo "Installed by package manager: ${arkstUsePkgManager}" fi if [ -n "${arkstCommit}" ]; then echo "Commit: ${arkstCommit:0:7}" fi if [ -n "${arkstTag}" ]; then echo "Release Tag: ${arkstTag}" fi blobsize="$(sed "s@^\\(arkst\\(Commit\\|Tag\\|RootUseEnv\\|UsePkgManager\\|GithubRepoOverride\\|GlobalCfgFileOverride\\|UserCfgFileOverride\\)\\)=.*@\\1=''@" "${arkstScriptPath}" | wc -c)" echo "Blob SHA: $( (echo -ne "blob ${blobsize}\0"; sed "s@^\\(arkst\\(Commit\\|Tag\\|RootUseEnv\\|UsePkgManager\\|GithubRepoOverride\\|GlobalCfgFileOverride\\|UserCfgFileOverride\\)\\)=.*@\\1=''@" "${arkstScriptPath}") | sha1sum | cut -d' ' -f1)" exit 1 ;; -h|--help) showUsage exit 1 ;; "") echo "arkmanager v${arkstVersion}: no command specified" showUsage exit 1 ;; esac # Handle no instances being specified if [[ "${#instances[@]}" == 0 && "$allinstances" == "no" ]]; then if [ -n "$defaultinstance" ]; then instances=( "$defaultinstance" ) else echo "No instances supplied for command ${command} ${options[*]} ${args[*]}" read -p "Do you wish to run this command for all instances?" -n 1 -r echo if [[ "$REPLY" =~ ^[Yy]$ ]]; then allinstances=yes else exit 1 fi fi fi # Handle cronjob commands specially case "$command" in install-cronjob) doInstallCronJob "${args[@]}" "${options[@]}" "$@" exit ;; remove-cronjob) doRemoveCronJob "${args[@]}" exit ;; esac # Handle all instances being requested if [[ "$allinstances" == "yes" ]]; then # shellcheck disable=SC2207 instances=( $(getAllInstanceNames) ) fi # Handle wait command specially case "$command" in wait) doWait "${options[@]}" if [ "$#" == 0 ]; then break fi continue ;; esac # Run the command for each instance requested for instance in "${instances[@]}"; do ( echo "Running command '${command}' for instance '${instance}'" useConfig "$instance" checkConfig "$command" case "$command" in run) doRun ;; start) doStart "${options[@]}" ;; stop) doStop shutdown "${options[@]}" ;; restart) doStop restart "${options[@]}" echo "$(timestamp): stop" >> "$logdir/$arkmanagerLog" ;; cancelshutdown) doCancelShutdown "${options[@]}" ;; install) doInstall "${options[@]}" ;; update) doUpdate "${options[@]}" ;; checkupdate) checkForUpdate ;; checkmodupdate) checkForModUpdate "${options[@]}" ;; installmod) doInstallMod "${args[@]}" "${options[@]}" ;; enablemod) doEnableMod "${args[@]}" ;; disablemod) doDisableMod "${args[@]}" ;; installmods) doInstallAllMods "${options[@]}" ;; uninstallmods) doUninstallAllMods ;; uninstallmod) doUninstallMod "${args[@]}" ;; reinstallmod) doUninstallMod "${args[@]}" doInstallMod "${args[@]}" "${options[@]}" ;; list-mods) listMods ;; backup) doBackup "${options[@]}" ;; restore) doRestore "${args[@]}" ;; broadcast) doBroadcast "${args[@]}" ;; notify) notify "${args[@]}" ;; saveworld) doSaveWorld ;; rconcmd) rconcmd "${args[@]}" ;; printconfig) doPrintConfig ;; testconfig) doTestConfig ;; status) printStatus ;; getpid) getServerPID ;; *) echo -n "arkmanager v${arkstVersion}: unknown command '$command' specified" showUsage exit 255 ;; esac ) laststatus=$? if [ $laststatus -eq 255 ]; then exit 1 elif [ $laststatus -ne 0 ]; then status=$laststatus fi done # Perform the restart portion of the restart command if [[ "$command" == "restart" ]]; then sleep 1 for instance in "${instances[@]}"; do ( echo "$(timestamp): restart" >> "$logdir/$arkmanagerLog" useConfig "$instance" doStart "${options[@]}" ) done fi if [ $# -eq 0 ]; then break fi done exit $status } # Only execute main function if script is not being sourced # by another script if [[ "$0" = "${BASH_SOURCE[0]}" || -z "${BASH_SOURCE[*]}" ]]; then main "$@" fi