#!/bin/bash # ARK: survival evolved manager # # Original author: LeXaT # Maintainer: FezVrasta # Contributors: Sispheor, Atriusftw, klightspeed, lexat, puseidr #disable exportall so options are no exported under Debian et al. set +o allexport # Script version arkstVersion='1.6' arkstTag='' arkstCommit='' arkstGithubRepo="FezVrasta/ark-server-tools" arkstRootUseEnv='' arkstGlobalCfgFile='/etc/arkmanager/arkmanager.cfg' arkstUserCfgFile='.arkmanager.cfg' doUpgradeTools() { local sudo=sudo if [ $(id -u) == 0 -o "$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 } doUpgradeToolsFromCommit(){ local sudo=sudo if [ $(id -u) == 0 -o "$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}'|" \ 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" "$0" --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() { local sudo=sudo if [ $(id -u) == 0 -o "$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}" -a -x "${install_datadir}/arkmanager-uninstall.sh" ]; then $sudo "${install_datadir}/arkmanager-uninstall.sh" exit 0 elif [ -n "${install_libexecdir}" -a -x "${install_libexecdir}/arkmanager-uninstall.sh" ]; then $sudo "${install_libexecdir}/arkmanager-uninstall.sh" exit 0 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 else exec /sbin/runuser "$steamcmd_user" -s "$steamcmd_user_shellexec" -c "$(printf "%q" "$0")$(printf " %q" "$@")" exit 1 fi } # Check the user is not currently running this script as root if [ "$(id -u)" == "0" ]; then runAsRoot "$@" exit 0 fi #--------------------- # Variables #--------------------- # Global variables if [ -f "${arkstGlobalCfgFile}" ]; then source "${arkstGlobalCfgFile}" fi if [ -f "${HOME}/${arkstUserCfgFile}" ]; then source "${HOME}/${arkstUserCfgFile}" fi if [[ -n "${steamcmd_user}" && "${steamcmd_user}" != "--me" && "$USERNAME" != "${steamcmd_user}" && -n "$usesudo" ]]; then exec sudo --user="${steamcmd_user}" "$0" "$@" 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" maxOpenFiles=100000 # 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." fi # steamcmdexec if [ ! -f "$steamcmdroot/$steamcmdexec" ] ; then echo -e "[" "$RED" "ERROR" "$NORMAL" "]" "\tYour SteamCMD exec could not be found." 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." 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." 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" fi fi if [ "$1" != "installmod" ] && [ "$1" != "installmods" ]; then # Warn if any mods are requested but not installed if [ -n "$arkserverroot" -a -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." fi done fi fi fi # Warn if mod_branch=Linux if [ "$mod_branch" == "Linux" -a -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." fi # Service configuration # logdir if [ ! -w "$logdir" ] ; then echo -e "[" "$RED" "ERROR" "$NORMAL" "]" "\tYou have not rights to write in the log directory." fi } # # Get setting from config or from ini file # $1 is the setting name # $2 is the default # getArkServerSetting() { local lcname="$(tr 'A-Z' 'a-z' <<<"${1}")" local varname for varname in "${!ark_@}"; do if [ "$(tr 'A-Z' 'a-z' <<<"$varname")" == "ark_${lcname}" ]; then echo "${!varname}" return fi done for varname in "${!arkopt_@}"; do 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 # getMultiHome() { getArkServerSetting "MultiHome" "${1}" } 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 [[ -z "${steamcmdhome}" || ! -d "${steamcmdhome}" ]]; then steamcmdhome="${HOME}" fi for d in "$steamworkshopdir" \ "$steamcmdhome/.steam/SteamApps/workshop" \ "$steamcmdhome/.steam/steamapps/workshop" \ "$steamcmdhome/Steam/SteamApps/workshop" \ "$steamcmdhome/Steam/steamapps/workshop" \ "${steamdataroot:-$steamcmdroot}/steamapps/workshop"; do if [[ -d "${d}" && -f "${d}/appworkshop_${mod_appid}.acf" ]]; then echo "$d" return fi done # default echo "$steamcmdhome/Steam/steamapps/workshop" } # # Determine SteamCMD data directory # getSteamAppInfoCache(){ if [[ -z "${steamcmdhome}" || ! -d "${steamcmdhome}" ]]; then steamcmdhome="${HOME}" fi for d in "${steamcmd_appinfocache}" \ "$steamcmdhome/.steam/appcache/appinfo.vdf" \ "$steamcmdhome/Steam/appcache/appinfo.vdf" \ "${steamdataroot:-$steamcmdroot}/appcache/appinfo.vdf"; do if [[ -f "${d}" ]]; then echo "$d" return fi done # default echo "${steamcmd_appinfocache:-$steamcmdhome/Steam/appcache/appinfo.vdf}" } # # Determine SteamCMD data directory # getSteamWorkshopLog(){ if [[ -z "${steamcmdhome}" || ! -d "${steamcmdhome}" ]]; then steamcmdhome="${HOME}" fi for d in "${steamcmd_workshoplog}" \ "$steamcmdhome/.steam/logs/workshop_log.txt" \ "$steamcmdhome/Steam/logs/workshop_log.txt" \ "${steamdataroot:-$steamcmdroot}/logs/workshop_log.txt"; do if [[ -f "${d}" ]]; then echo "$d" return fi done # default echo "${steamcmd_workshoplog:-$steamcmdhome/logs/workshop_log.txt}" } # # 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\}/${1}}" msg="${msg//\{instance\}/${instance}}" msg="${msg//\{server\}/${HOSTNAME}}" 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 HOME="${steamcmdhome}" "$steamcmdroot/$steamcmdexec" +@NoPromptForPassword 1 ${steamcmd_cmds_prelogin} +login ${steamlogin:-anonymous} ${steamcmd_cmds_postlogin} "$@" +quit } function runSteamCMDspinner(){ if [ -n "$verbose" ]; then printf "Executing" printf " %q" "$steamcmdroot/$steamcmdexec" +@NoPromptForPassword 1 ${steamcmd_cmds_prelogin} +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 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 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" -a "$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 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 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 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 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 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 depot manifest <&3; do hasmanifest=1 depot="${depot//\"/}" manifest="${manifest//\"/}" newmanifest="$(echo "${appinfo}" | while read 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 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 fi fi if [ -f "${arkserverroot}/${arkserveroldpidfile}" ]; then serverpid="$(<"${arkserverroot}/${arkserveroldpidfile}")" if kill -0 "$serverpid" >/dev/null 2>&1; then echo $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 "$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 "$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 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 [[ ( -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 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" ]; 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 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" ]; 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 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 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" # 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 # 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 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 $(ls /proc/$BASHPID/fd/); do [[ $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 for cfg in "${!configfile_@}"; do if [ -f "${!cfg}" ]; then ( 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` kill -INT $PID >/dev/null 2>&1 for (( i = 0; i < 20; i++ )); do sleep 1 if ! isTheServerRunning; then break fi done if isTheServerRunning; 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 ( source "${!cfg}" doStop ) fi done } # # install / update / download update # runSteamCMDAppUpdate(){ local installdir="$1" shift runSteamCMDspinner +force_install_dir "$installdir" +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" 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 "$shutdownreason" ]; then reason="$shutdownreason" 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 warnmsg 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}" -a "${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 if [ $? -eq 0 ]; then rm -rf "${arkStagingDir}/steamapps/downloading/${appid}" elif [ $? -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" -a -n "$arkStagingDir" -a "$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" -o -n "$modupdate" -o -n "$bgupdate" ]; then 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}" -a "${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 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 instmft= local availmft= local modname= local steamworkshopdir="$(getSteamWorkshopDir)" local cancheckmodavail=1 local modmissing= local revstatcode= local alluptodate=1 if [[ " $* " =~ " --revstatus " ]]; then revstatcode=1 fi 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 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' 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 ) | sort | uniq | grep '^[1-9][0-9]*$' | grep -v '^111111111$' } # # Get the Mod details of the installed mods and the requested mods # listMods(){ local modlist local modid moddir moddesc 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 localupd="$(getLocalModLastUpdated "$modid")" 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 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 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 f; do if [ \( ! -f "$moddestdir/${f%.z}" \) -o "$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|^.*