Archived
add upgrade command; reinstall keeps the same version
- terractl upgrade <name> lists the zips an instance can use; with a zip or 'latest' it checks the zip, stops the server, backs up worlds and configs to instances/.backups/, pins ZIP=, unpacks right away and starts the server again. Downgrades need --downgrade. - reinstall pins the installed zip when ZIP= is blank, so it never changes version, and keeps server/.installed (uses a .reinstall flag). - Resolve terractl's own path before cd /, so a relative $0 works. - Docs, completion and the daemon's command list; version 0.0.12. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ef63609db9
commit
96f7876729
7 files changed
+325
-31
No files matched your search
+216
-7
@@ -19,6 +19,7 @@ set -euo pipefail
|
||||
# user may not be allowed into, e.g. /root). Paths given on the command line are
|
||||
# resolved against where terractl was called from, via abspath().
|
||||
CALLER_PWD=$PWD
|
||||
SELF=$(readlink -f "$0") # before the cd, in case $0 is a relative path
|
||||
cd /
|
||||
|
||||
abspath() {
|
||||
@@ -77,7 +78,10 @@ Admin commands:
|
||||
attach <name> interactive console (Ctrl-D to detach)
|
||||
watch <name> [-n <lines>] [-q] follow the server's output, read-only (Ctrl-C to quit)
|
||||
cmd <name> <console command...> send a console command (e.g. save, say hi)
|
||||
reinstall <name> [--clean] re-unpack server on next start
|
||||
upgrade <name> show the server zips it can use, and which it runs
|
||||
upgrade <name> <zip>|latest [--downgrade] [--no-backup]
|
||||
change its server version now (backs up worlds first)
|
||||
reinstall <name> [--clean] re-unpack the same server version on next start
|
||||
(--clean also re-downloads tModLoader's .NET)
|
||||
fix-perms <name> give all of an instance's files back to the
|
||||
'$SVC_USER' user, with the usual modes (run as root)
|
||||
@@ -140,6 +144,7 @@ write_instance_conf() {
|
||||
cat > "$dir/instance.conf" <<EOF
|
||||
# Settings for terraria@$name
|
||||
# Changing TYPE or ZIP makes the next start re-unpack server/ (data/ is kept).
|
||||
# To change version, use: terractl upgrade $name <zip>
|
||||
|
||||
TYPE=$type # vanilla | tmodloader
|
||||
|
||||
@@ -221,8 +226,9 @@ install_server() {
|
||||
dir=$(inst_dir "$name")
|
||||
marker="$dir/server/.installed"
|
||||
|
||||
# Already installed with the right type (and pinned zip, if any)? Nothing to do.
|
||||
if [[ -f $marker ]]; then
|
||||
# Already installed with the right type (and pinned zip, if any), and not
|
||||
# asked to unpack it again ('reinstall')? Nothing to do.
|
||||
if [[ -f $marker && ! -e $dir/server/.reinstall ]]; then
|
||||
read -r have_type have_zip < "$marker" || true
|
||||
if [[ $have_type == "$TYPE" ]] && [[ -z $ZIP || $have_zip == "$(basename "$ZIP")" ]]; then
|
||||
return 0
|
||||
@@ -1101,8 +1107,49 @@ cmd_list() {
|
||||
zip_warning
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------ versions ----
|
||||
# The zip an instance runs is recorded in server/.installed when it's unpacked.
|
||||
# A pin (ZIP= in instance.conf) that differs from it makes the next start (the
|
||||
# 'prepare' step) unpack the pinned zip instead.
|
||||
|
||||
zip_pattern() {
|
||||
case $TYPE in
|
||||
vanilla) echo 'terraria-server-*.zip' ;;
|
||||
tmodloader) echo 'tModLoader*.zip' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# The zips in dist/ for this instance's TYPE, oldest first
|
||||
type_zips() {
|
||||
find "$DIST_DIR" -maxdepth 1 -type f -name "$(zip_pattern)" -printf '%f\n' | sort -V
|
||||
}
|
||||
|
||||
# The zip server/ was unpacked from (file name only), or nothing
|
||||
installed_zip() {
|
||||
local marker type="" zip=""
|
||||
marker="$(inst_dir "$1")/server/.installed"
|
||||
if [[ -r $marker ]]; then read -r type zip < "$marker" || true; fi
|
||||
echo "$zip"
|
||||
}
|
||||
|
||||
# instance.conf is sourced by bash, so only plain file names go into it
|
||||
safe_zip_name() { [[ $1 =~ ^[A-Za-z0-9._+-]+\.zip$ ]]; }
|
||||
|
||||
set_zip_pin() {
|
||||
local conf; conf="$(inst_dir "$1")/instance.conf"
|
||||
if grep -q '^ZIP=' "$conf"; then
|
||||
sed -i "s|^ZIP=.*|ZIP=$2|" "$conf"
|
||||
else
|
||||
printf 'ZIP=%s\n' "$2" >> "$conf"
|
||||
fi
|
||||
fix_owner "$conf"
|
||||
}
|
||||
|
||||
# Unpacks the same zip again on the next start. An unpinned instance is pinned
|
||||
# to the zip it runs first, as otherwise it would pick the newest zip in dist/
|
||||
# and so change version; changing version is what 'upgrade' is for.
|
||||
cmd_reinstall() {
|
||||
local name=$1 clean=0 dir; dir=$(inst_dir "$name")
|
||||
local name=$1 clean=0 dir installed; dir=$(inst_dir "$name")
|
||||
shift
|
||||
case ${1:-} in
|
||||
--clean) clean=1 ;;
|
||||
@@ -1113,7 +1160,19 @@ cmd_reinstall() {
|
||||
if [[ $clean -eq 1 ]] && instance_running "$name"; then
|
||||
die "'$name' is running; --clean would delete .NET under it. Stop it first: $(hint group stop "$name")"
|
||||
fi
|
||||
rm -f "$dir/server/.installed"
|
||||
load_conf "$name"
|
||||
installed=$(installed_zip "$name")
|
||||
if [[ -z $ZIP && -n $installed ]]; then
|
||||
[[ -f $DIST_DIR/$installed ]] ||
|
||||
die "'$name' runs $installed, which is no longer in $DIST_DIR. Put it back to unpack it again, or change version: $(hint root upgrade "$name")"
|
||||
safe_zip_name "$installed" || die "can't pin '$installed' in instance.conf; rename it in $DIST_DIR and use: $(hint root upgrade "$name" "<zip>")"
|
||||
set_zip_pin "$name" "$installed"
|
||||
log "pinned ZIP=$installed in instance.conf, so the same version is unpacked again"
|
||||
elif [[ -n $ZIP && -n $installed && $(basename "$ZIP") != "$installed" ]]; then
|
||||
log "note: instance.conf pins ZIP=$ZIP, but $installed is installed; the next start unpacks $(basename "$ZIP")"
|
||||
fi
|
||||
# Keeps .installed, so what it runs is still known (e.g. to 'upgrade')
|
||||
if [[ -d $dir/server ]]; then touch "$dir/server/.reinstall"; fix_owner "$dir/server/.reinstall"; fi
|
||||
if [[ $clean -eq 1 ]]; then
|
||||
rm -rf "$dir/server/dotnet" "$dir/server/tModLoader-Logs"
|
||||
echo "removed the downloaded .NET runtime and logs; .NET will be downloaded again"
|
||||
@@ -1121,6 +1180,155 @@ cmd_reinstall() {
|
||||
echo "server/ will be re-unpacked on next start: $(hint group restart "$name")"
|
||||
}
|
||||
|
||||
upgrade_list() {
|
||||
local z joined newest=""
|
||||
local -a zips=() note=()
|
||||
mapfile -t zips < <(type_zips)
|
||||
if [[ ${#zips[@]} -gt 0 ]]; then newest=${zips[-1]}; fi
|
||||
echo "'$name' ($TYPE) runs: ${installed:-nothing yet (unpacked on its first start)}"
|
||||
if [[ ${#zips[@]} -eq 0 ]]; then
|
||||
echo "No '$(zip_pattern)' zips in $DIST_DIR"
|
||||
return 0
|
||||
fi
|
||||
printf '%-40s %s\n' ZIP NOTE
|
||||
for z in "${zips[@]}"; do
|
||||
note=()
|
||||
if [[ $z == "$installed" ]]; then note+=(installed); fi
|
||||
if [[ -n $ZIP && $z == "${ZIP##*/}" ]]; then note+=(pinned); fi
|
||||
if [[ $z == "$newest" ]]; then note+=(newest); fi
|
||||
if ! svc_can_read "$DIST_DIR/$z"; then note+=("unreadable by $SVC_USER"); fi
|
||||
joined=$(printf '%s, ' "${note[@]}")
|
||||
printf '%-40s %s\n' "$z" "${joined%, }" | sed 's/ *$//'
|
||||
done
|
||||
if [[ -n $installed && ! -f $DIST_DIR/$installed ]]; then
|
||||
printf '%-40s %s\n' "$installed" "installed, no longer in $DIST_DIR"
|
||||
fi
|
||||
echo "Change version with: $(hint root upgrade "$name" "<zip>|latest")"
|
||||
}
|
||||
|
||||
# Changes the server version (zip) of an instance, up or down, and applies it
|
||||
# right away: checks the zip, stops the server, backs up its worlds and
|
||||
# configs, pins ZIP=, unpacks the zip and starts the server again. Anything
|
||||
# that fails before the new server/ is in place leaves the instance as it was.
|
||||
cmd_upgrade() {
|
||||
local name=$1; shift
|
||||
local target="" downgrade=0 backup=1 arg dir installed zip f listing lower verb state
|
||||
local was_running=0 installed_name old_pin backup_file="" unit="terraria@$name"
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--downgrade) downgrade=1 ;;
|
||||
--no-backup) backup=0 ;;
|
||||
-*) die "unknown option '$arg'" ;;
|
||||
*) [[ -z $target ]] || die "more than one zip given"; target=$arg ;;
|
||||
esac
|
||||
done
|
||||
dir=$(inst_dir "$name")
|
||||
[[ -f $dir/instance.conf ]] || die "no instance '$name'"
|
||||
load_conf "$name"
|
||||
installed=$(installed_zip "$name")
|
||||
if [[ -z $target ]]; then upgrade_list; return 0; fi
|
||||
[[ $EUID -eq 0 ]] || die "changing version needs root: sudo terractl upgrade $name $*"
|
||||
|
||||
# Check the zip before changing anything
|
||||
if [[ $target == latest ]]; then
|
||||
zip=$(type_zips | tail -n 1)
|
||||
[[ -n $zip ]] || die "no '$(zip_pattern)' zips in $DIST_DIR"
|
||||
elif [[ $target == */* ]]; then
|
||||
[[ $(dirname "$(abspath "$target")") -ef $DIST_DIR ]] ||
|
||||
die "$target isn't in $DIST_DIR; copy it there first"
|
||||
zip=$(basename "$target")
|
||||
else
|
||||
zip=$target
|
||||
fi
|
||||
# shellcheck disable=SC2053 # the pattern is a glob on purpose
|
||||
[[ $zip == $(zip_pattern) ]] || die "$zip isn't a $TYPE zip (expected $(zip_pattern))"
|
||||
safe_zip_name "$zip" || die "rename $zip to use only letters, digits and ._+- (it's written into instance.conf)"
|
||||
f="$DIST_DIR/$zip"
|
||||
[[ -f $f ]] || die "$f not found. Available: $(hint group upgrade "$name")"
|
||||
svc_can_read "$f" || die "the '$SVC_USER' user can't read $f; fix it with: sudo chmod 644 $f"
|
||||
|
||||
if [[ $zip == "$installed" ]]; then
|
||||
if [[ ${ZIP##*/} != "$zip" ]]; then set_zip_pin "$name" "$zip"; log "pinned ZIP=$zip in instance.conf"; fi
|
||||
echo "'$name' already runs $zip"
|
||||
return 0
|
||||
fi
|
||||
if [[ -n $installed ]]; then
|
||||
lower=$(printf '%s\n' "$installed" "$zip" | sort -V | head -n 1)
|
||||
if [[ $lower == "$zip" ]]; then
|
||||
verb=downgraded
|
||||
if [[ $downgrade -eq 0 ]]; then
|
||||
die "$zip is older than $installed, which '$name' runs now. Worlds saved by a newer version" \
|
||||
"can't be opened by an older one, and mods may not load. To go ahead anyway: $(hint root upgrade "$name" "$zip" --downgrade)"
|
||||
fi
|
||||
else
|
||||
verb=upgraded
|
||||
fi
|
||||
else
|
||||
verb=switched
|
||||
fi
|
||||
|
||||
command -v unzip >/dev/null || die "unzip is not installed"
|
||||
listing=$(unzip -Z1 "$f" 2>/dev/null) || die "$f isn't a readable zip (damaged, or not fully downloaded?)"
|
||||
case $TYPE in
|
||||
vanilla) grep -q '/Linux/TerrariaServer\.bin\.x86_64$' <<< "$listing" ||
|
||||
die "$f has no Linux/TerrariaServer.bin.x86_64; is it the dedicated server zip?" ;;
|
||||
tmodloader) grep -qE '(^|/)start-tModLoaderServer\.sh$' <<< "$listing" ||
|
||||
die "$f has no start-tModLoaderServer.sh; is it a tModLoader release zip?" ;;
|
||||
esac
|
||||
log "checking $zip..."
|
||||
unzip -tqq "$f" >/dev/null 2>&1 || die "$f is damaged (unzip -t found errors); download it again"
|
||||
|
||||
state=$(systemctl is-active "$unit" 2>/dev/null || true)
|
||||
if [[ $state == inactive || $state == failed ]] && instance_running "$name"; then
|
||||
die "'$name' is running outside systemd (e.g. 'terractl run' in another terminal); stop it first"
|
||||
fi
|
||||
if [[ $state == active || $state == activating || $state == reloading ]]; then
|
||||
was_running=1
|
||||
echo "stopping $unit (saves the world first)..."
|
||||
systemctl stop "$unit"
|
||||
fi
|
||||
|
||||
if [[ $backup -eq 1 ]]; then
|
||||
mkdir -p "$BASE_DIR/.backups"
|
||||
chmod 700 "$BASE_DIR/.backups"
|
||||
installed_name=${installed%.zip}
|
||||
backup_file="$BASE_DIR/.backups/$name-$(date +%Y%m%d-%H%M%S)-${installed_name:-new}"
|
||||
while [[ -e $backup_file.tar.gz ]]; do backup_file+=_; done
|
||||
backup_file+=.tar.gz
|
||||
# In a subshell, so a failure comes back here instead of exiting
|
||||
if ! ( trap cleanup EXIT; cmd_export "$name" "$backup_file" ) >/dev/null; then
|
||||
if [[ $was_running -eq 1 ]]; then systemctl start "$unit"; fi
|
||||
die "backing up '$name' failed, so its version wasn't changed"
|
||||
fi
|
||||
echo "backup of worlds and configs: $backup_file"
|
||||
fi
|
||||
|
||||
old_pin=$ZIP
|
||||
set_zip_pin "$name" "$zip"
|
||||
# Unpack now, as the service would, so any problem shows up here. Until the
|
||||
# new server/ is complete, the old one is left in place.
|
||||
if ! runuser -u "$SVC_USER" -- env HOME="$dir" "$SELF" prepare "$name"; then
|
||||
set_zip_pin "$name" "$old_pin"
|
||||
if [[ $was_running -eq 1 ]]; then systemctl start "$unit"; fi
|
||||
die "unpacking $zip failed (see above); '$name' still runs ${installed:-its old version}"
|
||||
fi
|
||||
|
||||
echo "$verb '$name' from ${installed:-nothing} to $zip"
|
||||
if [[ $was_running -eq 1 ]]; then
|
||||
systemctl start "$unit"
|
||||
echo "$unit started again"
|
||||
else
|
||||
echo "Start it with: $(hint group start "$name")"
|
||||
fi
|
||||
echo "Players need the version in $zip to join."
|
||||
if [[ $TYPE == tmodloader ]]; then
|
||||
echo "Check that its mods still load: $(hint group watch "$name")"
|
||||
fi
|
||||
if [[ -n $backup_file ]]; then
|
||||
echo "If its worlds need to go back to $installed, see 'Undoing an upgrade' in the guide."
|
||||
fi
|
||||
}
|
||||
|
||||
# Gives every file of an instance back to the service user, with the modes the
|
||||
# service creates files with (UMask=0027): the terraria group can read but not
|
||||
# change them, others get nothing. For files copied in as root, or with odd
|
||||
@@ -1184,7 +1392,7 @@ cmd_info() {
|
||||
# anything it does on this host, and passes on where it was called from.
|
||||
run_remote_helper() {
|
||||
local helper=$REMOTE_HELPER ver=$TERRACTL_VERSION self
|
||||
self=$(readlink -f "$0")
|
||||
self=$SELF
|
||||
# A development copy uses the helper from its source tree
|
||||
if [[ $helper == *@* ]]; then helper="$(dirname "$self")/../lib/terractl-remote"; fi
|
||||
if [[ $ver == *@* ]]; then ver=development; fi
|
||||
@@ -1218,7 +1426,7 @@ if [[ $command == prepare || $command == run ]]; then
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
command -v runuser >/dev/null || die "runuser not found; run as '$SVC_USER' instead"
|
||||
exec runuser -u "$SVC_USER" -- env HOME="$(inst_dir "$1")" \
|
||||
"$(readlink -f "$0")" "$command" "$@"
|
||||
"$SELF" "$command" "$@"
|
||||
elif [[ $(id -un) != "$SVC_USER" ]]; then
|
||||
# Being in the group isn't enough: files would end up owned by the caller
|
||||
die "'$command' needs root (it switches to the '$SVC_USER' user by itself): sudo terractl $command $1"
|
||||
@@ -1242,6 +1450,7 @@ case $command in
|
||||
mods) cmd_mods "$@" ;;
|
||||
new) cmd_new "$@" ;;
|
||||
reinstall) cmd_reinstall "$@" ;;
|
||||
upgrade) cmd_upgrade "$@" ;;
|
||||
fix-perms) cmd_fix_perms "$@" ;;
|
||||
delete) cmd_delete "$@" ;;
|
||||
rename) cmd_rename "$@" ;;
|
||||
|
||||
Reference in new issue
Block a user