diff --git a/.gitignore b/.gitignore index e43b0f9..6259ca6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .DS_Store +.*.sw? +*~ diff --git a/.version b/.version index c239c60..810ee4e 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -1.5 +1.6 diff --git a/README.asciidoc b/README.asciidoc index 31abc77..c224142 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -129,6 +129,9 @@ Instanceless commands `useconfig`:: Legacy command for specifying an instance for the following command(s) +`remove-mods`:: + Remove the specified mods from the `steamcmd` workshop directory + Commands acting on instances ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -146,12 +149,20 @@ instances. `--noautoupdate`;; Disables automatic updating on startup if it is enabled + `--alwaysrestart`;; + Enable automatically restarting the server even if it crashes + without becoming ready for player connections. + + `stop`:: Stops the server if it is running `--warn`;; Warns any connected players that the server is going down + `--warnreason`;; + Gives a reason for the shutdown. Defaults to `maintenance` + `--saveworld`;; Saves the world using `saveworld` - usually not necessary, as server usually saves the world on a graceful @@ -161,6 +172,9 @@ instances. Runs the `stop` command followed by the `restart` command. Accepts and passes the options for those commands + `--warnreason`;; + Gives a reason for the restart. Defaults to `a restart` + `install`:: Downloads and installs (or validates an existing install) of the ARK server @@ -205,21 +219,51 @@ instances. Downloads the update but does not apply it. Only has effect if a staging directory is set. +`cancelshutdown`:: + Cancels a pending update / shutdown / restart that was run with + the `--warn` option + `checkupdate`:: Checks if an ARK server update is available -`installmod `:: - Installs the specified mod into the `ShooterGame/Content/Mods` +`installmods`:: + Installs all mods specified in the instance config into the + `ShooterGame/Content/Mods` directory + +`uninstallmods`:: + Deletes all mods from the `ShooterGame/Content/Mods` directory + +`installmod [,[,...]]`:: + Installs the specified mods into the `ShooterGame/Content/Mods` directory -`uninstallmod `:: - Deletes the specified mod from the `ShooterGame/Content/Mods` +`uninstallmod [,[,...]]`:: + Deletes the specified mods from the `ShooterGame/Content/Mods` directory -`reinstallmod `:: +`removemod [,[,...]]`:: + Deletes the specified mods from the SteamCMD workshop directory + +`reinstallmod [,[,...]]`:: Runs the `uninstallmod` command followed by the `installmod` command +`enablemod `:: +`enablemod =`:: + Enables the `arkmod_` setting in the instance config. + modtype defaults to `game`. + Mod types: + + `game`;; + A mod in `GameModIds` + + `map`;; + The `MapModId` mod + + `tc`;; + `totalconversion`;; + The `TotalConversionMod` mod + `backup`:: Backs up the saved world and game config files to a compressed tar file in the backups directory specified in the config @@ -238,6 +282,46 @@ instances. `status`:: Prints the status of the ARK server +`install-cronjob `:: + Installs a cron job that executes the specified command. + This accepts any of the options the specified command accepts, + as well as the following options. In order to specify an + argument to the command (e.g. to the `broadcast` command), + use the `--arg=` option. + Please read your `man 5 crontab` manpage to determine what + minute and hour values are valid, as some implementations + may not accept e.g. the `*/n` minute / hour specification. + + `--daily`;; + The command should be executed daily + + `--hourly`;; + The command should be executed hourly + + `--hour=`;; + Specifies one or more hours when the command should execute. + This is the hour field of the cron job. + If you want to have the command execute every n hours, then + use `--hour='*/n'` + Default: `*` (i.e. all hours) + + `--minute=`;; + Specifies one or more minutes of the hour when the command + should execute. This is the minute field of the cron job. + If you want to have the command execute every n minutes, + then use `--minute='*/n'` + Default: `0` (i.e. the first minute of the hour) + + `--enable-output`;; + Enables the output from the command - the cron daemon usually + emails this to the user specified in the cron configuration + + `--arg=`;; + Specifies an argument to pass to the command + +`remove-cronjob `:: + Removes a cron job previously installed by `install-cronjob` + Configuration files ------------------- @@ -299,6 +383,12 @@ The following options can be overridden on a per-instance basis: The relative path within an ARK server install to place the autorestart lock file +`arkAlwaysRestartOnCrash`:: + Set to `true` to enable automatically restarting even when the + server has not become ready for player connections. + Be aware that this may cause the server to enter an endless + crash-restart loop if the cause of the crash is not resolved. + `arkAutoUpdateOnStart`:: Set to `true` to enable updating before server startup @@ -312,6 +402,16 @@ The following options can be overridden on a per-instance basis: `arkMaxBackupSizeMB`:: Limits the size of the stored backups +`arkPriorityBoost`:: + Attempts to boost the priority of the ARK server. + Negative values give a higher priority, and positive values give a lower priority. + Requires `sudo` and `renice` + +`arkCpuAffinity`:: + Attempts to set the CPU affinity of the ARK server. + Setting is a comma-delimited list of processor indices on which the server should run. + Requires `sudo` and `taskset` + `msgWarnUpdateMinutes`:: `msgWarnUpdateSeconds`:: `msgWarnRestartMinutes`:: @@ -321,6 +421,32 @@ The following options can be overridden on a per-instance basis: Templated messages for warnings, where `%d` is replaced with the number of minutes / seconds before the update / restart / shutdown +`msgWarnReason`:: +`msgTimeMinutes`:: +`msgTimeSeconds`:: +`msgReasonUpdateApp`:: +`msgReasonUpdateMod`:: +`msgReasonUpdateAppMod`:: +`msgReasonRestart`:: +`msgReasonShutdown`:: + Alternative templated messages for warnings with the following + replacement parameters: + + `{reason}`;; + Valid in `msgWarnReason`, replaced at runtime with the appropriate `msgReason*` template + + `{time}`;; + Valid in `msgWarnReason` and `msgReason*`, replaced at runtime with the appropriate `msgTime*` template + + `{modnamesupdated}`;; + Valid in `msgReason*Mod`, replaced at runtime with a comma-delimited list of updated mod names + + `{minutes}`;; + Valid in `msgTimeMinutes`, replaced at runtime with minutes remaining until shutdown + + `{seconds}`;; + Valid in `msgTimeSeconds`, replaced at runtime with seconds remaining until shutdown + `logdir`:: Specifies where to store log files @@ -368,7 +494,7 @@ Instance config files Instance config files are stored under `/etc/arkmanager/instances/.cfg`, -`~/.local/config/arkmanager/instances/.cfg` +`~/.config/arkmanager/instances/.cfg` or as specified in the `configfile_` options in the global config. @@ -401,6 +527,30 @@ the global config. the `-StructureDestructionTag=DestroySwampSnowStructures` option. +`arkmod_=`:: + Specifies a mod that can be enabled or disabled using + `enablemod` and `disablemod`. Note that mod ids specified + using these options are in addition to those specified directly + in the `ark_GameModIds` option, and override those specified in the + `ark_MapModId`, `serverMapMod` and `ark_TotalConversionMod` + options. Options are processed in the order they are specified + in the instance config file, and `arkmod_*` options in the + common config file are not applied. + Mod types: + + `game`;; + A mod to be specified in `GameModIds` + + `map`;; + The mod to be specified in `MapModId` + + `tc`;; + `totalconversion`;; + The mod to be specified in `TotalConversionMod` + + `disabled`;; + A disabled mod + Common ARK options ~~~~~~~~~~~~~~~~~~ diff --git a/netinstall.sh b/netinstall.sh index e1e679a..7ddab9b 100644 --- a/netinstall.sh +++ b/netinstall.sh @@ -4,6 +4,8 @@ # Net Installer, used with curl # +arkstGithubRepo="FezVrasta/ark-server-tools" + steamcmd_user="$1" channel=${2:-master} # if defined by 2nd argument install the defined version, otherwise install master shift @@ -19,50 +21,96 @@ elif [[ "$1" =~ ^--output= ]]; then shift fi -# Download and untar installation files -cd /tmp -COMMIT="`curl -L -k -s https://api.github.com/repos/FezVrasta/ark-server-tools/git/refs/heads/${channel} | sed -n 's/^ *"sha": "\(.*\)",.*/\1/p'`" - -if [ -z "$COMMIT" ]; then - if [ "$channel" != "master" ]; then - echo "Channel ${channel} not found - trying master" - channel=master - COMMIT="`curl -L -k -s https://api.github.com/repos/FezVrasta/ark-server-tools/git/refs/heads/${channel} | sed -n 's/^ *"sha": "\(.*\)",.*/\1/p'`" - fi +unstable= +if [ "$1" = "--unstable" ]; then + unstable=1 fi -if [ -z "$COMMIT" ]; then - echo "Unable to retrieve latest commit" +userinstall= +if [ "$1" = "--perform-user-install" ]; then + userinstall=yes +fi + +if [[ "$steamcmd_user" == "--me" && -z "$userinstall" ]]; then + echo "You have requested a user-install. You probably don't want this." + echo "A user-install will create ~/.config/arkmanager/instances/main.cfg" + echo "This config file will override /etc/arkmanager/instances/main.cfg" + echo "Add --perform-user-install if you really want this." exit 1 fi -mkdir ark-server-tools-${channel} -cd ark-server-tools-${channel} -curl -L -k -s https://github.com/FezVrasta/ark-server-tools/archive/${COMMIT}.tar.gz | tar xz +function doInstallFromCommit(){ + local commit="$1" + tmpdir="$(mktemp -t -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}'|" \ + arkmanager + echo "Running install.sh" + bash install.sh "$steamcmd_user" "${reinstall_args[@]}" + result=$? + cd / + rm -rf "$tmpdir" -# Install ARK Server Tools -cd ark-server-tools-${COMMIT}/tools -sed -i "s|^arkstCommit='.*'$|arkstCommit='${COMMIT}'|" arkmanager -version=`<../.version` -sed -i "s|^arkstVersion=\".*\"|arkstVersion='${version}'|" arkmanager -chmod +x install.sh -bash install.sh "$steamcmd_user" "$@" >"$output" 2>&1 + if [ "$result" = 0 ] || [ "$result" = 2 ]; then + echo "ARK Server Tools successfully installed" + else + echo "ARK Server Tools install failed" + fi + return $result +} -status=$? +function doInstallFromRelease(){ + local tagname= + local desc= -rm -rf /tmp/ark-server-tools-${channel} + echo "Getting latest release..." + # Read the variables from github + while IFS=$'\t' read 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') -# Print messages -case "$status" in - "0") - echo "ARK Server Tools were correctly installed in your system inside the home directory of $steamcmd_user!" - ;; + if [ -n "$tagname" ]; then + echo "Latest release is ${tagname}" + 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')" + doInstallFromCommit "$commit" + else + echo "Unable to get latest release" + return 1 + fi +} + +function doInstallFromBranch(){ + channel="$1" + commit="`curl -s "https://api.github.com/repos/${arkstGithubRepo}/git/refs/heads/${channel}" | sed -n 's/^ *"sha": "\(.*\)",.*/\1/p'`" + + if [ -z "$commit" ]; then + if [ -n "$unstable" ]; then + echo "Channel ${channel} not found - trying master" + doInstallFromBranch master + else + doInstallFromRelease + fi + else + doInstallFromCommit "$commit" + fi +} + +# Download and untar installation files +cd "$TEMP" + +if [ "$channel" = "master" ] && [ -z "$unstable" ]; then + doInstallFromRelease +else + doInstallFromBranch "$channel" +fi - "1") - echo "Something where wrong :(" - ;; - "2") - echo "WARNING: A previous version of ARK Server Tools was detected in your system, your old configuration was not overwritten. You may need to manually update it." - echo "ARK Server Tools were correctly installed in your system inside the home directory of $steamcmd_user!" - ;; -esac diff --git a/tools/arkmanager b/tools/arkmanager index 52c7633..f18494f 100755 --- a/tools/arkmanager +++ b/tools/arkmanager @@ -7,28 +7,21 @@ # Contributors: Sispheor, Atriusftw, klightspeed, lexat, puseidr # Script version -arkstVersion="1.5" +arkstVersion='1.6' +arkstTag='' arkstCommit='' +arkstGithubRepo="FezVrasta/ark-server-tools" +arkstRootUseEnv='' +arkstGlobalCfgFile='/etc/arkmanager/arkmanager.cfg' +arkstUserCfgFile='.arkmanager.cfg' 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=() + local reinstall_args=() if [ -n "$install_bindir" ]; then reinstall_args=( "${reinstall_args[@]}" "--bindir" "$install_bindir" ) fi @@ -38,23 +31,109 @@ doUpgradeTools() { 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 [ "$UID" == 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 -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 + 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 -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 + 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() { @@ -78,7 +157,12 @@ doUninstallTools() { runAsRoot(){ getConfigVar(){ - val="$(echo -ne "$(sed -n "/^$1=/{s|^[^=]*=||;s|[[:space:]]*\\(#.*\\)*\$||;s|^\"\\(.*\\)\"\$|\\1|;s|^'\\(.*\\)'\$|\\1|;p}" <"/etc/arkmanager/arkmanager.cfg" | tail -n1)")" + 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 @@ -86,7 +170,10 @@ runAsRoot(){ 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")" @@ -118,14 +205,16 @@ fi #--------------------- # Global variables -if [ -f "/etc/arkmanager/arkmanager.cfg" ]; then - source /etc/arkmanager/arkmanager.cfg +if [ -f "${arkstGlobalCfgFile}" ]; then + source "${arkstGlobalCfgFile}" fi -if [ -f "${HOME}/.arkmanager.cfg" ]; then - source "${HOME}/.arkmanager.cfg" +if [ -f "${HOME}/${arkstUserCfgFile}" ]; then + source "${HOME}/${arkstUserCfgFile}" fi +cd "$HOME" + lsof=lsof if [ -x /usr/sbin/lsof ]; then lsof=/usr/sbin/lsof @@ -148,9 +237,9 @@ 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}" +steamcmd_workshoplog="${steamcmd_workshoplog:-${HOME}/Steam/logs/workshop_log.txt}" if [ "$steamcmd_user" == "--me" ]; then install_datadir="${install_datadir:-${HOME}/.share/local/arkmanager}" @@ -158,6 +247,7 @@ else install_datadir="${install_datadir:-${install_bindir%/*}/share/arkmanager}" fi +declare -A modsrcdirs #--------------------- # functions @@ -167,7 +257,15 @@ fi # timestamp # timestamp() { - date +%T + 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}" } # @@ -199,6 +297,7 @@ checkConfig() { # SavedArks directory if [ -n "$arkserverroot" ]; then local savedarksdir="${arkserverroot}/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 @@ -306,7 +405,7 @@ rconcmd() { my $reqid = 1; sendpkt($sock, $reqid, 3, $password); my ($resid, $restype, $rcvbody) = recvpkt($sock); - die "Authentication failed" if $resid == -1; + die "Authentication failed" if $resid == -1 or $resid == 0xFFFFFFFF; } my $port = $ARGV[0]; @@ -320,10 +419,10 @@ rconcmd() { auth($socket, $password); sendpkt($socket, 2, 2, $command); my ($resid, $restype, $rcvbody) = recvpkt($socket); - if ($rcvbody eq "Server received, But no response!!") { + if ($rcvbody eq "Server received, But no response!! \n ") { print "Command processed\n"; } else { - print $rcvbody, "\n"; + print "\"", $rcvbody, "\"\n"; } ' "$(getRconPort)" "${ark_MultiHome:-127.0.0.1}" "$adminpass" "$1" } @@ -357,27 +456,48 @@ doBroadcastWithEcho(){ doBroadcast "$1" } +# +# 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(){ - "$steamcmdroot/$steamcmdexec" +@NoPromptForPassword 1 +login ${steamlogin:-anonymous} "$@" +quit + "$steamcmdroot/$steamcmdexec" +@NoPromptForPassword 1 ${steamcmd_cmds_prelogin} +login ${steamlogin:-anonymous} ${steamcmd_cmds_postlogin} "$@" +quit } function runSteamCMDspinner(){ if [ -n "$verbose" ]; then - echo - runSteamCMD "$@" + 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 <&1 >/dev/null 2>&1; then + if stty <&2 >/dev/null 2>&1; then progressDisplayType=spinner else progressDisplayType=dots fi fi - runSteamCMD "$@" >/dev/null 2>&1 & + 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\\' ) @@ -396,6 +516,12 @@ function runSteamCMDspinner(){ fi } +function runSteamCMDspinnerSubst(){ + local fd="$1" + shift + runSteamCMDspinner "$@" 3>&1 >/dev/fd/${fd} +} + # # Check if a new version is available but not apply it # @@ -424,10 +550,13 @@ function checkForUpdate(){ # Return 0 if update is needed, else return 1 # function isUpdateNeeded(){ - getCurrentVersion - getAvailableVersion - if [[ "$bnumber" == "Unknown" || "$bnumber" -eq "$instver" ]]; then + 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 @@ -465,10 +594,16 @@ function parseSteamACF(){ # 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="" + 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 } @@ -477,17 +612,45 @@ function getCurrentVersion(){ # 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 + 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 +} + +# +# Check if the update manifest matches the current manifest +# +function checkUpdateManifests(){ + appinfo="$(runSteamCMD +app_info_print "$appid" +quit)" + while read depot manifest <&3; do + newmanifest="$(echo "${appinfo}" | while read name val; do if [ "${name}" == "{" ]; then parseSteamACF ".depots.${depot}.manifests" "public"; break; fi; done)" + if [ "${newmanifest}" != "${manifest}" ]; then + return 1 + fi + done 3< <(sed -n '/^[{]$/,/^[}]$/{/^\t"MountedDepots"$/,/^\t[}]$/{/^\t\t/p}}' "${arkserverroot}/steamapps/appmanifest_${appid}.acf") + return 0 } # # Get the PID of the server process # function getServerPID(){ - ps -ef | grep "$arkserverroot/$arkserverexec" | grep -v grep | awk '{print $2}' + 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 + + if [[ -z "$arkopt_clusterid" || -f "${arkserverroot}/${arkoldautorestartfile}" ]]; then + ps -ef | grep "$arkserverroot/$arkserverexec" | grep -v grep | awk '{print $2}' + fi } # @@ -506,8 +669,11 @@ function isTheServerRunning(){ # # function isTheServerUp(){ - $lsof -i "${ark_MultiHome:+udp@}${ark_MultiHome}:$(getGamePort)" > /dev/null - result=$? + result=1 + if [ ! -x "$lsof" ]; then + "$lsof" -i "${ark_MultiHome:+udp@}${ark_MultiHome}:$(getGamePort)" > /dev/null + result=$? + fi if [ $result -ne 0 ]; then perl -MSocket -MFcntl -e ' my $port = int($ARGV[0]); @@ -570,30 +736,110 @@ function isTheServerOnline(){ # 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 "0" 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"; - ' "${ark_QueryPort}" "${ark_MultiHome:-127.0.0.1}" + 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)" "${ark_MultiHome:-127.0.0.1}" + 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)" "${ark_MultiHome:-127.0.0.1}" + fi } # # run function # doRun() { - cd "$arkserverroot" + 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" + 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 + done < <(sed -n 's/^\(arkmod_[^= ]*\)=.*/\1/p' <"$configfile") + if [ -n "$serverMapModId" ]; then - arkserveropts="-MapModID=$serverMapModId" + 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 @@ -602,6 +848,42 @@ doRun() { 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_}" @@ -638,11 +920,15 @@ doRun() { 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" - # set max open files limit before we start the server - ulimit -n $maxOpenFiles serverpid=0 restartserver=1 @@ -650,10 +936,11 @@ doRun() { # Shutdown the server when we are terminated shutdown_server(){ restartserver=0 - rm "$arkserverroot/$arkautorestartfile" + rm -f "$arkserverroot/$arkautorestartfile" if [ "$serverpid" -ne 0 ]; then - kill -INT $serverpid + kill -INT $serverpid >/dev/null 2>&1 fi + exit 0 } trap shutdown_server INT TERM @@ -667,10 +954,17 @@ doRun() { "$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 + 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 @@ -678,6 +972,7 @@ doRun() { # Grab the current server PID local pid="`getServerPID`" if [ "$pid" == "$serverpid" ]; then + serverdowntries=0 if [ "$restartserver" -eq 0 ]; then # Check if the server has fully started if ! isTheServerUp; then @@ -686,6 +981,28 @@ doRun() { touch "$arkserverroot/$arkautorestartfile" restartserver=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" + 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'" @@ -705,6 +1022,7 @@ doRun() { # doStop will remove the autorestart file if [ ! -f "$arkserverroot/$arkautorestartfile" ]; then restartserver=0 + fi if [ "$restartserver" -ne 0 ]; then @@ -717,22 +1035,57 @@ doRun() { # start function # doStart() { - if isTheServerRunning; then - echo "The server is already running" + 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 if [ "$arkAutoUpdateOnStart" == "true" ]; then if ! [[ " $* " =~ " --noautoupdate " ]]; then - echo "Updating server" - doUpdate --update-mods + logprint "Checking for updates before starting" + doUpdate --update-mods --no-autostart 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" + if [[ " $* " =~ " --alwaysrestart " ]]; then + arkAlwaysRestartOnCrash=true + fi + tput sc + logprint "The server is starting..." + + local pid=$! + if [[ -n "$arkPriorityBoost" || -n "$arkCpuAffinity" ]]; then + doRun --wait >(while read -r l; do printf "%s: [%s] %s\n" "$(timestamp)" "${instance}" "${l}" >>"$logdir/$arkserverLog"; done) 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 + doRun >"$logdir/$arkserverLog" 2>&1 & # output of this command is logged + fi tput rc; tput ed; - echo "The server is now running, and should be up within 10 minutes" + logprint "The server is now running, and should be up within 10 minutes" fi } @@ -755,20 +1108,40 @@ doStartAll(){ # stop the ARK server # doStop() { + if [ "$1" != "update" ]; then + rm -f "${arkserverroot}/.startAfterUpdate-${instance}" + fi + if isTheServerRunning; then - if [[ " $* " =~ " --warn " ]]; then - doWarn "$1" + 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 [[ " $* " =~ " --saveworld " ]]; then + if [[ -n "$dosave" ]]; then doSaveWorld fi tput sc - echo "Stopping server..." - echo "`timestamp`: stopping" >> "$logdir/$arkmanagerLog" + logprint "Stopping server; reason: $stopreason" rm -f "$arkserverroot/$arkautorestartfile" + rm -f "$arkserverroot/$arkoldautorestartfile" # kill the server with the PID PID=`getServerPID` - kill -INT $PID + kill -INT $PID >/dev/null 2>&1 for (( i = 0; i < 20; i++ )); do sleep 1 @@ -779,13 +1152,23 @@ doStop() { if isTheServerRunning; then tput rc - echo "Killing server..." - kill -KILL $PID + 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; - echo "The server has been stopped" - echo "`timestamp`: stopped" >> "$logdir/$arkmanagerLog" + logprint "The server has been stopped" else echo "The server is already stopped" fi @@ -831,9 +1214,164 @@ doInstall() { cd "$steamcmdroot" echo -n "Installing ARK server" # install the server + doDownloadSteamCMD runSteamCMDAppUpdate "$arkserverroot" validate # the current version should be the last version. We set our version - getCurrentVersion + 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 + 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" +} + +# +# Checks if a player has requested an update cancel in the last 5 minutes +# +isUpdateCancelRequested(){ + if [ -n "$chatCommandRestartCancel" ]; then + local canceltime="$( + find "${arkserverroot}/ShooterGame/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 } # @@ -842,110 +1380,127 @@ doInstall() { doWarn(){ cd "$arkserverroot" - local warnmsgmin - local warnmsgsec - - if [ "$1" == "update" ]; then - if [ -n "$msgWarnUpdateMinutes" ]; then - warnmsgmin="$msgWarnUpdateMinutes" - else - warnmsgmin="This ARK server will shutdown for an update in %d minutes" - fi - if [ -n "$msgWarnUpdateSeconds" ]; then - warnmsgsec="$msgWarnUpdateSeconds" - else - warnmsgsec="This ARK server will shutdown for an update in %d seconds" - fi - elif [ "$1" == "restart" ]; then - if [ -n "$msgWarnRestartMinutes" ]; then - warnmsgmin="$msgWarnRestartMinutes" - else - warnmsgmin="This ARK server will shutdown for a restart in %d minutes" - fi - if [ -n "$msgWarnRestartSeconds" ]; then - warnmsgsec="$msgWarnRestartSeconds" - else - warnmsgsec="This ARK server will shutdown for a restart in %d seconds" - fi - else - if [ -n "$msgWarnShutdownMinutes" ]; then - warnmsgmin="$msgWarnShutdownMinutes" - else - warnmsgmin="This ARK server will shutdown in %d minutes" - fi - if [ -n "$msgWarnShutdownSeconds" ]; then - warnmsgsec="$msgWarnShutdownSeconds" - else - warnmsgsec="This ARK server will shutdown in %d seconds" - fi - fi - - 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 + ( + 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 - if (( warnminutes > warninterval )); then - sleep 1m & + 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}" + 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 + 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" + rm -f "${arkserverroot}/${arkwarnlockfile}" + return 1 + fi + if (( warnminutes >= warninterval )); then + sleep 1m & + sleeppid=$! + printWarnMessage "$1" "$2" "minutes" "$warnminutes" + for (( min = warnminutes; min >= warninterval; min-- )); do + numplayers=$(numPlayersConnected) + echo "There are ${numplayers} players connected" + if [[ "numplayers" == "-1" ]]; then + echo "Server is not running. Shutting down immediately" + return 0 + elif (( (numplayers + 0) == 0 )); then + doBroadcastWithEcho "Nobody is connected. Shutting down immediately" + rm -f "${arkserverroot}/${arkwarnlockfile}" + return 0 + fi + if isUpdateCancelRequested; then + doBroadcastWithEcho "Restart cancelled by player request" + return 1 + fi + wait $sleeppid + if (( $min > $warninterval )); then + sleep 1m & + sleeppid=$! + fi + done + warnminutes=$(( warninterval - 1 )) + 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=$! - warnmsg="$(printf "$warnmsgmin" "$warnminutes")" - doBroadcastWithEcho "$warnmsg" - for (( min = warnminutes - 1; min >= warninterval; min-- )); do + if [ "`getServerPID`" != "$pid" ]; then + echo "Server has stopped. Aborting update" + rm -f "${arkserverroot}/${arkwarnlockfile}" + return 1 + fi + printWarnMessage "$1" "$2" "seconds" "$warnseconds" + if (( warnseconds >= 20 )); then numplayers=$(numPlayersConnected) - if (( numplayers + 0 == 0 )); then - echo "Nobody is connected. Shutting down immediately" + echo "There are ${numplayers} players connected" + if [[ "numplayers" == "-1" ]]; then + echo "Server is not running. Shutting down immediately" + return 0 + elif (( (numplayers + 0) == 0 )); then + doBroadcastWithEcho "Nobody is connected. Shutting down immediately" + rm -f "${arkserverroot}/${arkwarnlockfile}" return 0 fi - wait $sleeppid - if (( $min > $warninterval )); then - sleep 1m & - sleeppid=$! + if isUpdateCancelRequested; then + doBroadcastWithEcho "Restart cancelled by player request" + return 1 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 - warnmsg="$(printf "$warnmsgsec" "$warnseconds")" - doBroadcastWithEcho "$warnmsg" - if (( warnseconds >= 20 )); then - numplayers=$(numPlayersConnected) - if (( numplayers + 0 == 0 )); then - echo "Nobody is connected. Shutting down immediately" - return 0 fi - fi - wait $sleeppid - warnseconds=$warninterval - done - fi + wait $sleeppid + warnseconds=$warninterval + done + fi - if [ "`getServerPID`" != "$pid" ]; then - echo "Server has stopped. Aborting $1" - return 1 - fi + rm -f "${arkserverroot}/${arkwarnlockfile}" - return 0 + if [ "`getServerPID`" != "$pid" ]; then + echo "Server has stopped. Aborting $1" + return 1 + fi + + return 0 + ) + + return $? } # @@ -953,60 +1508,73 @@ doWarn(){ # 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= 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 + case "$arg" in + --force) appupdate=1; force=1; ;; + --safe) updatetype=safe; ;; + --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; ;; + *) + echo "Unrecognized option $arg" + echo "Try 'arkmanager -h' or 'arkmanager --help' for more information." + exit 1 + esac done - echo "$$" >"${arkserverroot}/.ark-update.lock.$$" 2>/dev/null + # 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}/.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 + 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}/.ark-update.lock" + rm -f "${arkserverroot}/${arkupdatelockfile}" else break fi done - rm -f "${arkserverroot}/.ark-update.lock.$$" + rm -f "${arkserverroot}/${arkupdatelockfile}.${BASHPID}" + + logprint "Checking for update; PID: ${BASHPID}" if [ -n "$modupdate" ]; then - if ! doDownloadAllMods; then - modupdate= + if [ -z "$nodownload" ]; then + if ! doDownloadAllMods; then + modupdate= + fi fi if ! isAnyModUpdateNeeded; then modupdate= @@ -1015,78 +1583,104 @@ doUpdate() { cd "$arkserverroot" - if isUpdateNeeded; then + if [ -n "$appupdate" ] || isUpdateNeeded; then appupdate=1 if [ -n "${arkStagingDir}" -a "${arkStagingDir}" != "${arkserverroot}" ]; then if [ ! -d "$arkStagingDir/ShooterGame" ]; then - echo "Copying to staging directory" + logprint "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" + cp -l "$arkserverroot/"* "$arkStagingDir" >/dev/null 2>&1 else rsync -a "$arkserverroot/." "$arkStagingDir/." fi rm -rf "$arkStagingDir/ShooterGame/Content/Mods/"* rm -rf "$arkStagingDir/ShooterGame/Saved/"* + rm -rf "$arkStagingDir/Engine/Binaries/ThirdParty/SteamCMD/Linux/steamapps" + cp -al "$arkserverroot/ShooterGame/Content/Mods/111111111/." "$arkStagingDir/ShooterGame/Content/Mods/111111111" + cp -l "$arkserverroot/ShooterGame/Content/Mods/111111111.mod" "$arkStagingDir/ShooterGame/Content/Mods/111111111.mod" fi - echo -n "Downloading ARK update" - cd "$steamcmdroot" - runSteamCMDAppUpdate "$arkStagingDir" $validate - if [ -d "${arkStagingDir}/steamapps/downloading/${appid}" ]; then - echo "Update download interrupted" - return 1 + if [ -z "$nodownload" ]; then + echo -n "Downloading ARK update" + logprint "Downloading ARK update" >/dev/null + doDownloadSteamCMD + cd "$steamcmdroot" + if runSteamCMDAppUpdate "$arkStagingDir" $validate; then + rm -rf "${arkStagingDir}/steamapps/downloading/${appid}" + 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/$arkautorestartfile" && "$arkserverroot/$arkautorestartfile" -ot "${arkserverroot}/steamapps/appmanifest_${appid}.acf" ]]; then + logprint "Server was updated while it was running" + bgupdate=1 + fi + if [ -n "$downloadonly" ]; then if [ -n "$appupdate" -a -n "$arkStagingDir" -a "$arkStagingDir" != "$arkserverroot" ]; then - echo "Server update downloaded" + logprint "Server update downloaded" fi if [ -n "$modupdate" ]; then - echo "Mod update downloaded" + logprint "Mod update downloaded" fi - echo "Not applying update - download-only enabled" - elif [ -n "$appupdate" -o -n "$modupdate" ]; then + 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" == "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" + logprint "Save file older than 1 minute. Delaying update." sleep 30s done - echo "`timestamp`: Save file newer than 1 minute. Performing an update." >> "$logdir/update.log" + logprint "Save file newer than 1 minute. Performing an update." 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" + if (( numplayers != 0 )); then + logprint "${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" + logprint "Saving world" doSaveWorld fi - doStop + doStop update # If user wants to back-up, we do it here. @@ -1096,13 +1690,12 @@ doUpdate() { if [ -n "$appupdate" ]; then if [ -d "${arkStagingDir}" -a "${arkStagingDir}" != "${arkserverroot}" ]; then - echo "Applying update from staging directory" + logprint "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 -lu --remove-destination "$arkStagingDir/"* "$arkserverroot" >/dev/null 2>&1 cp -au --remove-destination "$arkStagingDir/steamapps/." "$arkserverroot/steamapps" else rsync -a "$arkStagingDir/." "$arkserverroot" @@ -1119,36 +1712,62 @@ doUpdate() { 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" $validate fi # the current version should be the last version. We set our version - getCurrentVersion - echo "`timestamp`: update to $instver complete" >> "$logdir/update.log" + instver="$(getCurrentVersion)" + logprint "Update to $instver complete" fi - if [ -n "$modupdate" ]; then + if [ -n "$modupdate" ] && [ -z "$arkflag_automanagedmods" ]; then for modid in $(getModIds); do if isModUpdateNeeded $modid; then - echo "Updating mod $modid" + logprint "Updating mod $modid" doExtractMod $modid - echo "`timestamp`: Mod $modid updated" >> "$logdir/update.log" + logprint "Mod $modid updated" fi done fi - - # we restart the server only if it was started before the update - if [ $serverWasAlive -eq 1 ]; then - doStart --noautoupdate + if [ -z "$bgupdate" ]; then + touch "${arkserverroot}/steamapps/appmanifest_${appid}.acf" fi else - echo "Your server is already up to date! The most recent version is ${bnumber}." - echo "`timestamp`: No update needed." >> "$logdir/update.log" + logprint "Your server is already up to date! The most recent version is ${bnumber}." fi; - rm -f "${arkserverroot}/.ark-update.lock" + 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 } # @@ -1159,8 +1778,27 @@ 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 find "${arkserverroot}/ShooterGame/Content/Mods" -maxdepth 1 -type d -printf "%P\n" - ) | sort | uniq | grep '^[1-9][0-9]*$' + ) | sort | uniq | grep '^[1-9][0-9]*$' | grep -v '^111111111$' +} + +# +# Checks if a mod update is available before trying to download it +isModUpdateAvailable(){ + local modid="$1" + if [ ! -f "$steamcmdroot/steamapps/workshop/appworkshop_${mod_appid}.acf" ]; then return 0; fi + local instmft="$(sed -n '/^\t"WorkshopItemsInstalled"$/,/^\t[}]$/{/^\t\t"'"${modid}"'"$/,/^\t\t[}]$/{s|^\t\t\t"manifest"\t\t"\(.*\)"$|\1|p}}' <"$steamcmdroot/steamapps/workshop/appworkshop_${mod_appid}.acf")" + if [ -z "$instmft" ]; then return 0; fi + local remmft="$(curl -s -d "itemcount=1&publishedfileids[0]=${modid}" http://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1 | sed -n 's|^[[:space:]]*"hcontent_file": "\(.*\)",|\1|p')" + if [[ -n "${remmft}" && "${instmft}" != "${remmft}" ]]; then + return 0 # true + fi + return 1 # false } # @@ -1168,20 +1806,36 @@ getModIds(){ # doDownloadMod(){ local modid=$1 + local steamcmdroot="$steamcmdroot" + if [ -n "$arkflag_automanagedmods" ]; then + steamcmdroot="$arkserverroot/Engine/Binaries/ThirdParty/SteamCMD/Linux" + fi + doDownloadSteamCMD local modsrcdir="$steamcmdroot/steamapps/workshop/content/$mod_appid/$modid" local moddldir="$steamcmdroot/steamapps/workshop/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" - runSteamCMDspinner +workshop_download_item $mod_appid $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 else echo - if [ ! -d "$moddldir" ]; then + 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 @@ -1191,10 +1845,11 @@ doDownloadMod(){ fi echo "Mod $modid not fully downloaded - retrying" fi - done + 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" @@ -1206,9 +1861,18 @@ doDownloadMod(){ # Downloads all installed and requested mods from the Steam workshop # doDownloadAllMods(){ + local fail=0 + local success=0 for modid in $(getModIds); do - doDownloadMod $modid || return 1 + if isModUpdateAvailable $nodid; then + if doDownloadMod $modid; then + success=1 + else + fail=1 + fi + fi done + [[ $success == 1 || $fail == 0 ]] && return 0 || return 1 } # @@ -1220,13 +1884,26 @@ isModUpdateNeeded(){ local moddestdir="$arkserverroot/ShooterGame/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" \) ] || [ "$(<"$moddestdir/.modbranch")" != "$modbranch" ]; then + 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 @@ -1245,17 +1922,49 @@ isModUpdateNeeded(){ 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|^.*
\([^<]*\)
.*|\1|p')" + + if [ -n "$modname" ]; then + echo "$modname" + else + perl -e ' + my $data; + { local $/; $data = ; } + my $mapnamelen = unpack("@0 L<", $data); + my $mapname = substr($data, 4, $mapnamelen - 1); + print $mapname + ' <"${modsrcdir}/mod.info" + fi +} + # # Checks if any installed or requested mods need to be updated # isAnyModUpdateNeeded(){ + modnamesupdated="" + local ismodupdateneeded=1 for modid in $(getModIds); do if isModUpdateNeeded $modid; then - return 0 + ismodupdateneeded=0 + if [ -n "$modnamesupdated" ]; then + modnamesupdated="${modnamesupdated}, " + fi + modnamesupdated="${modnamesupdated}$(getModName "$modid")" fi done - return 1 + return $ismodupdateneeded } # @@ -1267,13 +1976,26 @@ doExtractMod(){ local moddestdir="$arkserverroot/ShooterGame/Content/Mods/$modid" local modbranch="${mod_branch:-Windows}" + # Bypass the 111111111 modid used by Primitive+ + if [ "$modid" = "111111111" ]; then + return 0 + fi + + if [ -n "${modsrcdirs[$modid]}" ]; then + modsrcdir="${modsrcdirs[$modid]}" + fi + for varname in "${!mod_branch_@}"; do if [ "mod_branch_$modid" == "$varname" ]; then modbranch="${!varname}" fi done - if [ \( ! -f "$moddestdir/.modbranch" \) ] || [ "$(<"$moddestdir/.modbranch")" != "$modbranch" ]; then + 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 rm -rf "$moddestdir" fi @@ -1348,26 +2070,42 @@ doExtractMod(){ fi done + modname="$(curl -s "http://steamcommunity.com/sharedfiles/filedetails/?id=${modid}" | sed -n 's|^.*
\([^<]*\)
.*|\1|p')" + + if [ -f "${moddestdir}/.mod" ]; then + rm "${moddestdir}/.mod" + fi + 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); - print pack("L< L< L< Z8 L< C L< L<", $ARGV[0], 0, 8, "ModName", 1, 0, 1, $mapfilelen); - print $mapfile; + my $nummaps = unpack("@" . ($mapnamelen + 4) . " L<", $data); + my $pos = $mapnamelen + 8; + my $modname = ($ARGV[1] || $mapname) . "\x00"; + my $modnamelen = length($modname); + my $modpath = "../../../ShooterGame/Content/Mods/" . $ARGV[0] . "\x00"; + my $modpathlen = length($modpath); + print pack("L< L< L< Z$modnamelen L< Z$modpathlen L<", + $ARGV[0], 0, $modnamelen, $modname, $modpathlen, $modpath, + $nummaps); + for (my $mapnum = 0; $mapnum < $nummaps; $mapnum++){ + my $mapfilelen = unpack("@" . ($pos) . " L<", $data); + my $mapfile = substr($data, $mapnamelen + 12, $mapfilelen); + print pack("L< Z$mapfilelen", $mapfilelen, $mapfile); + $pos = $pos + 4 + $mapfilelen; + } print "\x33\xFF\x22\xFF\x02\x00\x00\x00\x01"; - ' $modid <"$moddestdir/mod.info" >"$moddestdir/.mod" + ' $modid "$modname" <"$moddestdir/mod.info" >"${moddestdir}.mod" if [ -f "$moddestdir/modmeta.info" ]; then - cat "$moddestdir/modmeta.info" >>"$moddestdir/.mod" + cat "$moddestdir/modmeta.info" >>"${moddestdir}.mod" else - echo -ne '\x01\x00\x00\x00\x08\x00\x00\x00ModType\x00\x02\x00\x00\x001\x00' >>"$moddestdir/.mod" + echo -ne '\x01\x00\x00\x00\x08\x00\x00\x00ModType\x00\x02\x00\x00\x001\x00' >>"${moddestdir}.mod" fi - echo "$modbranch" >"$moddestdir/.modbranch" + echo "$modbranch" >"$moddestdir/__arkmanager_modbranch__.info" fi } @@ -1375,27 +2113,105 @@ doExtractMod(){ # Downloads mod and installs it into mods directory # doInstallMod(){ - local modid=$1 + local modid + for modid in "${1//,/ }"; do + if [ -f "$steamcmdroot/steamapps/workshop/appworkshop_${mod_appid}.acf" ]; then + sed -i "/^\\t\\t\"${modid}\"/,/^\\t\\t}/d" "$steamcmdroot/steamapps/workshop/appworkshop_${mod_appid}.acf" + fi - if [ -f "$steamcmdroot/steamapps/workshop/appworkshop_${mod_appid}.acf" ]; then - sed -i "/^\\t\\t\"${modid}\"/,/^\\t\\t}/d" "$steamcmdroot/steamapps/workshop/appworkshop_${mod_appid}.acf" - fi + if doDownloadMod $modid; then + doExtractMod $modid + echo "Mod $modid installed" + fi + done +} - if doDownloadMod $modid; then - doExtractMod $modid - echo "Mod $modid installed" - fi +# +# Downloads and installs all requested mods +# +doInstallAllMods(){ + for modid in $(getModIds); do + doInstallMod "$modid" + done +} + +# +# Removes all mods from the mods directory +# +doUninstallAllMods(){ + for modid in $(getModIds); do + if [[ "$modid" != "111111111" && "$modid" != "TheCenter" ]]; then + doUninstallMod "$modid" + fi + done } # # Removes mod from mods directory # doUninstallMod(){ - local modid=$1 - local moddir="$arkserverroot/ShooterGame/Content/Mods/$modid" - if [ -d "${moddir}" ]; then - rm -rf "${moddir}" + local modid + for modid in "${1//,/ }"; do + local moddir="$arkserverroot/ShooterGame/Content/Mods/$modid" + local modfile="$arkserverroot/ShooterGame/Content/Mods/${modid}.mod" + if [ -d "${moddir}" ]; then + rm -rf "${moddir}" + fi + if [ -f "${modfile}" ]; then + rm -f "$modfile" + fi + done +} + +# +# Enables a mod in the config +# +doEnableMod(){ + local modid="${1%=*}" + local modtype="${1#*=}" + if [ "$modtype" = "$1" ]; then + modtype=game fi + local modvar="arkmod_${modid}" + if [ -n "${!modvar}" ]; then + sed -i "s|^\(${modvar}\)=[^ ]*|\1=${modtype}|" "$configfile" + else + echo "${modvar}=${modtype}" >>"$configfile" + fi +} + +# +# Disable a mod in the config +# +doDisableMod(){ + local modid="$1" + local modvar="arkmod_$modid" + if [ "$ark_GameModIds" = *"$modid"* ]; then + sed -i "s|^\(ark_GameModIds=\(\|[\"']\)\(\|[^\"' ]*,\)\)${modid},*|\1|" "$configfile" + fi + if [ -n "$modvar" ]; then + sed -i "s|^\(arkmod_${modid}\)=[^ ]*|\1=disabled|" "$configfile" + fi +} + +# +# Removes mod from steamcmd workshop directory +# +doRemoveMods(){ + local modid + for modid in ${1//,/ }; do + if [ -f "$steamcmdroot/steamapps/workshop/appworkshop_${mod_appid}.acf" ]; then + sed -i "/^\\t\\t\"${modid}\"/,/^\\t\\t}/d" "$steamcmdroot/steamapps/workshop/appworkshop_${mod_appid}.acf" + fi + + if [ -d "$steamcmdroot/steamapps/workshop/content/${mod_appid}/${modid}" ]; then + rm -rf "$steamcmdroot/steamapps/workshop/content/${mod_appid}/${modid}" + fi + + if [ -d "$steamcmdroot/steamapps/workshop/downloads/${mod_appid}/${modid}" ]; then + rm -rf "$steamcmdroot/steamapps/workshop/downloads/${mod_appid}/${modid}" + fi + done } # @@ -1429,18 +2245,59 @@ doBackup(){ savedir="${ark_AltSaveDirectoryName}" fi - # ARK server uses Write-Unlink-Rename - echo -ne "${NORMAL} Copying ARK world file " - cp -p "${arkserverroot}/ShooterGame/Saved/${savedir}/${serverMap##*/}.ark" "${backupdir}/${serverMap##*/}.ark" - if [ ! -f "${backupdir}/${serverMap##*/}.ark" ]; then - sleep 2 - cp -p "${arkserverroot}/ShooterGame/Saved/${savedir}/${serverMap##*/}.ark" "${backupdir}/${serverMap##*/}.ark" + saverootdir="${arkserverroot}/ShooterGame/Saved" + savedcfgdir="${saverootdir}/Config/LinuxServer" + 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" + savedir="${cisavedir}" + else + echo -e " ${NORMAL}[ ${RED}ERROR${NORMAL} ] Saved arks directory does not exist" + return 1 + fi fi + + echo "${NORMAL} Saved arks directory is ${savedir}" + + # ARK server uses Write-Unlink-Rename + echo -ne "${NORMAL} Copying ARK world file (${serverMap}) " + + # Take into account screwed up casing of saved ark files + # in some environments + mapfile="$(find "${savedir}" -iname "${serverMap##*/}.ark" | head -n1)" + + if [ -z "$mapfile" ]; then + sleep 2 + mapfile="$(find "${savedir}" -iname "${serverMap##*/}.ark" | head -n1)" + fi + # If both attempts fail, server may have # crashed between unlink and rename - if [ ! -f "${backupdir}/${serverMap##*/}.ark" ]; then - cp -p "${arkserverroot}/ShooterGame/Saved/${savedir}/${serverMap##*/}.tmp" "${backupdir##*/}/${serverMap##*/}.ark" + if [ -z "$mapfile" ]; then + mapfile="$(find "${savedir}" -iname "${serverMap##*/}.tmp" | head -n1)" fi + + # If neither the ark nor the tmp file exists, then the + # map name may be incorrect. Try to get any ark or tmp + # file in the saved arks directory + if [ -z "$mapfile" ]; then + mapfile="$(find "${savedir}" -iname "*.ark" | head -n1)" + + if [ -z "$mapfile" ]; then + mapfile="$(find "${savedir}" -iname "*.tmp" | head -n1)" + fi + fi + + if [ -f "${mapfile}" ]; then + cp -p "${mapfile}" "${backupdir}/${serverMap##*/}.ark" + fi + if [ -f "${backupdir}/${serverMap##*/}.ark" ]; then echo -e "${NORMAL}\e[68G[ ${GREEN}OK${NORMAL} ]" else @@ -1452,49 +2309,76 @@ doBackup(){ # ARK server uses a non-blocking lock and will # fail to update the file if the lock fails. echo -e "${NORMAL} Copying ARK profile files" - for f in "${arkserverroot}/ShooterGame/Saved/${savedir}/"*.arkprofile; do - echo -ne "${NORMAL} ${f##*/} " - cp -p "${f}" "${backupdir}/${f##*/}" - if [ ! -s "${backupdir}/${f##*/}" ]; then - sleep 2 + for f in "${savedir}/"*.arkprofile; do + if [ -f "${f}" ]; then + echo -ne "${NORMAL} ${f##*/} " cp -p "${f}" "${backupdir}/${f##*/}" - fi - # If both attempts fail, server may have - # crashed between truncate and write - if [ ! -s "${backupdir}/${f##*/}" ]; then - cp -p "${f%.arkprofile}.tmpprofile" "${backupdir}/${f##*/}" - fi - if [ -s "${backupdir}/${f##*/}" ]; then - echo -e "${NORMAL}\e[68G[ ${GREEN}OK${NORMAL} ]" - else - echo -e "${NORMAL}\e[68G[ ${RED}FAILED${NORMAL} ]" + if [ ! -s "${backupdir}/${f##*/}" ]; then + sleep 2 + cp -p "${f}" "${backupdir}/${f##*/}" + fi + # If both attempts fail, server may have + # crashed between truncate and write + if [[ ! -s "${backupdir}/${f##*/}" && -f "${f%.arkprofile}.tmpprofile" ]]; then + cp -p "${f%.arkprofile}.tmpprofile" "${backupdir}/${f##*/}" + fi + if [ -s "${backupdir}/${f##*/}" ]; then + echo -e "${NORMAL}\e[68G[ ${GREEN}OK${NORMAL} ]" + else + echo -e "${NORMAL}\e[68G[ ${RED}FAILED${NORMAL} ]" + fi fi done # ARK server uses Lock-Truncate-Write-Unlock echo -e "${NORMAL} Copying ARK tribe files " - for f in "${arkserverroot}/ShooterGame/Saved/${savedir}/"*.arktribe; do - echo -ne "${NORMAL} ${f##*/} " - cp -p "${f}" "${backupdir}/${f##*/}" - if [ ! -s "${backupdir}/${f##*/}" ]; then - sleep 2 + for f in "${savedir}/"*.arktribe; do + if [ -f "${f}" ]; then + echo -ne "${NORMAL} ${f##*/} " cp -p "${f}" "${backupdir}/${f##*/}" + if [ ! -s "${backupdir}/${f##*/}" ]; then + sleep 2 + cp -p "${f}" "${backupdir}/${f##*/}" + fi + # If both attempts fail, server may have + # crashed between truncate and write + if [[ ! -s "${backupdir}/${f##*/}" && -f "${f%.arktribe}.tmptribe" ]]; then + cp -p "${f%.arktribe}.tmptribe" "${backupdir}/${f##*/}" + fi + if [ -s "${backupdir}/${f##*/}" ]; then + echo -e "${NORMAL}\e[68G[ ${GREEN}OK${NORMAL} ]" + else + echo -e "${NORMAL}\e[68G[ ${RED}FAILED${NORMAL} ]" + fi fi - # If both attempts fail, server may have - # crashed between truncate and write - if [ ! -s "${backupdir}/${f##*/}" ]; then - cp -p "${f%.arktribe}.tmptribe" "${backupdir}/${f##*/}" - fi - if [ -s "${backupdir}/${f##*/}" ]; then - echo -e "${NORMAL}\e[68G[ ${GREEN}OK${NORMAL} ]" - else - echo -e "${NORMAL}\e[68G[ ${RED}FAILED${NORMAL} ]" + done + + # ARK server uses Lock-Truncate-Write-Unlock + echo -e "${NORMAL} Copying ARK tribute tribe files " + for f in "${savedir}/"*.arktributetribe; do + if [ -f "${f}" ]; then + echo -ne "${NORMAL} ${f##*/} " + cp -p "${f}" "${backupdir}/${f##*/}" + if [ ! -s "${backupdir}/${f##*/}" ]; then + sleep 2 + cp -p "${f}" "${backupdir}/${f##*/}" + fi + # If both attempts fail, server may have + # crashed between truncate and write + if [[ ! -s "${backupdir}/${f##*/}" && -f "${f%.arktributetribe}.tmptributetribe" ]]; then + cp -p "${f%.arktributetribe}.tmptributetribe" "${backupdir}/${f##*/}" + fi + if [ -s "${backupdir}/${f##*/}" ]; then + echo -e "${NORMAL}\e[68G[ ${GREEN}OK${NORMAL} ]" + else + echo -e "${NORMAL}\e[68G[ ${RED}FAILED${NORMAL} ]" + fi fi done # ARK server uses Lock-Truncate-Write-Unlock echo -ne "${NORMAL} Copying GameUserSettings.ini " - cp -p "${arkserverroot}/ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini" "${backupdir}/GameUserSettings.ini" + cp -p "${savedcfgdir}/GameUserSettings.ini" "${backupdir}/GameUserSettings.ini" if [ ! -s "${backupdir}/GameUserSettings.ini" ]; then sleep 2 cp -p "${f}" "${backupdir}/${f##*/}" @@ -1508,7 +2392,7 @@ doBackup(){ echo -ne "${NORMAL} Copying Game.ini " - cp -p "${arkserverroot}/ShooterGame/Saved/Config/LinuxServer/Game.ini" "${backupdir}/Game.ini" + cp -p "${savedcfgdir}/Game.ini" "${backupdir}/Game.ini" if [ ! -s "${backupdir}/Game.ini" ]; then sleep 2 cp -p "${f}" "${backupdir}/${f##*/}" @@ -1551,6 +2435,75 @@ doBackup(){ fi } +# +# Install a cron job to execute a particular command +# +doInstallCronJob(){ + hour='*' + minute='0' + cmdopts="${arkCronExtraOpts}" + cmdargs="" + instargs="" + output=">/dev/null 2>&1" + arkmanagerpath="${0}" + command="$1" + shift + + for opt in "$@"; do + case "$opt" in + --daily) + ;; + --hourly) + hour='*' + ;; + --hour=*) + hour="${opt#--hour=}" + ;; + --minute=*) + minute="${opt#--minute=}" + ;; + --enable-output) + output= + ;; + --arg=*) + cmdargs="${cmdargs} $(printf "%q" "${opt#--arg=}")" + ;; + --*) + cmdopts="${cmdopts} $(printf "%q" "${opt}")" + ;; + *) + cmdargs="${cmdargs} $(printf "%q" "${opt}")" + ;; + esac + done + + if [ -n "$allinstances" ]; then + instargs="@all" + else + for inst in "${instances[@]}"; do + instargs="${instargs} $(printf "%q" "@${inst}")" + done + fi + + cronjob="${minute} ${hour} * * * ${arkmanagerpath} --cronjob ${command} ${instargs} ${cmdopts} --args ${cmdargs} -- ${output}" + + (crontab -l | \ + sed -e "\\# [*] [*] [*] ${arkmanagerpath} --cronjob ${command} ${instargs} #d"; + echo "${cronjob}" ) | \ + crontab - +} + +# +# Removes an installed cron job +# +doRemoveCronJob(){ + arkmanagerpath="${0}" + command="$1" + + crontab -l | \ + sed -e "\\# [*] [*] [*] ${arkmanagerpath} --cronjob ${command} @${instance} #d" | \ + crontab - +} # # Print the status of the server (running? online? version?) @@ -1579,6 +2532,28 @@ printStatus(){ my $maxplayers = ord(substr($rest, 3, 1)); print "Server Name: $servername\n"; print "Players: $players / $maxplayers\n"; + send($socket, "\xff\xff\xff\xff\x55\xff\xff\xff\xff", 0, $sockaddr); + $data = ""; + recv($socket, $data, 1400, 0) or (print "Challenge request failed" and exit(1)); + if (ord(substr($data, 4, 1)) == 0x41) { + my $chal = substr($data, 5); + send($socket, "\xff\xff\xff\xff\x55" . $chal, 0, $sockaddr); + $data = ""; + recv($socket, $data, 1400, 0) or (print "A2S_PLAYERS request failed" and exit(1)); + } + ord(substr($data, 4, 1)) != 0x44 and (print ("A2S_PLAYERS Response: : " . unpack("H*", $data)) and exit(1)); + my $players = ord(substr($data, 5, 1)); + my $active = 0; + my $pdata = substr($data, 6); + for my $i (0 .. $players) { + my $idx = ord(substr($pdata, 0, 1)); + my ($name, $rest) = split(/\x00/, substr($pdata, 1), 2); + $pdata = substr($rest, 8); + if ($name ne "") { + $active = $active + 1; + } + } + print "Active Players: $active\n"; ' "$(getQueryPort)" "${ark_MultiHome:-127.0.0.1}" if isTheServerOnline; then @@ -1589,8 +2564,11 @@ printStatus(){ fi fi - getCurrentVersion - echo -e "$NORMAL" "Server version: " "$GREEN" $instver "$NORMAL" + instver="$(getCurrentVersion)" + echo -e "$NORMAL" "Server build ID: " "$GREEN" $instver "$NORMAL" + if false && [ -f "$arkserverroot/version.txt" ]; then + echo -e "$NORMAL" "Server version: " "$GREEN" "$(<"$arkserverroot/version.txt")" "$NORMAL" + fi } getAllInstanceNames(){ @@ -1632,12 +2610,29 @@ doListAllInstances(){ fi } +doPrintConfig(){ + declare -A vars + declare -A vals + for v in $(eval echo \$\{\!{a..z}\*\} \$\{\!{A..Z}\*\} \$\{\!_\*\}); do + vals["$v"]="${!v}" + done + for cfgfile in "$configfile" "${HOME}/${arkstUserCfgFile}" "${arkstGlobalCfgFile}"; do + while read v; do + val="$(source "$cfgfile"; echo "${!v}")" + if [[ "$val" = "${vals[$v]}" && -z "${vars[$v]}" ]]; then + vars["$v"]="$cfgfile" + echo "${cfgfile} => ${v}" + fi + done < <(sed -n 's/^[[:space:]]*\([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p' <"$cfgfile") + done +} + useConfig() { configfile= if [ -f "/etc/arkmanager/instances/${1}.cfg" ]; then configfile="/etc/arkmanager/instances/${1}.cfg" fi - if [ -f "${HOME}/.config/arkmanager/instances/${1}.cfg" ]; then + if [[ -f "${HOME}/.config/arkmanager/instances/${1}.cfg" && "${HOME}/.config/arkmanager/instances/${1}.cfg" -nt "/etc/arkmanager/instances/${1}.cfg" ]]; then configfile="${HOME}/.config/arkmanager/instances/${1}.cfg" fi for varname in "${!configfile_@}"; do @@ -1646,19 +2641,35 @@ useConfig() { break fi done - if [ -z "$configfile" ]; then - echo "Error: no config files for instance $1" - exit 1 + if [[ -z "$configfile" && -n "$arkSingleInstance" ]]; then + if [ -f "${HOME}/${arkstUserCfgFile}" ]; then + configfile="${HOME}/${arkstUserCfgFile}" + else + configfile="${arkstGlobalCfgFile}" + fi + else + if [ -z "$configfile" ]; then + echo "Error: no config files for instance $1" + exit 1 + fi + if [ ! -f "$configfile" ]; then + echo "Error: config file $configfile does not exist" + exit 1 + fi + source "$configfile" fi - if [ ! -f "$configfile" ]; then - echo "Error: config file $configfile does not exist" - exit 1 - fi - source "$configfile" if [ -z "$arkserverroot" ]; then echo "Error: arkserverroot not set" exit 1 fi + arkautorestartfile="${arkautorestartfile:-ShooterGame/Saved/.autorestart-${1}}" + arkoldautorestartfile="ShooterGame/Saved/.autorestart" + arkserverpidfile="${arkserverpidfile:-ShooterGame/Saved/.arkserver-${1}.pid}" + arkserveroldpidfile="ShooterGame/Saved/.arkserver.pid" + arkmanagerpidfile="${arkmanagerpidfile:-ShooterGame/Saved/.arkmanager-${1}.pid}" + arkwarnlockfile="${arkwarnlockfile:-ShooterGame/Saved/.ark-warn-${1}.lock}" + # This is linked to the directory, not to the instance + arkupdatelockfile="${arkupdatelockfile:-ShooterGame/Saved/.ark-update.lock}" } showUsage() { @@ -1687,6 +2698,8 @@ showUsage() { echo "installmod Installs a mod from the Steam workshop" echo "uninstallmod Removes the mod from the Mods directory" echo "reinstallmod Removes and re-installs a mod in the Mods directory" + echo "install-cronjob Adds a cron job using the specified command" + echo "remove-cronjob Removes a cron job that used the specified command" echo "restart Stops the server and then starts it" echo "run Runs the server without daemonizing" echo "start Starts the server" @@ -1710,212 +2723,286 @@ showUsage() { # Main program #--------------------- -# check the configuration and throw errors or warnings if needed -checkConfig +main(){ + # check the configuration and throw errors or warnings if needed + checkConfig -while true; do - options=( ) - allinstances=no - instances=( ) - args=( ) - command="$1" - shift - nrarg=0 + while true; do + options=( ) + allinstances=no + instances=( ) + args=( ) + command="$1" + shift + nrarg=0 - # get the number of arguments for commands that take arguments - case "$command" in - installmod) nrarg=1; ;; - uninstallmod) nrarg=1; ;; - reinstallmod) nrarg=1; ;; - broadcast) nrarg=1; ;; - rconcmd) nrarg=1; ;; - useconfig) nrarg=1; ;; - esac - - # Enumerate the options and arguments - while [ $# -ne 0 ]; do - case "$1" in - --) - shift - break - ;; - --args) - nrarg=$# - ;; + # Handle global options + case "$command" in --verbose) verbose=1 + continue ;; --dots) progressDisplayType=dots + continue ;; --spinner) progressDisplayType=spinner + continue ;; - --*) - options+=( "$1" ) + --cronjob) + inCronJob=true + continue ;; - @all) - allinstances=yes - ;; - @*) - instances+=( "${1#@}" ) - ;; - *) - if [ $nrarg -gt 0 ]; then - args+=( "$1" ) - (( nrarg-- )) - else + esac + + # get the number of arguments for commands that take arguments + case "$command" in + installmod) nrarg=1; ;; + uninstallmod) nrarg=1; ;; + reinstallmod) nrarg=1; ;; + broadcast) nrarg=1; ;; + rconcmd) nrarg=1; ;; + useconfig) nrarg=1; ;; + install-cronjob) nrarg=1; ;; + remove-cronjob) nrarg=1; ;; + remove-mods) nrarg=1; ;; + esac + + # Enumerate the options and arguments + while [ $# -ne 0 ]; do + case "$1" in + --) + shift break + ;; + --args) + nrarg=$# + ;; + --verbose) + verbose=1 + ;; + --dots) + progressDisplayType=dots + ;; + --spinner) + progressDisplayType=spinner + ;; + --*) + options+=( "$1" ) + ;; + @all) + allinstances=yes + ;; + @*) + instances+=( "${1#@}" ) + ;; + *) + if [ $nrarg -gt 0 ]; then + args+=( "$1" ) + (( nrarg-- )) + else + break + fi + ;; + esac + shift + done + + # handle non-instance separately + case "$command" in + upgrade-tools) + doUpgradeTools + exit + ;; + uninstall-tools) + doUninstallTools + exit + ;; + useconfig) + defaultinstance="${args[0]}" + continue + ;; + remove-mods) + doRemoveMods "${args[0]}" + if [ $# -eq 0 ]; then + exit 0 + else + continue fi ;; - esac - shift - done - - # handle non-instance separately - case "$command" in - upgrade-tools) - doUpgradeTools - exit - ;; - uninstall-tools) - doUninstallTools - exit - ;; - useconfig) - defaultinstance="${args[0]}" - continue - ;; - list-instances) - doListAllInstances "${options[@]}" - exit - ;; - --version) - echo "Version: ${arkstVersion}" - echo "Channel: ${arkstChannel}" - if [ -n "${arkstCommit}" ]; then - echo "Commit: ${arkstCommit:0:7}" - fi - exit 1 - ;; - -h|--help) - showUsage - exit 1 - ;; - "") - echo "arkmanager v${arkstVersion}: no command specified" - showUsage - exit 1 - ;; - esac - - # Handle no instances being specified - if [[ "${#instances[@]}" == 0 && "$allinstances" == "no" ]]; then - if [ -n "$defaultinstance" ]; then - instances=( "$defaultinstance" ) - else - echo "No instances supplied for command ${command} ${options[*]} ${args[*]}" - read -p "Do you wish to run this command for all instances?" -n 1 -r - echo - if [[ "$REPLY" =~ ^[Yy]$ ]]; then - allinstances=yes - else + list-instances) + doListAllInstances "${options[@]}" + exit + ;; + --version) + echo "Version: ${arkstVersion}" + echo "Channel: ${arkstChannel}" + if [ -n "${arkstCommit}" ]; then + echo "Commit: ${arkstCommit:0:7}" + fi + if [ -n "${arkstTag}" ]; then + echo "Release Tag: ${arkstTag}" + fi + blobsize="$(sed "s@^\\(arkst\\(Commit\\|Tag\\|RootUseEnv\\)\\)=.*@\\1=''@" "$0" | wc -c)" + echo "Blob SHA: $( (echo -ne "blob ${blobsize}\0"; sed "s@^\\(arkst\\(Commit\\|Tag\\|RootUseEnv\\)\\)=.*@\\1=''@" "$0") | sha1sum | cut -d' ' -f1)" exit 1 + ;; + -h|--help) + showUsage + exit 1 + ;; + "") + echo "arkmanager v${arkstVersion}: no command specified" + showUsage + exit 1 + ;; + esac + + # Handle no instances being specified + if [[ "${#instances[@]}" == 0 && "$allinstances" == "no" ]]; then + if [ -n "$defaultinstance" ]; then + instances=( "$defaultinstance" ) + else + echo "No instances supplied for command ${command} ${options[*]} ${args[*]}" + read -p "Do you wish to run this command for all instances?" -n 1 -r + echo + if [[ "$REPLY" =~ ^[Yy]$ ]]; then + allinstances=yes + else + exit 1 + fi fi fi - fi - - # Handle all instances being requested - if [[ "$allinstances" == "yes" ]]; then - instances=( $(getAllInstanceNames) ) - fi - - # Run the command for each instance requested - for instance in "${instances[@]}"; do - ( - echo "Running command '${command}' for instance '${instance}'" - useConfig "$instance" - checkConfig + # Handle cronjob commands specially case "$command" in - run) - doRun + install-cronjob) + doInstallCronJob "${args[@]}" "${options[@]}" "$@" + exit ;; - start) - doStart "${options[@]}" - ;; - stop) - doStop shutdown "${options[@]}" - ;; - restart) - doStop restart "${options[@]}" - echo "`timestamp`: stop" >> "$logdir/$arkmanagerLog" - ;; - install) - doInstall - ;; - update) - doUpdate "${options[@]}" - ;; - checkupdate) - checkForUpdate - ;; - installmod) - doInstallMod "${args[@]}" - ;; - uninstallmod) - doUninstallMod "${args[@]}" - ;; - reinstallmod) - doUninstallMod "${args[@]}" - doInstallMod "${args[@]}" - ;; - backup) - doBackup - ;; - broadcast) - doBroadcast "${args[@]}" - ;; - saveworld) - doSaveWorld - ;; - rconcmd) - rconcmd "${args[@]}" - ;; - status) - printStatus - ;; - *) - echo -n "arkmanager v${arkstVersion}: unknown command '$command' specified" - showUsage - exit 255 + remove-cronjob) + doRemoveCronJob "${args[@]}" + exit ;; esac - ) - laststatus=$? - if [ $laststatus -eq 255 ]; then - exit 1 - elif [ $laststatus -ne 0 ]; then - status=$laststatus - fi - done - # Perform the restart portion of the restart command - if [[ "$command" == "restart" ]]; then - sleep 1 + # Handle all instances being requested + if [[ "$allinstances" == "yes" ]]; then + instances=( $(getAllInstanceNames) ) + fi + + # Run the command for each instance requested for instance in "${instances[@]}"; do ( + echo "Running command '${command}' for instance '${instance}'" useConfig "$instance" - doStart "${options[@]}" - echo "`timestamp`: start" >> "$logdir/$arkmanagerLog" - echo "`timestamp`: restart" >> "$logdir/$arkmanagerLog" + checkConfig + + case "$command" in + run) + doRun + ;; + start) + doStart "${options[@]}" + ;; + stop) + doStop shutdown "${options[@]}" + ;; + restart) + doStop restart "${options[@]}" + echo "`timestamp`: stop" >> "$logdir/$arkmanagerLog" + ;; + cancelshutdown) + doCancelShutdown "${options[@]}" + ;; + install) + doInstall + ;; + update) + doUpdate "${options[@]}" + ;; + checkupdate) + checkForUpdate + ;; + installmod) + doInstallMod "${args[@]}" + ;; + enablemod) + doEnableMod "${args[@]}" + ;; + disablemod) + doDisableMod "${args[@]}" + ;; + installmods) + doInstallAllMods + ;; + uninstallmods) + doUninstallAllMods + ;; + uninstallmod) + doUninstallMod "${args[@]}" + ;; + reinstallmod) + doUninstallMod "${args[@]}" + doInstallMod "${args[@]}" + ;; + backup) + doBackup + ;; + broadcast) + doBroadcast "${args[@]}" + ;; + saveworld) + doSaveWorld + ;; + rconcmd) + rconcmd "${args[@]}" + ;; + printconfig) + doPrintConfig + ;; + status) + printStatus + ;; + *) + echo -n "arkmanager v${arkstVersion}: unknown command '$command' specified" + showUsage + exit 255 + ;; + esac ) + laststatus=$? + if [ $laststatus -eq 255 ]; then + exit 1 + elif [ $laststatus -ne 0 ]; then + status=$laststatus + fi done - fi - if [ $# -eq 0 ]; then - break - fi -done + # Perform the restart portion of the restart command + if [[ "$command" == "restart" ]]; then + sleep 1 + for instance in "${instances[@]}"; do + ( + echo "`timestamp`: restart" >> "$logdir/$arkmanagerLog" + useConfig "$instance" + doStart "${options[@]}" + ) + done + fi + + if [ $# -eq 0 ]; then + break + fi + done + + exit $status +} + +# Only execute main function if script is not being sourced +# by another script +if [[ "$(caller)" =~ ^0 ]]; then + main "$@" +fi -exit $status diff --git a/tools/arkmanager.cfg b/tools/arkmanager.cfg index b563ff2..34391a0 100644 --- a/tools/arkmanager.cfg +++ b/tools/arkmanager.cfg @@ -8,6 +8,7 @@ steamcmdroot="/home/steam/steamcmd" # path of yo steamcmdexec="steamcmd.sh" # name of steamcmd executable steamcmd_user="steam" # name of the system user who own steamcmd folder steamcmd_appinfocache="/home/steam/Steam/appcache/appinfo.vdf" # cache of the appinfo command +steamcmd_workshoplog="/home/steam/Steam/logs/workshop_log.txt" # Steam workshop log #steamlogin="anonymous" # Uncomment this to specify steam login instead of using anonymous login # config environment @@ -32,6 +33,10 @@ msgWarnRestartMinutes="This ARK server will shutdown for a restart in %d minutes msgWarnRestartSeconds="This ARK server will shutdown for a restart in %d seconds" msgWarnShutdownMinutes="This ARK server will shutdown in %d minutes" msgWarnShutdownSeconds="This ARK server will shutdown in %d seconds" +msgWarnCancelled="Restart cancelled by player request" + +# Restart cancel chat command +#chatCommandRestartCancel="/cancelupdate" # ARK server common options - use ark_= # comment out these values if you want to define them diff --git a/tools/install.sh b/tools/install.sh index cf801ed..eac2d60 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -3,6 +3,7 @@ userinstall=no steamcmd_user= showusage=no +migrateconfig=no while [ -n "$1" ]; do case "$1" in @@ -63,6 +64,9 @@ while [ -n "$1" ]; do DATADIR="$2" shift ;; + --migrate-config) + migrateconfig=yes + ;; -*) echo "Invalid option '$1'" showusage=yes @@ -169,7 +173,7 @@ if [ "$userinstall" == "yes" ]; then "${INSTALL_ROOT}${INSTANCEDIR}/instance.cfg.example" # Copy arkmanager.cfg to ~/.arkmanager.cfg.NEW - cp arkmanager.cfg "${INSTALL_ROOT}${CONFIGFILE}.NEW" + cp arkmanager.cfg "${INSTALL_ROOT}${CONFIGFILE}.example" # Change the defaults in the new config file sed -i -e "s|^steamcmd_user=\"steam\"|steamcmd_user=\"--me\"|" \ -e "s|\"/home/steam|\"${PREFIX}|" \ @@ -177,18 +181,24 @@ if [ "$userinstall" == "yes" ]; then -e "s|^install_bindir=.*|install_bindir=\"${BINDIR}\"|" \ -e "s|^install_libexecdir=.*|install_libexecdir=\"${LIBEXECDIR}\"|" \ -e "s|^install_datadir=.*|install_datadir=\"${DATADIR}\"|" \ - "${INSTALL_ROOT}${CONFIGFILE}.NEW" + "${INSTALL_ROOT}${CONFIGFILE}.example" # Copy arkmanager.cfg to ~/.arkmanager.cfg if it doesn't already exist if [ -f "${INSTALL_ROOT}${CONFIGFILE}" ]; then - bash ./migrate-config.sh "${INSTALL_ROOT}${CONFIGFILE}" - bash ./migrate-main-instance.sh "${INSTALL_ROOT}${CONFIGFILE}" "${INSTALL_ROOT}${INSTANCEDIR}/main.cfg" + SUFFIX= + if [ "$migrateconfig" = "no" ]; then + SUFFIX=".NEW" + cp "${INSTALL_ROOT}${CONFIGFILE}" "${INSTALL_ROOT}${CONFIGFILE}${SUFFIX}" + fi + + bash ./migrate-config.sh "${INSTALL_ROOT}${CONFIGFILE}${SUFFIX}" + bash ./migrate-main-instance.sh "${INSTALL_ROOT}${CONFIGFILE}${SUFFIX}" "${INSTALL_ROOT}${INSTANCEDIR}/main.cfg${SUFFIX}" echo "A previous version of ARK Server Tools was detected in your system, your old configuration was not overwritten. You may need to manually update it." echo "A copy of the new configuration file was included in '${CONFIGFILE}.NEW'. Make sure to review any changes and update your config accordingly!" exit 2 else - mv -n "${INSTALL_ROOT}${CONFIGFILE}.NEW" "${INSTALL_ROOT}${CONFIGFILE}" + cp -n "${INSTALL_ROOT}${CONFIGFILE}.example" "${INSTALL_ROOT}${CONFIGFILE}" cp -n "${INSTALL_ROOT}/${INSTANCEDIR}/instance.cfg.example" "${INSTALL_ROOT}/${INSTANCEDIR}/main.cfg" fi else @@ -301,24 +311,30 @@ else # Copy arkmanager.cfg inside linux configuation folder if it doesn't already exists mkdir -p "${INSTALL_ROOT}/etc/arkmanager" chown "$steamcmd_user" "${INSTALL_ROOT}/etc/arkmanager" - cp arkmanager.cfg "${INSTALL_ROOT}${CONFIGFILE}.NEW" - chown "$steamcmd_user" "${INSTALL_ROOT}${CONFIGFILE}.NEW" + cp arkmanager.cfg "${INSTALL_ROOT}${CONFIGFILE}.example" + chown "$steamcmd_user" "${INSTALL_ROOT}${CONFIGFILE}.example" sed -i -e "s|^steamcmd_user=\"steam\"|steamcmd_user=\"$steamcmd_user\"|" \ -e "s|\"/home/steam|\"/home/$steamcmd_user|" \ -e "s|^install_bindir=.*|install_bindir=\"${BINDIR}\"|" \ -e "s|^install_libexecdir=.*|install_libexecdir=\"${LIBEXECDIR}\"|" \ -e "s|^install_datadir=.*|install_datadir=\"${DATADIR}\"|" \ - "${INSTALL_ROOT}${CONFIGFILE}.NEW" + "${INSTALL_ROOT}${CONFIGFILE}.example" if [ -f "${INSTALL_ROOT}${CONFIGFILE}" ]; then - bash ./migrate-config.sh "${INSTALL_ROOT}${CONFIGFILE}" - bash ./migrate-main-instance.sh "${INSTALL_ROOT}${CONFIGFILE}" "${INSTALL_ROOT}${INSTANCEDIR}/main.cfg" + SUFFIX= + if [ "$migrateconfig" = "no" ]; then + SUFFIX=".NEW" + cp "${INSTALL_ROOT}${CONFIGFILE}" "${INSTALL_ROOT}${CONFIGFILE}${SUFFIX}" + fi + + bash ./migrate-config.sh "${INSTALL_ROOT}${CONFIGFILE}${SUFFIX}" + bash ./migrate-main-instance.sh "${INSTALL_ROOT}${CONFIGFILE}${SUFFIX}" "${INSTALL_ROOT}${INSTANCEDIR}/main.cfg${SUFFIX}" echo "A previous version of ARK Server Tools was detected in your system, your old configuration was not overwritten. You may need to manually update it." echo "A copy of the new configuration file was included in /etc/arkmanager. Make sure to review any changes and update your config accordingly!" exit 2 else - mv -n "${INSTALL_ROOT}${CONFIGFILE}.NEW" "${INSTALL_ROOT}${CONFIGFILE}" + cp -n "${INSTALL_ROOT}${CONFIGFILE}.example" "${INSTALL_ROOT}${CONFIGFILE}" cp -n "${INSTALL_ROOT}/${INSTANCEDIR}/instance.cfg.example" "${INSTALL_ROOT}/${INSTANCEDIR}/main.cfg" fi fi diff --git a/tools/migrate-main-instance.sh b/tools/migrate-main-instance.sh index 79cdfd6..4d32a84 100755 --- a/tools/migrate-main-instance.sh +++ b/tools/migrate-main-instance.sh @@ -3,7 +3,7 @@ configfile="$1" instancefile="$2" -if grep "^arkserverroot=" <"$configfile" >/dev/null 2>&1 && [ ! -f "$instancefile" ]; then +if ! grep '^arkSingleInstance=' <"$configfile" >/dev/null 2>&1 && grep "^arkserverroot=" <"$configfile" >/dev/null 2>&1 && [ ! -f "$instancefile" ]; then sed -n '/^#*\(ark\(\|flag\|opt\)_[^=]*\|arkserverroot\|serverMap\(\|ModId\)\)=/p' <"$configfile" >"$instancefile" sed -i '/^ark\(serverroot\|_\(RCONPort\|Port\|QueryPort\)\)=/d' "$configfile" echo 'defaultinstance="main"' >>"$configfile" diff --git a/tools/systemd/arkmanager.init b/tools/systemd/arkmanager.init index fa760d2..c63be0c 100755 --- a/tools/systemd/arkmanager.init +++ b/tools/systemd/arkmanager.init @@ -1,6 +1,6 @@ #!/bin/bash -DAEMON=/usr/bin/arkmanager +DAEMON="/usr/bin/arkmanager" for service in $(${DAEMON} list-instances --brief); do case "$1" in diff --git a/tools/systemd/arkmanager.service b/tools/systemd/arkmanager.service index c2d4053..55ab37d 100644 --- a/tools/systemd/arkmanager.service +++ b/tools/systemd/arkmanager.service @@ -5,7 +5,8 @@ After=network.target [Service] ExecStart=/usr/libexec/arkmanager/arkmanager.init start ExecStop=/usr/libexec/arkmanager/arkmanager.init stop -Type=oneshot +Type=forking +RemainAfterExit=yes [Install] WantedBy=multi-user.target