#!/bin/bash # ARK: survival evolved manager # # Original author: LeXaT # Maintainer: FezVrasta # Contributors: Sispheor, Atriusftw, klightspeed, lexat, puseidr # Script version arkstVersion="1.6" arkstCommit='' doUpgradeTools() { local sudo=sudo if [ "$UID" == 0 -o "$steamcmd_user" == "--me" ]; then sudo= fi echo "arkmanager v${arkstVersion}: Checking for updates..." arkstLatestVersion=`curl -s https://raw.githubusercontent.com/FezVrasta/ark-server-tools/${arkstChannel}/.version` arkstLatestCommit=`curl -s https://api.github.com/repos/FezVrasta/ark-server-tools/git/refs/heads/${arkstChannel} | sed -n 's/^ *"sha": "\(.*\)",.*/\1/p'` if [ "$arkstLatestVersion" == "Not Found" ]; then echo "Channel ${arkstChannel} does not exist" echo echo "Available channels:" curl -s https://api.github.com/repos/FezVrasta/ark-server-tools/git/refs/heads | sed -n 's|^ *"ref": "refs/heads/\(.*\)",|\1|p' echo return fi 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 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 -en "\n" if [[ $REPLY =~ ^[Yy]$ ]]; then curl -s https://raw.githubusercontent.com/FezVrasta/ark-server-tools/${arkstChannel}/netinstall.sh | $sudo bash -s -- ${steamcmd_user} ${arkstChannel} "${reinstall_args[@]}" exit 0 fi 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 -en "\n" if [[ $REPLY =~ ^[Yy]$ ]]; then curl -s https://raw.githubusercontent.com/FezVrasta/ark-server-tools/${arkstChannel}/netinstall.sh | $sudo bash -s -- ${steamcmd_user} ${arkstChannel} "${reinstall_args[@]}" exit 0 fi else echo "Your ARK server tools are already up to date" fi } doUninstallTools() { local sudo=sudo if [ "$UID" == 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}" <"/etc/arkmanager/arkmanager.cfg" | tail -n1)")" if [ -n "$val" ]; then echo "$val" else echo "$2" fi } arkstChannel="$(getConfigVar arkstChannel "master")" 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")" 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 su "$steamcmd_user" -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 "/etc/arkmanager/arkmanager.cfg" ]; then source /etc/arkmanager/arkmanager.cfg fi if [ -f "${HOME}/.arkmanager.cfg" ]; then source "${HOME}/.arkmanager.cfg" 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}" arkautorestartfile="${arkautorestartfile:-ShooterGame/Saved/.autorestart}" 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 +%T } # # 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 # 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}/ShooterGame/Saved/${ark_AltSaveDirectoryName:-SavedArks}" if [ ! -w "${savedarksdir}" ]; then echo -e "[" "$RED" "ERROR" "$NORMAL" "]" "\tThe ARK SavedArks directory is not writable, and saveworld will fail" fi fi # Warn if any mods are requested but not installed if [ -n "$arkserverroot" -a -d "${arkserverroot}/ShooterGame/Content/Mods" ]; then for modid in $(getModIds); do if [ ! -f "${arkserverroot}/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 # 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 varname="ark_$1" if [ -n "${!varname}" ]; then echo "${!varname}" else local val="$(tr -d '\0\376\377' <"${arkserverroot}/ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini" | sed -n '/^\[ServerSettings\]/,/^\[.*\]/{s/^'"$1"'[[:space:]]*=[[:space:]]*//p;}' )" if [ -n "$val" ]; then echo "$val" else echo "$2" fi fi } # # Get server admin password # getAdminPassword() { getArkServerSetting "ServerAdminPassword" "" } # # Get server RCON Port # getRconPort() { getArkServerSetting "RCONPort" "32330" } # # Get server Game Port # getGamePort() { echo "${ark_Port:-7778}" } # # Get server Query Port # getQueryPort(){ echo "${ark_QueryPort:-27015}" } # # 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; } 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!!") { print "Command processed\n"; } else { print $rcvbody, "\n"; } ' "$(getRconPort)" "${ark_MultiHome:-127.0.0.1}" "$adminpass" "$1" } # # Save world # doSaveWorld() { rconcmd saveworld } # # Exit cleanly # doExitServer() { rconcmd doexit } # # Broadcast message # doBroadcast(){ rconcmd "broadcast $1" } # # Broadcast message with echo # doBroadcastWithEcho(){ echo "$1" doBroadcast "$1" } # # SteamCMD helper function # function runSteamCMD(){ "$steamcmdroot/$steamcmdexec" +@NoPromptForPassword 1 +login ${steamlogin:-anonymous} "$@" +quit } function runSteamCMDspinner(){ if [ -n "$verbose" ]; then if command >&3; then runSteamCMD "$@" | tee /dev/fd/3 else runSteamCMD "$@" fi return $? else if [ -z "$progressDisplayType" ]; then if stty <&1 >/dev/null 2>&1; then progressDisplayType=spinner else progressDisplayType=dots fi fi if command >&3; 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(){ getCurrentVersion getAvailableVersion if [[ "$bnumber" == "Unknown" || "$bnumber" -eq "$instver" ]]; then return 1 # no update needed 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 instver=`while read name val; do if [ "${name}" == "{" ]; then parseSteamACF "" "buildid"; break; fi; done <"${arkserverroot}/steamapps/appmanifest_${appid}.acf"` echo $instver > "$arkserverroot/arkversion" else instver="" fi } # # Get the current available server version on steamdb # function getAvailableVersion(){ rm -f "$steamcmd_appinfocache" bnumber=`runSteamCMD +app_info_update 1 +app_info_print "$appid" +quit | while read name val; do if [ "${name}" == "{" ]; then parseSteamACF ".depots.branches.public" "buildid"; break; fi; done` if [ -z "$bnumber" ]; then bnumber="Unknown" fi } # # Get the PID of the server process # function getServerPID(){ ps -ef | grep "$arkserverroot/$arkserverexec" | grep -v grep | awk '{print $2}' } # # 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(){ $lsof -i "${ark_MultiHome:+udp@}${ark_MultiHome}:$(getGamePort)" > /dev/null result=$? 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)" "${ark_MultiHome:-127.0.0.1}" 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 "$ark_MultiHome" ]; then publicip="$(curl --interface "${ark_MultiHome}" -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\": \""([^\"]*):([0-9]*)"\"" ]]; then return 0 else return 1 fi } # # Check if anybody is connected to the server # function numPlayersConnected(){ 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)" "${ark_MultiHome:-127.0.0.1}" } # # run function # doRun() { cd "$arkserverroot" arkserveropts="$serverMap" if [ -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}/ShooterGame/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 unset $varname done < <(sed -n 's/^\(ark\(\|opt\|flag\)_[^= ]*\)=.*/\1/p' <"$configfile") # bring in ark_... options for varname in "${!ark_@}"; do 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 done # bring in arkflag_... flags for varname in "${!arkflag_@}"; do name="${varname#arkflag_}" arkextraopts=( "${arkextraopts[@]}" "-${name}" ) done # bring in arkopt_... options for varname in "${!arkopt_@}"; do name="${varname#arkopt_}" val="${!varname}" if [ -n "$val" ]; then arkextraopts=( "${arkextraopts[@]}" "-${name}=${val}" ) fi done arkserveropts="${arkserveropts}?listen" # run the server in background echo "`timestamp`: start" # set max open files limit before we start the server ulimit -n $maxOpenFiles serverpid=0 restartserver=1 # Shutdown the server when we are terminated shutdown_server(){ restartserver=0 rm "$arkserverroot/$arkautorestartfile" if [ "$serverpid" -ne 0 ]; then kill -INT $serverpid fi } trap shutdown_server INT TERM # Auto-restart loop while [ $restartserver -ne 0 ]; do echo -n "`timestamp`: Running" 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 "`timestamp`: Server PID: $serverpid" # Disable auto-restart so we don't get caught in a restart loop rm -f "$arkserverroot/$arkautorestartfile" restartserver=0 sleep 5 while true; do # Grab the current server PID local pid="`getServerPID`" if [ "$pid" == "$serverpid" ]; then if [ "$restartserver" -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" touch "$arkserverroot/$arkautorestartfile" restartserver=1 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 echo "`timestamp`: restarting server" fi done } # # start function # doStart() { if isTheServerRunning; then echo "The server is already running" else if [ "$arkAutoUpdateOnStart" == "true" ]; then if ! [[ " $* " =~ " --noautoupdate " ]]; then echo "Updating server" doUpdate --update-mods fi fi tput sc echo "The server is starting..." doRun >"$logdir/$arkserverLog" 2>&1 & # output of this command is logged echo "`timestamp`: start" >> "$logdir/$arkmanagerLog" tput rc; tput ed; echo "The server is now running, and should be up within 10 minutes" 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 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#*=}"; ;; --saveworld) dosave=1; ;; esac done if [[ -n "$dowarn" ]]; then doWarn "$1" "$warnreason" fi if [[ -n "$dosave" ]]; then doSaveWorld fi tput sc echo "Stopping server..." echo "`timestamp`: stopping" >> "$logdir/$arkmanagerLog" rm -f "$arkserverroot/$arkautorestartfile" # kill the server with the PID PID=`getServerPID` kill -INT $PID for (( i = 0; i < 20; i++ )); do sleep 1 if ! isTheServerRunning; then break fi done if isTheServerRunning; then tput rc echo "Killing server..." kill -KILL $PID fi tput rc; tput ed; echo "The server has been stopped" echo "`timestamp`: stopped" >> "$logdir/$arkmanagerLog" 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(){ runSteamCMDspinner +force_install_dir "$1" +app_update $appid $2 } # # 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 runSteamCMDAppUpdate "$arkserverroot" validate # the current version should be the last version. We set our version getCurrentVersion } # # Formats a warning message based on replacement strings # printWarnMessage(){ local msg 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 [ "$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//\{modnamesupdated\}/${modnamesupdated}}" msg="${msg//\{reason\}/${reason}}" printf "%s\n" "$msg" 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" } # # Waits for a configurable number of minutes before updating the server # doWarn(){ cd "$arkserverroot" ( update_cancelled(){ if [ -n "$msgUpdateCancelled" ]; then msg="${msgUpdateCancelled//%s/$1}" else msg="Update cancelled by operator ($1)" fi doBroadcastWithEcho "${msg}" } 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 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 ) for warninterval in "${warnintervals[@]}"; do if [ "`getServerPID`" != "$pid" ]; then echo "Server has stopped. Aborting $1" return 1 fi if (( warnminutes >= warninterval )); then sleep 1m & sleeppid=$! printWarnMessage "$1" "$2" "minutes" "$warnminutes" for (( min = warnminutes - 1; min >= warninterval; min-- )); do numplayers=$(numPlayersConnected) echo "There are ${numplayers} players connected" if (( (numplayers + 0) == 0 )); then doBroadcastWithEcho "Nobody is connected. Shutting down immediately" return 0 fi wait $sleeppid if (( $min > $warninterval )); then sleep 1m & sleeppid=$! fi done warnminutes=$warninterval fi done local warnseconds=120 warnintervals=( 90 60 45 30 20 15 10 5 0 ) for warninterval in "${warnintervals[@]}"; do sleep $(( warnseconds - warninterval ))s & sleeppid=$! if [ "`getServerPID`" != "$pid" ]; then echo "Server has stopped. Aborting update" return 1 fi printWarnMessage "$1" "$2" "seconds" "$warnseconds" if (( warnseconds >= 20 )); then numplayers=$(numPlayersConnected) echo "There are ${numplayers} players connected" if (( (numplayers + 0) == 0 )); then doBroadcastWithEcho "Nobody is connected. Shutting down immediately" return 0 fi fi wait $sleeppid warnseconds=$warninterval done fi 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 updatetype=normal local validate= local modupdate= local saveworld= local downloadonly= for arg in "$@"; do if [ "$arg" == "--force" ]; then appupdate=1 elif [ "$arg" == "--safe" ]; then updatetype=safe elif [ "$arg" == "--warn" ]; then updatetype=warn elif [ "$arg" == "--ifempty" ]; then updatetype=ifempty elif [ "$arg" == "--validate" ]; then validate=validate appupdate=1 elif [ "$arg" == "--saveworld" ]; then saveworld=1 elif [ "$arg" == "--update-mods" ]; then modupdate=1 elif [ "$arg" == "--backup" ]; then arkBackupPreUpdate=true elif [[ "$arg" =~ "^--stagingdir=" ]]; then arkStagingDir="${ark#--stagingdir=}" elif [ "$arg" == "--downloadonly" ]; then downloadonly=1 else echo "Unrecognized option $arg" echo "Try 'arkmanager -h' or 'arkmanager --help' for more information." exit 1 fi done echo "$$" >"${arkserverroot}/.ark-update.lock.$$" 2>/dev/null while true; do if ! ln "${arkserverroot}/.ark-update.lock.$$" "${arkserverroot}/.ark-update.lock" 2>/dev/null; then local lockpid="$(<"${arkserverroot}/.ark-update.lock")" if [ -n "$lockpid" ] && [ "$lockpid" != "$$" ] && kill -0 "$lockpid" 2>/dev/null; then echo "Update already in progress (PID: $lockpid)" rm -f "${arkserverroot}/.ark-update.lock.$$" 2>/dev/null return 1 fi rm -f "${arkserverroot}/.ark-update.lock" else break fi done rm -f "${arkserverroot}/.ark-update.lock.$$" if [ -n "$modupdate" ]; then if ! doDownloadAllMods; then modupdate= fi if ! isAnyModUpdateNeeded; then modupdate= fi fi cd "$arkserverroot" if isUpdateNeeded; then appupdate=1 if [ -n "${arkStagingDir}" -a "${arkStagingDir}" != "${arkserverroot}" ]; then if [ ! -d "$arkStagingDir/ShooterGame" ]; then echo "Copying to staging directory" mkdir -p "$arkStagingDir" if [ "$(stat -c "%d" "$arkserverroot")" == "$(stat -c "%d" "$arkStagingDir")" ]; then cp -al "$arkserverroot/ShooterGame/." "$arkStagingDir/ShooterGame" cp -al "$arkserverroot/Engine/." "$arkStagingDir/Engine" cp -al "$arkserverroot/linux64/." "$arkStagingDir/linux64" cp -al "$arkserverroot/PackageInfo.bin" "$arkStagingDir/PackageInfo.bin" cp -al "$arkserverroot/steamclient.so" "$arkStagingDir/steamclient.so" cp -a "$arkserverroot/steamapps/." "$arkStagingDir/steamapps" else rsync -a "$arkserverroot/." "$arkStagingDir/." fi rm -rf "$arkStagingDir/ShooterGame/Content/Mods/"* rm -rf "$arkStagingDir/ShooterGame/Saved/"* fi echo -n "Downloading ARK update" cd "$steamcmdroot" runSteamCMDAppUpdate "$arkStagingDir" $validate if [ -d "${arkStagingDir}/steamapps/downloading/${appid}" ]; then echo "Update download interrupted" return 1 fi fi fi if [ -n "$downloadonly" ]; then if [ -n "$appupdate" -a -n "$arkStagingDir" -a "$arkStagingDir" != "$arkserverroot" ]; then echo "Server update downloaded" fi if [ -n "$modupdate" ]; then echo "Mod update downloaded" fi echo "Not applying update - download-only enabled" elif [ -n "$appupdate" -o -n "$modupdate" ]; then if isTheServerRunning; then if [ "$updatetype" == "safe" ]; then while [ ! `find $arkserverroot/ShooterGame/Saved/SavedArks -mmin -1 -name ${serverMap##*/}.ark` ]; do echo "`timestamp`: Save file older than 1 minute. Delaying update." >> "$logdir/update.log" sleep 30s done echo "`timestamp`: Save file newer than 1 minute. Performing an update." >> "$logdir/update.log" elif [ "$updatetype" == "warn" ]; then if ! doWarn update; then return 1 fi elif [ "$updatetype" == "ifempty" ]; then numplayers=$(( $(numPlayersConnected) + 0 )) if (( numplayers != 0 )); then echo "${numplayers} players are still connected" return 1 fi fi fi # 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 if [ -n "$saveworld" ]; then echo "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 echo "Applying update from staging directory" if [ "$(stat -c "%d" "$arkserverroot")" == "$(stat -c "%d" "$arkStagingDir")" ]; then cp -alu --remove-destination "$arkStagingDir/ShooterGame/." "$arkserverroot/ShooterGame" cp -alu --remove-destination "$arkStagingDir/Engine/." "$arkserverroot/Engine" cp -alu --remove-destination "$arkStagingDir/linux64/." "$arkserverroot/linux64" cp -alu --remove-destination "$arkStagingDir/PackageInfo.bin" "$arkserverroot/PackageInfo.bin" cp -alu --remove-destination "$arkStagingDir/steamclient.so" "$arkserverroot/steamclient.so" cp -au --remove-destination "$arkStagingDir/steamapps/." "$arkserverroot/steamapps" else rsync -a "$arkStagingDir/." "$arkserverroot" fi cd "$arkserverroot" find Engine ShooterGame linux64 -depth -print | grep -v '^ShooterGame/\(Saved\|Content/Mods\)' | while read f; do if [ ! -e "${arkStagingDir}/${f}" ]; then if [ -f "$f" ]; then rm "${f}" else rmdir "${f}" fi fi done else echo -n "Performing ARK update" cd "$steamcmdroot" runSteamCMDAppUpdate "$arkserverroot" $validate fi # the current version should be the last version. We set our version getCurrentVersion echo "`timestamp`: update to $instver complete" >> "$logdir/update.log" fi if [ -n "$modupdate" ]; then for modid in $(getModIds); do if isModUpdateNeeded $modid; then echo "Updating mod $modid" doExtractMod $modid echo "`timestamp`: Mod $modid updated" >> "$logdir/update.log" fi done fi # we restart the server only if it was started before the update if [ $serverWasAlive -eq 1 ]; then doStart --noautoupdate fi else echo "Your server is already up to date! The most recent version is ${bnumber}." echo "`timestamp`: No update needed." >> "$logdir/update.log" fi; rm -f "${arkserverroot}/.ark-update.lock" } # # Get the Mod IDs of the installed mods and the requested mods # getModIds(){ ( echo "${serverMapModId}" echo "${ark_TotalConversionMod}" echo "${ark_GameModIds}" | tr ',' '\n' find "${arkserverroot}/ShooterGame/Content/Mods" -maxdepth 1 -type d -printf "%P\n" ) | sort | uniq | grep '^[1-9][0-9]*$' } # # Downloads a mod from the Steam workshop # doDownloadMod(){ local modid=$1 local modsrcdir="$steamcmdroot/steamapps/workshop/content/$mod_appid/$modid" local moddldir="$steamcmdroot/steamapps/workshop/downloads/$mod_appid" cd "$steamcmdroot" retries=10 while true; do echo -n "Downloading mod $modid" local output=$(runSteamCMDspinnerSubst 5 +workshop_download_item $mod_appid $modid) 5>&1 result=$? if [ $result -eq 0 ]; then modsrcdir="$(echo "$output" | sed -n 's@^Success. Downloaded item [0-9][0-9]* to "\([^"]*\)" .*@\1@p')" break else echo if [ ! -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 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(){ for modid in $(getModIds); do doDownloadMod $modid || return 1 done } # # Checks if the files a mod owns need to be updated # isModUpdateNeeded(){ local modid=$1 local modsrcdir="$steamcmdroot/steamapps/workshop/content/$mod_appid/$modid" local moddestdir="$arkserverroot/ShooterGame/Content/Mods/$modid" local modbranch="${mod_branch:-Windows}" 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 modsrcdir="$steamcmdroot/steamapps/workshop/content/$mod_appid/$modid" if [ -n "${modsrcdirs[$modid]}" ]; then modsrcdir="${modsrcdirs[$modid]}" fi modname="$(curl -s "http://steamcommunity.com/sharedfiles/filedetails/?id=${modid}" | sed -n 's|^.*