This commit is contained in:
abrown38 committed 2026-09-30 17:11:22 +00:00
1 parent 4f0886b6cd
commit 02017d470f
16 files changed
+2665 -84

No files matched your search

+18
View File
@@ -6,6 +6,24 @@
- New `/etc/systemd/journald@terraria.conf` for the servers' journal size and retention (default: up to 256 MB).
- New internal `terractl journal-access`, run as root before each server start: creates the servers' journal folder (never `/var/log/journal` itself) and gives the `terraria` group read access, repairing existing journal files too.
- The note shown by `attach` and `cmd` when output can't be read now distinguishes "not in the `terraria` group" from "in it, but the journal isn't set up yet".
- New polkit rule (`/usr/share/polkit-1/rules.d/50-terractl.rules`): members of the `terraria` group can `systemctl start`, `stop` and `restart` `terraria@<name>` without `sudo`. `enable`, `disable` and all other units still need root. Needs `polkitd` 0.106 or newer, now a recommended package; Ubuntu 22.04's polkit can't run the rule.
- New `enable` and `disable` commands: `terractl enable <name> [--now]` makes a server start at boot (`--now` also starts it), `terractl disable <name> [--now]` stops that (`--now` also stops it, saving the world). They need root, like `systemctl enable`. `enable` refuses names without an instance, so a typo can't create a new instance at boot. `new` and `import` now suggest `terractl enable <name> --now`.
- `list` has a new `BOOT` column showing whether each server starts at boot (`enabled`, `disabled`, or e.g. `masked`).
- Manage other hosts, like LXD/Incus remotes:
- `sudo terractl daemon enable` starts `terractld.service`, a daemon other hosts connect to (HTTPS, TCP port 7776; `TERRACTL_LISTEN` in `/etc/default/terractl` changes it). It's off until enabled. `daemon disable` and `daemon status` turn it off and show its state.
- `sudo terractl daemon trust add <name>` creates a one-time token for a computer, valid for 24 hours; `daemon trust list` and `daemon trust remove <name>` show and revoke access. On that computer, `terractl remote add <remote> <token>` adds the host; `remote list` and `remote remove` manage remotes, which are stored per user.
- `terractl --host <remote> <command>` runs any command on a remote, including `attach`, `watch` and `export`/`import`/`mods add` with files from your computer. The daemon runs it as root there, with messages adjusted to `terractl --host <remote> ...`.
- `terractl copy` and `terractl move` copy, move or rename instances, on one host or between hosts (`[<remote>:]<name>`). Paths and ports follow the destination's settings, and a missing server zip is copied along. `move` stops the source first, starts it again if the transfer fails, and starts the moved instance where it was running before.
- Security: the token carries the daemon's certificate fingerprint, which the client checks on every connection; each computer gets its own key, and the daemon stores only hashes. The daemon runs as root with systemd sandboxing.
- Written in Python (standard library only), in `/usr/lib/terraria-server-manager/terractl-remote`. The package now depends on `python3` and `openssl`.
- New `start`, `stop` and `restart` commands, the same as `systemctl start|stop|restart terraria@<name>`, for this host and for `--host`. `start` refuses names that have no instance. The service's own stop step is now `terractl service-stop`; units that still call `terractl stop <name> <pid>` keep working.
- New `rename` command: `terractl rename <name> <new name>` renames the folder, updates `serverconfig.txt`, and carries over the service (stopped and started again if it was running, and still starting at boot). The port and unpacked server are kept. `move` within one host uses it.
- New `info` command: this host's folders, zips and instances, or an instance's type, port, server zip and state, as `key=value` lines.
- `import` replaces a `ZIP=` pin that's an absolute path not present on this host with the zip of the same name in this host's `dist/`.
- Messages that suggest a command now suggest `terractl` commands (`terractl stop <name>` rather than `systemctl stop terraria@<name>`), with `sudo` only when it's needed: not in a root shell, and not for `terraria` group members starting, stopping or restarting a server.
- New `watch` command: `terractl watch <name> [-n <lines>] [-q]` follows the server's output like `attach`, but never sends input, so it's safe to leave open. Works for the `terraria` group without `sudo`, and while the server is stopped. `-q` leaves out the lines about what it's doing.
- Bash completion for the new commands, remote names after `--host`, and `<remote>:<name>` for `copy` and `move`.
- Clearer messages when `run` or `prepare` is typed without `sudo` (the old one, "must run as root or 'terraria'", read as if the `terraria` group were enough), and when `stop` is typed by hand.
- Upgrading: running servers keep logging to the system journal until restarted; the package lists the ones to restart.
## 0.0.10 - 2026-09-30
+33 -7
View File
@@ -17,10 +17,18 @@ SYSUSERSDIR := $(PREFIX)/lib/sysusers.d
TMPFILESDIR := $(PREFIX)/lib/tmpfiles.d
DOCDIR := $(PREFIX)/share/doc/$(NAME)
DATADIR := $(PREFIX)/share/$(NAME)
PKGLIBDIR := $(PREFIX)/lib/$(NAME)
COMPLETIONDIR := $(PREFIX)/share/bash-completion/completions
# polkit only reads /usr/share/polkit-1/rules.d and /etc/polkit-1/rules.d
ifeq ($(PREFIX),/usr)
POLKITDIR ?= /usr/share/polkit-1/rules.d
else
POLKITDIR ?= $(SYSCONFDIR)/polkit-1/rules.d
endif
# Fill in version and install paths
SUBST = sed -e 's|@VERSION@|$(VERSION)|g' -e 's|@BINDIR@|$(BINDIR)|g' -e 's|@DOCDIR@|$(DOCDIR)|g'
SUBST = sed -e 's|@VERSION@|$(VERSION)|g' -e 's|@BINDIR@|$(BINDIR)|g' -e 's|@DOCDIR@|$(DOCDIR)|g' \
-e 's|@PKGLIBDIR@|$(PKGLIBDIR)|g'
MAINT_SCRIPTS := packaging/deb/postinst packaging/deb/prerm packaging/deb/postrm
STAGE := build/deb-root
@@ -34,22 +42,33 @@ check:
bash -n bin/terractl
bash -n completions/bash/terractl
for f in $(MAINT_SCRIPTS); do sh -n $$f; done
python3 -c 'import ast, sys; ast.parse(open(sys.argv[1]).read(), sys.argv[1])' lib/terractl-remote
@if command -v shellcheck >/dev/null; then \
shellcheck bin/terractl completions/bash/terractl $(MAINT_SCRIPTS); \
else \
echo "shellcheck not installed; skipped (apt install shellcheck)"; \
fi
@if python3 -m pyflakes --version >/dev/null 2>&1; then \
python3 -m pyflakes lib/terractl-remote; \
else \
echo "pyflakes not installed; skipped (apt install python3-pyflakes)"; \
fi
install:
install -d $(DESTDIR)$(BINDIR) $(DESTDIR)$(UNITDIR) $(DESTDIR)$(SYSUSERSDIR) \
$(DESTDIR)$(TMPFILESDIR) $(DESTDIR)$(DOCDIR) $(DESTDIR)$(DATADIR) \
$(DESTDIR)$(COMPLETIONDIR) $(DESTDIR)$(SYSCONFDIR)/default $(DESTDIR)$(SYSCONFDIR)/systemd
$(DESTDIR)$(TMPFILESDIR) $(DESTDIR)$(DOCDIR) $(DESTDIR)$(DATADIR) $(DESTDIR)$(PKGLIBDIR) \
$(DESTDIR)$(COMPLETIONDIR) $(DESTDIR)$(SYSCONFDIR)/default $(DESTDIR)$(SYSCONFDIR)/systemd \
$(DESTDIR)$(POLKITDIR)
$(SUBST) bin/terractl > $(DESTDIR)$(BINDIR)/terractl
chmod 755 $(DESTDIR)$(BINDIR)/terractl
install -m 755 lib/terractl-remote $(DESTDIR)$(PKGLIBDIR)/terractl-remote
$(SUBST) systemd/terraria@.service > $(DESTDIR)$(UNITDIR)/terraria@.service
$(SUBST) systemd/terractld.service > $(DESTDIR)$(UNITDIR)/terractld.service
chmod 644 $(DESTDIR)$(UNITDIR)/terraria@.service $(DESTDIR)$(UNITDIR)/terractld.service
install -m 644 systemd/terraria@.socket $(DESTDIR)$(UNITDIR)/
install -m 644 config/sysusers.d/terractl.conf $(DESTDIR)$(SYSUSERSDIR)/
install -m 644 config/tmpfiles.d/terractl.conf $(DESTDIR)$(TMPFILESDIR)/
install -m 644 config/polkit/terractl.rules $(DESTDIR)$(POLKITDIR)/50-terractl.rules
install -m 644 docs/guide.md $(DESTDIR)$(DOCDIR)/README.md
install -m 644 tools/windows/export-tmod-mods.ps1 $(DESTDIR)$(DATADIR)/
install -m 644 completions/bash/terractl $(DESTDIR)$(COMPLETIONDIR)/terractl
@@ -68,20 +87,27 @@ install:
@if [ -z "$(DESTDIR)" ]; then \
systemd-sysusers terractl.conf && \
systemd-tmpfiles --create terractl.conf && \
if [ -d /run/systemd/system ]; then systemctl daemon-reload; fi && \
if [ -d /run/systemd/system ]; then \
systemctl daemon-reload && systemctl try-restart terractld.service; \
fi && \
$(BINDIR)/terractl journal-access && \
echo "Installed terractl $(VERSION). See $(DOCDIR)/README.md"; \
fi
uninstall:
@if [ -z "$(DESTDIR)" ] && [ -d /run/systemd/system ]; then \
systemctl disable --now terractld.service 2>/dev/null || true; \
fi
rm -f $(DESTDIR)$(BINDIR)/terractl \
$(DESTDIR)$(UNITDIR)/terraria@.service $(DESTDIR)$(UNITDIR)/terraria@.socket \
$(DESTDIR)$(UNITDIR)/terractld.service \
$(DESTDIR)$(SYSUSERSDIR)/terractl.conf $(DESTDIR)$(TMPFILESDIR)/terractl.conf \
$(DESTDIR)$(COMPLETIONDIR)/terractl
rm -rf $(DESTDIR)$(DOCDIR) $(DESTDIR)$(DATADIR)
$(DESTDIR)$(COMPLETIONDIR)/terractl $(DESTDIR)$(POLKITDIR)/50-terractl.rules
rm -rf $(DESTDIR)$(DOCDIR) $(DESTDIR)$(DATADIR) $(DESTDIR)$(PKGLIBDIR)
@if [ -z "$(DESTDIR)" ] && [ -d /run/systemd/system ]; then systemctl daemon-reload; fi
@echo "Removed. Kept: $(SYSCONFDIR)/default/terractl, $(SYSCONFDIR)/systemd/journald@terraria.conf,"
@echo "/var/lib/terraria, the servers' journal and the terraria user."
@echo "/var/lib/terraria, /var/lib/terractl (the daemon's certificate and trusted"
@echo "computers), the servers' journal and the terraria user."
deb: check
rm -rf $(STAGE)
+5 -3
View File
@@ -5,7 +5,7 @@ Run any number of vanilla **Terraria** and **tModLoader** dedicated servers on o
```bash
sudo terractl new calamity tmodloader
sudo terractl mods calamity add ~/tmod-export
sudo systemctl enable --now terraria@calamity
sudo terractl enable calamity --now
sudo terractl attach calamity
```
@@ -15,18 +15,20 @@ sudo terractl attach calamity
- **Vanilla and tModLoader side by side**, chosen per instance.
- **No manual installs.** Drop the official server zips into `/var/lib/terraria/dist`; they're unpacked on demand, and each instance can be pinned to a specific version.
- **Safe shutdowns.** Stopping a server saves the world first.
- **A live console.** `terractl attach` shows the server's output and lets you type commands into it.
- **A live console.** `terractl attach` shows the server's output and lets you type commands into it; `terractl watch` just shows the output.
- **Mod management.** `terractl mods` installs, enables and removes mods, with correct file ownership.
- **Tab completion** for commands, instance names, mods and console commands.
- **Export and import.** Pack an instance into one `.tar.gz` for backups or moving to another machine, and import it anywhere, with paths, ports and ownership adjusted.
- **Manage other hosts.** Run any command on another host with `terractl --host nas ...`, and copy or move instances between hosts with `terractl copy` and `terractl move`, like LXD/Incus remotes. Hosts opt in by running the `terractld` daemon; access is per computer, over pinned TLS.
- **Hardened.** Servers run as an unprivileged `terraria` user and can only write to their own data.
- **Access for moderators.** Members of the `terraria` group can use the console and read the servers' own logs, without `sudo` and without seeing the rest of the system's logs.
- **Access for moderators.** Members of the `terraria` group can start, stop and restart servers, use the console and read the servers' own logs, without `sudo` and without seeing the rest of the system's logs.
- **Helper for Windows.** A PowerShell script exports your enabled tModLoader mods for the server.
## Requirements
- Debian 12+ or Ubuntu 22.04+ (other systemd-based distributions should work with `make install`)
- `unzip`, `systemd`; for tModLoader, also `curl` and ICU (`libicu`)
- `python3` and `openssl`, for managing other hosts
## Install
+282 -25
View File
@@ -10,6 +10,9 @@
# serverconfig.txt normal Terraria server config (port, world, ...)
# server/ unpacked server binaries (replaceable)
# data/ worlds, mods, player data (kept across reinstalls)
#
# Managing other hosts (--host, remote, daemon, copy, move) is done by
# terractl-remote, in Python; see run_remote_helper.
set -euo pipefail
# Work from / so nothing depends on the caller's directory (which the 'terraria'
@@ -33,10 +36,32 @@ SVC_USER="${TERRARIA_USER:-terraria}"
STOP_TIMEOUT="${TERRARIA_STOP_TIMEOUT:-80}" # keep below TimeoutStopSec
JOURNAL_NS=terraria # LogNamespace= in terraria@.service
TERRACTL_VERSION="@VERSION@" # filled in by 'make install' / 'make deb'
REMOTE_HELPER="@PKGLIBDIR@/terractl-remote" # likewise
die() { echo "terractl: $*" >&2; exit 1; }
log() { echo "terractl: $*" >&2; }
# Is <user> (default: whoever runs this) in the terraria group?
in_group() {
[[ " $(id -nG "${1:-$(id -un)}" 2>/dev/null) " == *" $SVC_USER "* ]]
}
# A terractl command to suggest: hint root|group <command> [args...]. 'root'
# commands need sudo; 'group' ones only for people outside the terraria group
# (start/stop/restart are allowed to the group by the polkit rule). Under sudo,
# it advises the person who ran sudo. When terractld runs a command for another
# host, TERRACTL_VIA says how that host reaches this one (e.g. "--host nas").
hint() {
local need=$1 who=${SUDO_USER:-$(id -un)}; shift
if [[ -n ${TERRACTL_VIA:-} ]]; then
echo "terractl $TERRACTL_VIA $*"
elif [[ $who == root ]] || { [[ $need == group ]] && in_group "$who"; }; then
echo "terractl $*"
else
echo "sudo terractl $*"
fi
}
usage() {
cat <<EOF
Usage: terractl <command> <instance> [args]
@@ -44,8 +69,13 @@ Usage: terractl <command> <instance> [args]
Admin commands:
new <name> [vanilla|tmodloader] [port] create an instance (run as root)
list show instances, type, port, state
list show instances, type, port, boot, state
info [<name>] paths and zips of this host, or details of an instance
start|stop|restart <name> start, stop (saves the world) or restart a server
enable <name> [--now] start at boot (--now: also start it now)
disable <name> [--now] don't start at boot (--now: also stop it now)
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
(--clean also re-downloads tModLoader's .NET)
@@ -54,17 +84,32 @@ Admin commands:
mods <name> enable|disable <mod>... change which installed mods load
mods <name> remove <mod>... uninstall mods
delete <name> [-y] [--no-backup] stop, disable and remove an instance
rename <name> <new name> rename an instance (restarted if it was running)
export <name> [file.tar.gz] [--with-server]
archive an instance (configs, worlds, mods)
import <name> <file.tar.gz> create instance <name> from an archive
(worlds/configs archived to $BASE_DIR/.deleted/)
Other hosts:
--host <remote> <command> [args] run a command on another host (e.g. --host nas list)
copy [<remote>:]<name> [<remote>:][<new name>] [--with-server]
copy an instance, on this host or between hosts
move [<remote>:]<name> [<remote>:][<new name>] [--with-server]
move (or rename) an instance; stops it while moving
remote add <name> <token> add a host to manage (token from 'daemon trust add')
remote add <name> <address> [<token>] ... when the token's addresses don't work from here
remote list | remote remove <name> show or forget remotes
daemon enable|disable|status let other hosts manage this one (terractld.service)
daemon trust add <client> create a one-time token for a computer to join
daemon trust list | trust remove <client>
show or revoke computers that may manage this host
Used by terraria@.service:
journal-access let the terraria group read the servers' journal
prepare <name> create dirs/configs, unpack server if needed
run <name> exec the server
stop <name> <mainpid> graceful shutdown via console "exit" (service only;
to stop a server use: systemctl stop terraria@<name>)
service-stop <name> <mainpid> graceful shutdown via console "exit" (service only;
to stop a server use: terractl stop <name>)
EOF
exit "${1:-1}"
}
@@ -239,7 +284,7 @@ take_lock() {
state=$(systemctl show -p ActiveState --value "terraria@$name" 2>/dev/null || true)
case $state in
active|activating|deactivating|reloading)
die "terraria@$name is $state; stop it first: systemctl stop terraria@$name" ;;
die "terraria@$name is $state; stop it first: $(hint group stop "$name")" ;;
*)
die "instance '$name' is already running outside systemd (e.g. 'terractl run' in another terminal); stop that first" ;;
esac
@@ -293,11 +338,12 @@ cmd_run() {
esac
}
cmd_stop() {
# ExecStop of terraria@.service
cmd_service_stop() {
local name=$1 pid=${2:-} fifo="$RUN_DIR/$1.stdin" i
# systemd sets INVOCATION_ID for every process it runs for a service
if [[ -z ${INVOCATION_ID:-} ]]; then
die "'stop' is only for terraria@.service. To stop the server: systemctl stop terraria@$name"
die "'service-stop' is only called by terraria@.service. To stop the server: $(hint group stop "$name")"
fi
if [[ -n $pid && ! $pid =~ ^[0-9]+$ ]]; then die "invalid process ID '$pid'"; fi
if [[ -z $pid ]] || ! kill -0 "$pid" 2>/dev/null; then exit 0; fi
@@ -314,6 +360,42 @@ cmd_stop() {
log "server did not exit within ${STOP_TIMEOUT}s; leaving it to systemd"
}
# start/stop/restart through systemd: root, or the terraria group through the
# polkit rule (50-terractl.rules)
cmd_svc() {
local action=$1 name=$2 unit="terraria@$2" state; shift 2
[[ $# -eq 0 ]] || die "usage: terractl $action <name>"
# Only checkable with read access to the instances (root or the group)
if [[ -x $BASE_DIR && ! -f $(inst_dir "$name")/instance.conf ]]; then
die "no instance '$name' (create it with: $(hint root new "$name"))"
fi
if [[ $EUID -ne 0 ]] && ! in_group; then
die "'$action' needs root or the '$SVC_USER' group: sudo terractl $action $name"
fi
state=$(systemctl is-active "$unit" 2>/dev/null || true)
case $action in
start)
if [[ $state == active ]]; then echo "$unit is already running"; return 0; fi
systemctl start "$unit"
echo "$unit started" ;;
stop)
if [[ $state == inactive || $state == failed ]]; then
if instance_running "$name"; then
die "'$name' is running outside systemd (e.g. 'terractl run' in another terminal); type 'exit' in its console"
fi
echo "$unit isn't running"
return 0
fi
echo "stopping $unit (saves the world first)..."
systemctl stop "$unit"
echo "$unit stopped" ;;
restart)
echo "restarting $unit (saves the world first)..."
systemctl restart "$unit"
echo "$unit restarted" ;;
esac
}
# The servers log to their own journal (LogNamespace=terraria), which the
# 'terraria' group can read; see cmd_journal_access. It lives in
# <machine-id>.terraria under /var/log/journal (or /run/log/journal).
@@ -341,7 +423,7 @@ can_read_journal() {
journal_hint() {
{
echo "Note: commands are sent, but you can't see the server's output."
echo "$1"
if [[ " $(id -Gn) " == *" $SVC_USER "* ]]; then
echo "You're in the '$SVC_USER' group, but the servers' journal isn't set up for it"
echo "yet. That happens automatically when a server (re)starts, or run once:"
@@ -385,7 +467,7 @@ cmd_cmd() {
if ! can_read_journal; then
printf '%s\n' "$*" > "$fifo"
echo "sent."
journal_hint
journal_hint "Note: commands are sent, but you can't see the server's output."
return 0
fi
@@ -412,7 +494,7 @@ cmd_cmd() {
printf '%s\n' "$out"
else
echo "sent; no output from the server within 5s (it may be busy or still starting)."
echo "Watch it with: terractl attach $name"
echo "Watch it with: $(hint group attach "$name")"
fi
}
@@ -427,7 +509,7 @@ cmd_attach() {
journalctl --namespace="$JOURNAL_NS" -u "terraria@$name" -f -n 30 -o cat &
ATTACH_PID=$! # killed by cleanup() however attach ends
else
journal_hint
journal_hint "Note: commands are sent, but you can't see the server's output."
fi
trap 'echo; exit 0' INT
@@ -436,7 +518,7 @@ cmd_attach() {
[[ -z $line ]] && continue
if [[ $line == exit ]]; then
echo "(not sent: 'exit' would shut the server down. Use Ctrl-D to detach," \
"or 'systemctl stop terraria@$name' to stop it.)"
"or '$(hint group stop "$name")' to stop it.)"
continue
fi
printf '%s\n' "$line" > "$fifo"
@@ -444,6 +526,37 @@ cmd_attach() {
echo
}
# Like attach, but only shows output: nothing typed is sent to the server, so
# it's safe to leave running. Works whether or not the server is running.
cmd_watch() {
local name=$1 lines=30 quiet=0; shift
while [[ $# -gt 0 ]]; do
case $1 in
-n|--lines)
[[ ${2:-} =~ ^[0-9]+$ ]] || die "$1 needs a number of lines"
lines=$2; shift ;;
-q|--quiet) quiet=1 ;;
*) die "unknown option '$1'" ;;
esac
shift
done
# Only checkable with read access to the instances (root or the group)
if [[ -x $BASE_DIR && ! -f $(inst_dir "$name")/instance.conf ]]; then
die "no instance '$name'"
fi
if ! can_read_journal; then
journal_hint "Can't show the server's output: no read access to the servers' journal."
exit 1
fi
if [[ $quiet -eq 0 ]]; then
if ! systemctl is-active --quiet "terraria@$name" 2>/dev/null; then
echo "terraria@$name isn't running; output will appear here once it starts."
fi
echo "Watching terraria@$name (read-only). Ctrl-C quits; the server keeps running."
fi
exec journalctl --namespace="$JOURNAL_NS" -u "terraria@$name" -f -n "$lines" -o cat
}
cmd_delete() {
local name=$1; shift
local assume_yes=0 backup=1 dir archive answer
@@ -497,6 +610,39 @@ cmd_delete() {
echo "deleted instance '$name'"
}
# Gives an instance a new name: its folder, the paths in serverconfig.txt, and
# its service (stopped and started again, and still starting at boot if it did).
cmd_rename() {
local name=$1 new=${2:-} dir newdir state enabled
[[ $# -eq 2 ]] || die "usage: terractl rename <name> <new name>"
valid_name "$new"
[[ $EUID -eq 0 ]] || die "rename must be run as root: sudo terractl rename $name $new"
dir=$(inst_dir "$name"); newdir=$(inst_dir "$new")
[[ -f $dir/instance.conf ]] || die "no instance '$name'"
[[ ! -e $newdir ]] || die "instance '$new' already exists"
state=$(systemctl is-active "terraria@$name" 2>/dev/null || true)
enabled=$(systemctl is-enabled "terraria@$name" 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
echo "stopping terraria@$name (saves the world first)..."
systemctl stop "terraria@$name"
fi
if [[ $enabled == enabled ]]; then systemctl disable -q "terraria@$name"; fi
systemctl reset-failed "terraria@$name.service" "terraria@$name.socket" 2>/dev/null || true
mv "$dir" "$newdir"
if [[ -f $newdir/serverconfig.txt ]]; then
sed -i -E "s#^(world|worldpath|banlist)=.*/data/#\1=$newdir/data/#" "$newdir/serverconfig.txt"
fi
echo "renamed '$name' to '$new'"
if [[ $enabled == enabled ]]; then systemctl enable -q "terraria@$new"; echo "terraria@$new starts at boot"; fi
if [[ $state == active || $state == activating || $state == reloading ]]; then
systemctl start "terraria@$new"
echo "terraria@$new started"
fi
}
# ------------------------------------------------------- export/import ----
# Archive layout: terractl-export.txt (info about the export; optional)
# <instance>/... (instance.conf, serverconfig.txt, data/, [server/])
@@ -653,6 +799,13 @@ cmd_import() {
# Keep the server version the world was exported with, unless one is pinned already
load_conf "$name"
# A pin to an absolute path from another host usually doesn't exist here: use
# the zip of that name in this host's dist folder instead
if [[ $ZIP == /* && ! -r $ZIP && $(basename "$ZIP") =~ ^[A-Za-z0-9._+-]+\.zip$ ]]; then
log "ZIP=$ZIP isn't on this host; now ZIP=$(basename "$ZIP") (from $DIST_DIR)"
ZIP=$(basename "$ZIP")
sed -i "s/^ZIP=.*/ZIP=$ZIP/" "$dir/instance.conf"
fi
if [[ -z $ZIP && $meta_zip =~ ^[A-Za-z0-9._+-]+\.zip$ ]]; then
sed -i "s/^ZIP=.*/ZIP=$meta_zip/" "$dir/instance.conf"
ZIP=$meta_zip
@@ -671,7 +824,7 @@ cmd_import() {
else
echo "imported '$name' into $dir"
fi
echo "Review $dir/serverconfig.txt, then: systemctl enable --now terraria@$name"
echo "Review $dir/serverconfig.txt, then: $(hint root enable "$name" --now)"
}
# ---------------------------------------------------------------- mods ----
@@ -699,7 +852,7 @@ write_enabled() { # write_enabled <mods dir> <name>...
mods_restart_hint() {
if systemctl is-active --quiet "terraria@$name" 2>/dev/null; then
echo "Restart to apply: sudo systemctl restart terraria@$name"
echo "Restart to apply: $(hint group restart "$name")"
fi
}
@@ -712,7 +865,7 @@ mods_list() {
done
if [[ ${#files[@]} -eq 0 ]]; then
echo "No mods installed in $mdir"
echo "Add some with: terractl mods $name add <folder or .tmod files>"
echo "Add some with: $(hint root mods "$name" add "<folder or .tmod files>")"
return 0
fi
printf '%-32s %s\n' MOD STATE
@@ -820,7 +973,7 @@ cmd_mods() {
if [[ $# -gt 0 ]]; then shift; fi
local dir mdir
dir=$(inst_dir "$name")
[[ -f $dir/instance.conf ]] || die "no instance '$name' (create it with: terractl new $name tmodloader)"
[[ -f $dir/instance.conf ]] || die "no instance '$name' (create it with: $(hint root new "$name" tmodloader))"
load_conf "$name"
[[ $TYPE == tmodloader ]] || die "'$name' is a $TYPE instance; mods need TYPE=tmodloader"
mdir="$dir/data/Mods"
@@ -850,26 +1003,67 @@ Created $type instance '$name' in $dir
Review $dir/serverconfig.txt, then:
EOF
if [[ $type == tmodloader ]]; then
echo " terractl mods $name add <folder or .tmod files> (before first start)"
echo " $(hint root mods "$name" add "<folder or .tmod files>") (before first start)"
fi
cat <<EOF
systemctl enable --now terraria@$name
terractl attach $name
$(hint root enable "$name" --now)
$(hint group attach "$name")
EOF
}
# Whether the server starts at boot. Needs root: systemd doesn't let polkit
# rules limit enabling to our units, so the terraria group can't be allowed it.
cmd_enable() {
local action=$1 name=$2 now=0 unit="terraria@$2"; shift 2
while [[ $# -gt 0 ]]; do
case $1 in
--now) now=1 ;;
*) die "unknown option '$1'" ;;
esac
shift
done
[[ $EUID -eq 0 ]] || die "'$action' needs root: sudo terractl $action $name$(if [[ $now -eq 1 ]]; then echo " --now"; fi)"
if [[ $action == enable ]]; then
# Enabling a name without an instance would create a default one at boot
[[ -f $(inst_dir "$name")/instance.conf ]] ||
die "no instance '$name' (create it with: $(hint root new "$name"))"
systemctl enable -q "$unit"
if [[ $now -eq 1 ]]; then
systemctl start "$unit"
echo "$unit started and will start at boot"
elif systemctl is-active --quiet "$unit"; then
echo "$unit will start at boot"
else
echo "$unit will start at boot. To start it now: $(hint group start "$name")"
fi
else
systemctl disable -q "$unit"
if [[ $now -eq 1 ]]; then
echo "stopping $unit (saves the world first)..."
systemctl stop "$unit"
echo "$unit stopped and won't start at boot"
elif systemctl is-active --quiet "$unit"; then
echo "$unit won't start at boot; it is still running. To stop it: $(hint group stop "$name")"
else
echo "$unit won't start at boot"
fi
fi
}
cmd_list() {
local d n port state
printf '%-20s %-11s %-6s %s\n' INSTANCE TYPE PORT STATE
local d n port boot state
printf '%-20s %-11s %-6s %-9s %s\n' INSTANCE TYPE PORT BOOT STATE
for d in "$BASE_DIR"/*/; do
[[ -f $d/instance.conf ]] || continue
n=$(basename "$d")
(
load_conf "$n"
port=$(sed -n 's/^port=\([0-9][0-9]*\).*/\1/p' "$d/serverconfig.txt" 2>/dev/null | tail -n1)
# enabled, disabled, or e.g. masked
boot=$(systemctl is-enabled "terraria@$n" 2>/dev/null || true)
state=$(systemctl is-active "terraria@$n" 2>/dev/null || true)
if [[ $state != active ]] && instance_running "$n"; then state="running (outside systemd)"; fi
printf '%-20s %-11s %-6s %s\n' "$n" "$TYPE" "${port:--}" "$state"
printf '%-20s %-11s %-6s %-9s %s\n' "$n" "$TYPE" "${port:--}" "${boot:--}" "$state"
) 2>/dev/null || printf '%-20s %s\n' "$n" "(cannot read config - try sudo)"
done
}
@@ -884,17 +1078,69 @@ cmd_reinstall() {
esac
[[ -f $dir/instance.conf ]] || die "no instance '$name'"
if [[ $clean -eq 1 ]] && instance_running "$name"; then
die "'$name' is running; --clean would delete .NET under it. Stop it first: systemctl stop terraria@$name"
die "'$name' is running; --clean would delete .NET under it. Stop it first: $(hint group stop "$name")"
fi
rm -f "$dir/server/.installed"
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"
fi
echo "server/ will be re-unpacked on next start: systemctl restart terraria@$name"
echo "server/ will be re-unpacked on next start: $(hint group restart "$name")"
}
# key=value facts, one per line, for people and for terractl-remote (copy, move,
# tab completion). Without a name: this host. Repeated keys are lists.
cmd_info() {
local name=${1:-} dir f ver installed="" port
if [[ -z $name ]]; then
ver=$TERRACTL_VERSION; if [[ $ver == *@* ]]; then ver=development; fi
echo "version=$ver"
echo "host=$(hostname 2>/dev/null || echo unknown)"
echo "base_dir=$BASE_DIR"
echo "dist_dir=$DIST_DIR"
echo "run_dir=$RUN_DIR"
for f in "$DIST_DIR"/*.zip; do
if [[ -f $f ]]; then echo "dist_zip=$(basename "$f")"; fi
done
for f in "$BASE_DIR"/*/instance.conf; do
if [[ -f $f ]]; then echo "instance=$(basename "$(dirname "$f")")"; fi
done
return 0
fi
dir=$(inst_dir "$name")
echo "instance=$name"
if [[ ! -f $dir/instance.conf ]]; then echo "exists=no"; return 0; fi
load_conf "$name"
if [[ -r $dir/server/.installed ]]; then read -r _ installed < "$dir/server/.installed" || true; fi
port=$(sed -n 's/^port=\([0-9][0-9]*\).*/\1/p' "$dir/serverconfig.txt" 2>/dev/null | tail -n1) || true
echo "exists=yes"
echo "dir=$dir"
echo "type=$TYPE"
echo "port=$port"
echo "pinned_zip=$ZIP"
echo "installed_zip=$installed"
echo "state=$(systemctl is-active "terraria@$name" 2>/dev/null || true)"
echo "boot=$(systemctl is-enabled "terraria@$name" 2>/dev/null || true)"
echo "running=$(if instance_running "$name"; then echo yes; else echo no; fi)"
}
# Other hosts are handled by terractl-remote (Python). It runs this script for
# 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")
# 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
command -v python3 >/dev/null || die "this needs python3 (sudo apt install python3)"
TERRACTL_BIN=$self TERRACTL_VERSION=$ver TERRACTL_CALLER_PWD=$CALLER_PWD \
TERRACTL_LISTEN=${TERRACTL_LISTEN:-} exec python3 "$helper" "$@"
}
[[ $# -ge 1 ]] || usage
case $1 in
--host|--host=*|-H|remote|daemon|copy|move) run_remote_helper "$@" ;;
esac
command=$1; shift
case $command in
-h|--help|help) usage 0 ;;
@@ -904,6 +1150,7 @@ case $command in
else echo "terractl $TERRACTL_VERSION"; fi
exit 0 ;;
list) cmd_list; exit ;;
info) if [[ $# -eq 0 ]]; then cmd_info; exit; fi ;;
journal-access) cmd_journal_access; exit ;;
esac
[[ $# -ge 1 ]] || usage
@@ -917,20 +1164,30 @@ if [[ $command == prepare || $command == run ]]; then
exec runuser -u "$SVC_USER" -- env HOME="$(inst_dir "$1")" \
"$(readlink -f "$0")" "$command" "$@"
elif [[ $(id -un) != "$SVC_USER" ]]; then
die "$command must run as root or '$SVC_USER'"
# 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"
fi
fi
case $command in
prepare) cmd_prepare "$1" ;;
run) cmd_run "$1" ;;
stop) cmd_stop "$@" ;;
service-stop) cmd_service_stop "$@" ;;
stop)
# Before 0.0.11 the service stopped servers with 'terractl stop <name> <pid>';
# a unit loaded before an upgrade may still do that
if [[ -n ${INVOCATION_ID:-} && $# -eq 2 ]]; then cmd_service_stop "$@"; else cmd_svc stop "$@"; fi ;;
start|restart) cmd_svc "$command" "$@" ;;
info) cmd_info "$1" ;;
cmd) cmd_cmd "$@" ;;
attach) cmd_attach "$1" ;;
watch) cmd_watch "$@" ;;
enable|disable) cmd_enable "$command" "$@" ;;
mods) cmd_mods "$@" ;;
new) cmd_new "$@" ;;
reinstall) cmd_reinstall "$@" ;;
delete) cmd_delete "$@" ;;
rename) cmd_rename "$@" ;;
export) cmd_export "$@" ;;
import) cmd_import "$@" ;;
*) usage ;;
+75 -5
View File
@@ -32,6 +32,33 @@ _terractl_instances() {
fi
}
# Remotes this user added with 'terractl remote add'
_terractl_remotes() {
local f=${TERRACTL_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/terractl}/remotes.json
[[ -r $f ]] || return 0
python3 -c 'import json, sys; print("\n".join(json.load(open(sys.argv[1]))["remotes"]))' "$f" 2>/dev/null
}
# Instance names on a remote. This asks its daemon, so it can take a moment.
_terractl_remote_instances() {
timeout 5 terractl --host "$1" info 2>/dev/null </dev/null | sed -n 's/^instance=//p'
}
# [<remote>:]<name> for copy and move
_terractl_places() {
if [[ $cur == -* ]]; then
_terractl_reply "--with-server"
elif [[ $cur == *:* ]]; then
mapfile -t COMPREPLY < <(compgen -P "${cur%:*}:" -W "$(_terractl_remote_instances "${cur%:*}")" -- "${cur##*:}")
else
mapfile -t COMPREPLY < <(compgen -W "$(_terractl_instances) $(_terractl_remotes | sed 's/$/:/')" -- "$cur")
# After "<remote>:" comes an instance name, not a space
if [[ ${#COMPREPLY[@]} -eq 1 && ${COMPREPLY[0]} == *: ]]; then compopt -o nospace 2>/dev/null; fi
fi
# Readline sees only the part after the last ':'
if declare -F __ltrim_colon_completions >/dev/null 2>&1; then __ltrim_colon_completions "$cur"; fi
}
# Mods installed in an instance (*.tmod files), or enabled ones (enabled.json)
_terractl_mods() {
local mdir f
@@ -70,16 +97,33 @@ _terractl() {
if declare -F _init_completion >/dev/null 2>&1; then
# shellcheck disable=SC2034 # prev is set by _init_completion but not needed here
local prev
_init_completion || return
# -n : keeps "<remote>:<name>" (copy, move) in one word
_init_completion -n : || return
else
cur=${COMP_WORDS[COMP_CWORD]}
words=("${COMP_WORDS[@]}")
cword=$COMP_CWORD
fi
local commands="new list attach cmd mods reinstall delete export import prepare run --help --version"
local commands="new list info start stop restart enable disable attach watch cmd mods reinstall delete rename export import"
local console="save say playing kick ban motd password time dawn noon dusk midnight settle seed version maxplayers port clear help"
# terractl --host <remote> <command> ...: the same as on this host, except
# that instance names come from the remote and mod names aren't offered
local host=""
if [[ ${words[1]:-} == --host || ${words[1]:-} == -H ]]; then
if (( cword == 2 )); then
_terractl_reply "$(_terractl_remotes)"
return
fi
host=${words[2]}
words=("${words[0]}" "${words[@]:3}")
cword=$((cword - 2))
commands+=" --help --version"
else
commands+=" copy move remote daemon prepare run --host --help --version"
fi
if (( cword == 1 )); then
_terractl_reply "$commands"
return
@@ -97,10 +141,31 @@ _terractl() {
# import <new name> <file.tar.gz>: the name is new, so nothing to offer
if (( cword == 3 )); then _terractl_files tar.gz; fi
return ;;
copy|move)
_terractl_places
return ;;
remote)
if (( cword == 2 )); then
_terractl_reply "add list remove"
elif (( cword == 3 )) && [[ ${words[2]} == remove ]]; then
_terractl_reply "$(_terractl_remotes)"
fi
return ;;
daemon)
if (( cword == 2 )); then
_terractl_reply "enable disable status trust"
elif (( cword == 3 )) && [[ ${words[2]} == trust ]]; then
_terractl_reply "add list remove"
fi
return ;;
esac
if (( cword == 2 )); then
_terractl_reply "$(_terractl_instances)"
if [[ -n $host ]]; then
_terractl_reply "$(_terractl_remote_instances "$host")"
else
_terractl_reply "$(_terractl_instances)"
fi
return
fi
@@ -110,6 +175,11 @@ _terractl() {
if (( cword == 3 )); then _terractl_reply "$console"; fi ;;
reinstall)
if (( cword == 3 )); then _terractl_reply "--clean"; fi ;;
enable|disable)
if (( cword == 3 )); then _terractl_reply "--now"; fi ;;
watch)
# the number after -n is free-form
if [[ $cur == -* ]]; then _terractl_reply "-n -q"; fi ;;
delete)
_terractl_reply "-y --yes --no-backup" ;;
export)
@@ -124,9 +194,9 @@ _terractl() {
# .tmod files and folders (e.g. the Windows export folder)
_terractl_files tmod ;;
enable|remove)
_terractl_reply "$(_terractl_mods "$instance" installed)" ;;
if [[ -z $host ]]; then _terractl_reply "$(_terractl_mods "$instance" installed)"; fi ;;
disable)
_terractl_reply "$(_terractl_mods "$instance" enabled)" ;;
if [[ -z $host ]]; then _terractl_reply "$(_terractl_mods "$instance" enabled)"; fi ;;
esac ;;
esac
}
+5
View File
@@ -12,3 +12,8 @@
# Seconds to wait for a server to save and exit on stop (keep below 90)
#TERRARIA_STOP_TIMEOUT=80
# Where terractld (terractl daemon) listens for other hosts: an address and/or
# port. Default: all addresses, port 7776. After changing it:
# sudo systemctl restart terractld
#TERRACTL_LISTEN=10.0.10.77:7776
+23
View File
@@ -0,0 +1,23 @@
// Members of the 'terraria' group may start, stop and restart Terraria servers
// (terraria@<name>.service) without sudo. Nothing else: enable/disable and all
// other units still need root.
//
// Starting a name that has no instance yet creates a vanilla instance with
// default settings, as it does for root.
//
// Needs polkit 0.106 or newer (JavaScript rules); Ubuntu 22.04's 0.105 ignores it.
polkit.addRule(function(action, subject) {
if (action.id != "org.freedesktop.systemd1.manage-units" ||
!subject.isInGroup("terraria")) {
return polkit.Result.NOT_HANDLED;
}
// Same names terractl accepts (valid_name)
var unit = action.lookup("unit") || "";
var verb = action.lookup("verb") || "";
var verbs = ["start", "stop", "restart", "try-restart", "reset-failed"];
if (/^terraria@[A-Za-z0-9_][A-Za-z0-9_.-]*\.service$/.test(unit) &&
verbs.indexOf(verb) >= 0) {
return polkit.Result.YES;
}
return polkit.Result.NOT_HANDLED;
});
+226 -40
View File
@@ -16,14 +16,15 @@ Run any number of vanilla Terraria and tModLoader dedicated servers on one Linux
10. [Configuration](#configuration)
11. [Mods (tModLoader)](#mods-tmodloader)
12. [Versions and upgrades](#versions-and-upgrades)
13. [Deleting and restoring](#deleting-and-restoring)
13. [Deleting, renaming and restoring](#deleting-renaming-and-restoring)
14. [Exporting and importing](#exporting-and-importing)
15. [Backups](#backups)
16. [Permissions](#permissions)
17. [Running by hand for debugging](#running-by-hand-for-debugging)
18. [Troubleshooting](#troubleshooting)
19. [Tab completion](#tab-completion)
20. [Command reference](#command-reference)
15. [Managing other hosts](#managing-other-hosts)
16. [Backups](#backups)
17. [Permissions](#permissions)
18. [Running by hand for debugging](#running-by-hand-for-debugging)
19. [Troubleshooting](#troubleshooting)
20. [Tab completion](#tab-completion)
21. [Command reference](#command-reference)
---
@@ -49,10 +50,13 @@ Everything is installed by the `terraria-server-manager` package:
| `/usr/lib/systemd/system/terraria@.service` | Template service. One copy runs per instance. |
| `/usr/lib/systemd/system/terraria@.socket` | Creates the console pipe the server reads commands from. |
| `/usr/bin/terractl` | Helper script. The service calls it to set up, run and stop servers; you use it for everyday management. |
| `/etc/default/terractl` | Optional overrides (locations, stop timeout). |
| `/usr/lib/terraria-server-manager/terractl-remote` | The part of `terractl` that manages other hosts (`--host`, `remote`, `daemon`, `copy`, `move`), in Python. |
| `/usr/lib/systemd/system/terractld.service` | The daemon that lets other hosts manage this one. Off until you turn it on (see [Managing other hosts](#managing-other-hosts)). |
| `/etc/default/terractl` | Optional overrides (locations, stop timeout, the daemon's address). |
| `/etc/systemd/journald@terraria.conf` | Size and retention of the servers' own journal (see [Logs](#logs)). |
| `/usr/lib/sysusers.d/terractl.conf` | Defines the `terraria` system user. |
| `/usr/lib/tmpfiles.d/terractl.conf` | Creates `/var/lib/terraria` and its subdirectories. |
| `/usr/share/polkit-1/rules.d/50-terractl.rules` | Lets members of the `terraria` group start, stop and restart servers without `sudo`. |
| `/usr/share/terraria-server-manager/export-tmod-mods.ps1` | Optional. Copy to your Windows PC to collect your enabled tModLoader mods. |
What happens when an instance starts:
@@ -61,7 +65,7 @@ What happens when an instance starts:
2. `terractl prepare <name>` creates the instance directory and default configs if they're missing, and unpacks the server zip if it hasn't been unpacked yet.
3. `terractl run <name>` starts the vanilla or tModLoader server, reading commands from the pipe and logging to the servers' own journal (see [Logs](#logs)).
On stop, `terractl stop` sends `exit` through the pipe so the world is saved before the server shuts down.
On stop, `terractl service-stop` sends `exit` through the pipe so the world is saved before the server shuts down.
---
@@ -86,6 +90,8 @@ On stop, `terractl stop` sends `exit` through the pipe so the world is saved bef
└── .deleted/ Backups made by 'terractl delete'
/run/terraria/<name>.stdin Console pipe (exists only while running)
/var/lib/terractl/ The daemon's certificate and trusted computers (root only)
```
The server can write only inside `instances/`. It reads `dist/` but can never change it.
@@ -100,15 +106,15 @@ The package works on Debian 12 or newer and Ubuntu 22.04 or newer.
sudo apt install ./terraria-server-manager_0.0.9_all.deb
```
Using `apt install ./file.deb` (rather than `dpkg -i`) installs the dependencies automatically. `unzip` is required. `curl` and the ICU library are "recommended", which apt installs by default; they're only needed for tModLoader instances, which crash with `Aborted` without ICU.
Using `apt install ./file.deb` (rather than `dpkg -i`) installs the dependencies automatically. `unzip` is required. `curl` and the ICU library are "recommended", which apt installs by default; they're only needed for tModLoader instances, which crash with `Aborted` without ICU. `polkitd` is also recommended: it lets members of the `terraria` group start and stop servers without `sudo` (see [Permissions](#permissions)).
Installing the package:
- creates the `terraria` system user and group;
- creates `/var/lib/terraria/`, `/var/lib/terraria/instances/` and `/var/lib/terraria/dist/`;
- installs the units and helper script.
- installs the units, the helper script and the polkit rule.
It doesn't start or enable anything.
It doesn't start or enable anything, including the daemon for managing the host from elsewhere.
If you use a firewall, open each instance's port for **TCP**. Terraria uses TCP only.
@@ -173,6 +179,8 @@ sudo make install # or install directly without a package (PREFIX=/usr/
sudo make uninstall # removes a 'make install' (keeps settings and data)
```
`make install` with a prefix other than `/usr` puts the polkit rule in `/etc/polkit-1/rules.d/`, because polkit doesn't read `/usr/local`.
The version comes from the `VERSION` file. Set `Maintainer:` in `packaging/deb/control` to yourself before publishing packages.
### Changing locations
@@ -281,7 +289,7 @@ Instance names may contain letters, digits, `_`, `-` and `.`, and must not start
sudo terractl new calamity tmodloader
sudo terractl mods calamity add ~/tmod-export # your mods, before the first start
sudo nano /var/lib/terraria/instances/calamity/serverconfig.txt # optional: world size, difficulty
sudo systemctl enable --now terraria@calamity
sudo terractl enable calamity --now
sudo terractl attach calamity # watch it load and generate the world
```
@@ -291,24 +299,41 @@ You can also skip `new` and just run `sudo systemctl start terraria@<name>`. Tha
## Starting, stopping and boot
Instances are ordinary systemd services:
Instances are ordinary systemd services, which `terractl` starts and stops for you:
```bash
sudo systemctl start terraria@calamity
sudo systemctl stop terraria@calamity # saves the world, then exits
sudo systemctl restart terraria@calamity
sudo systemctl enable terraria@calamity # start at boot
sudo systemctl disable terraria@calamity # don't start at boot
sudo systemctl enable --now terraria@calamity # both at once
systemctl status terraria@calamity
sudo terractl start calamity
sudo terractl stop calamity # saves the world, then exits
sudo terractl restart calamity
```
To see all instances with their type, port and state:
These are the same as `systemctl start|stop|restart terraria@<name>`, which work too; `systemctl status terraria@<name>` shows the details. `terractl start` refuses names that have no instance, whereas `systemctl start` creates one.
Whether a server starts at boot is set with `terractl`:
```bash
sudo terractl enable calamity # start at boot
sudo terractl enable calamity --now # start at boot, and start it now
sudo terractl disable calamity # don't start at boot (a running server keeps running)
sudo terractl disable calamity --now # don't start at boot, and stop it now (saves the world)
```
These do the same as `systemctl enable`/`disable terraria@<name>`, which also still work. `terractl enable` refuses names that have no instance, so a typo can't create a new instance at the next boot.
Members of the `terraria` group can `start`, `stop` and `restart` servers without `sudo` (see [Permissions](#permissions)). `enable` and `disable` always need `sudo`.
To see all instances with their type, port, whether they start at boot, and state:
```bash
sudo terractl list
```
```
INSTANCE TYPE PORT BOOT STATE
calamity tmodloader 7778 enabled active
survival vanilla 7777 disabled inactive
```
Behaviour to be aware of:
- **Crashes restart automatically** after 10 seconds.
@@ -330,10 +355,20 @@ sudo terractl attach calamity
This shows the last 30 lines of output and then follows it live. Anything you type is sent to the server, with arrow-key editing and history. **Ctrl-D** or **Ctrl-C** detaches, and the server keeps running.
`attach` will not send `exit`, since that would stop the server. Use `systemctl stop` to stop it.
`attach` will not send `exit`, since that would stop the server. Use `terractl stop` to stop it.
The one visible difference from a real terminal: the server's `:` prompt only appears once a newline follows it.
### Read-only: `watch`
```bash
terractl watch calamity # last 30 lines, then follow
terractl watch calamity -n 200 # start with the last 200 lines
terractl watch calamity -q # without the lines about what it's doing
```
`watch` shows the server's output like `attach` does, but never sends anything to the server. That makes it safe to leave open, for instance on a second screen, with no risk of typing into the console. **Ctrl-C** quits and the server keeps running. It also works while the server is stopped: it shows the last output and picks up again when the server starts.
### One-off commands: `cmd`
```bash
@@ -343,7 +378,7 @@ sudo terractl cmd calamity say Restarting in 5 minutes
`cmd` prints the server's reply, then returns once the server has been quiet for a second (at most 5 seconds). For example, `terractl cmd calamity playing` lists who's online. That makes it handy in scripts and cron jobs too. If the server doesn't answer within 5 seconds (for instance while it's still loading), `cmd` says so; the command has still been sent.
Members of the `terraria` group can use `attach` and `cmd` fully, output included, without `sudo`. See [Logs](#logs) for how that's arranged.
Members of the `terraria` group can use `attach`, `watch` and `cmd` fully, output included, without `sudo`. See [Logs](#logs) for how that's arranged.
### Logs
@@ -543,7 +578,7 @@ sudo systemctl restart terraria@calamity
---
## Deleting and restoring
## Deleting, renaming and restoring
```bash
sudo terractl delete calamity # asks you to type the name
@@ -557,11 +592,17 @@ To restore an instance, import its backup. You can give it back its old name, or
```bash
sudo terractl import calamity /var/lib/terraria/instances/.deleted/calamity-20260928-134422.tar.gz
sudo systemctl enable --now terraria@calamity # server/ is unpacked again on start
sudo terractl enable calamity --now # server/ is unpacked again on start
```
`.deleted/` is never cleaned up automatically. Remove old archives yourself.
To give an instance another name, use `rename`. It stops the server if it's running, renames the folder, updates the paths in `serverconfig.txt`, and starts it again under the new name. The port, world, mods and unpacked server stay as they are, and if it started at boot, it still does:
```bash
sudo terractl rename calamity calamity-old
```
---
## Exporting and importing
@@ -599,11 +640,13 @@ After extracting, it adjusts the instance for its new home:
`import` also accepts:
- the backups that `delete` makes (see [Deleting and restoring](#deleting-and-restoring));
- the backups that `delete` makes (see [Deleting, renaming and restoring](#deleting-renaming-and-restoring));
- a plain archive of an instance folder, including one from the old `/opt/terraria` manual setup, e.g. `tar -czf old.tar.gz -C /opt/terraria calamity`.
### Moving a server to another machine
If the new machine runs the daemon, `sudo terractl move calamity newhost:` does all of this in one step; see [Managing other hosts](#managing-other-hosts). By hand:
```bash
# old machine
sudo terractl export calamity /tmp/calamity.tar.gz
@@ -611,11 +654,116 @@ scp /tmp/calamity.tar.gz newhost:/tmp/
# new machine (with terraria-server-manager installed, and the same zip in dist/)
sudo terractl import calamity /tmp/calamity.tar.gz
sudo systemctl enable --now terraria@calamity
sudo terractl enable calamity --now
```
---
## Managing other hosts
One computer can manage the Terraria servers of several hosts. A host that should be managed runs **the daemon**, `terractld`. The computers that manage it add it as a **remote**, as with LXD or Incus remotes. Then any command can be run there with `--host`, and instances can be copied and moved between hosts:
```bash
terractl --host nas list
terractl --host nas attach calamity
sudo terractl move calamity nas: # this host → nas
terractl copy nas:calamity pi:test # nas → pi, as 'test'
```
### Turning on the daemon
On the host to be managed:
```bash
sudo terractl daemon enable
```
This starts `terractld.service` and makes it start at boot. It listens on **TCP port 7776**, on all addresses. If a firewall is active, allow that port (for example `sudo ufw allow 7776/tcp`), ideally only from the computers that will manage the host. To listen on one address or on another port, set `TERRACTL_LISTEN` in `/etc/default/terractl` (e.g. `TERRACTL_LISTEN=10.0.10.77:7776`) and run `sudo systemctl restart terractld`.
`sudo terractl daemon disable` turns it off again. `terractl daemon status` shows whether it's running, where it listens and its certificate's fingerprint, and with `sudo`, which computers it trusts.
The daemon is an ordinary systemd service: systemd starts it at boot, restarts it if it crashes, and keeps its log. `journalctl -u terractld` lists every command it ran and for which computer.
### Adding a remote
Each computer that should manage the host needs a one-time token. On the host with the daemon:
```bash
sudo terractl daemon trust add laptop
```
`laptop` is a name for the computer, used in the host's log and to revoke it later. The command prints what to run on that computer:
```bash
terractl remote add nas tctl1_eyJ2IjoxLCJmcCI6...
```
`nas` is the name you'll use for the host there; choose any. The token contains the host's addresses, so usually nothing else is needed. If none of them works from the computer (behind NAT, for example), add the address: `terractl remote add nas 203.0.113.5 tctl1_...`, or `nas.example.com:7800` for another port.
A token works **once**, **within 24 hours**. Remotes are kept per user in `~/.config/terractl/remotes.json`, readable only by you. Under `sudo`, the remotes of the person who ran sudo are used.
```bash
terractl remote list # your remotes
terractl remote remove nas # forget one
```
### Running commands on a remote: `--host`
Put `--host <remote>` in front of any command:
```bash
terractl --host nas list
terractl --host nas new calamity tmodloader
terractl --host nas mods calamity add ~/tmod-export # sends the files
terractl --host nas enable calamity --now
terractl --host nas attach calamity
terractl --host nas export calamity # fetches ./calamity-<date>-<time>.tar.gz
terractl --host nas import calamity2 calamity.tar.gz # sends the archive
```
`<remote>` is a remote's name, or the address it was added with (`--host 10.0.10.77`). No `sudo` is needed on either side: the daemon runs each command as root on its host, as if it had been typed there with `sudo`. File arguments (`mods add`, `import`, `export`) are files on *your* computer; they're sent to the host or fetched from it. Messages that suggest a command show it as `terractl --host nas ...`.
Not available with `--host`:
- `remote`, `daemon`, `copy` and `move` run on your computer (copy and move take `<remote>:<name>` instead).
- The service's own commands (`prepare`, `run`, `service-stop`, `journal-access`).
- Editing `serverconfig.txt` still needs a shell on that host.
### Copying and moving instances
```bash
terractl copy calamity nas: # to nas, same name
sudo terractl copy nas:calamity cal-test # from nas to this host, as cal-test
terractl copy nas:calamity pi: # between two remotes
sudo terractl move calamity nas: # to nas; removed here
terractl move nas:calamity pi: # between two remotes
sudo terractl move calamity survival2 # rename, on this host (same as rename)
```
Each side is `[<remote>:]<name>`, and without a remote it's this host. Leave out the destination name (`nas:`) to keep the name.
A copy is an [export](#exporting-and-importing) on the source, a transfer, and an import on the destination, so everything import does applies. Paths are set to the **destination's** folders, whatever its `TERRARIA_BASE` is: they're asked from the destination, never assumed. A port that's taken there is replaced with a free one, and the server version is pinned.
- **The server zip comes along if needed.** If the destination's `dist/` doesn't have the zip the instance runs, it's copied from the source's `dist/`. If the source doesn't have it either, nothing is changed and you're told so; put the zip on the destination first, or use `--with-server` to take the unpacked server along.
- **Copy leaves the source running.** A running server is asked to save first, so the copy has the world as of that save. Anything that happens in the game after it isn't included. The copy isn't started.
- **Move within one host is a rename** (`sudo terractl move calamity calamity-old`, or `nas:a nas:b`): nothing is copied, and the port and unpacked server stay the same. See [Deleting, renaming and restoring](#deleting-renaming-and-restoring).
- **Move stops the source first**, which saves the world, and removes it from the source only after the import worked. `delete`'s usual backup is kept in the source's `.deleted/`. If the source was running, the moved instance is started on the destination, and if it started at boot, it does there too. If something fails before the import has worked, the source is started again and nothing else changes.
- **Transfers go through the computer you run the command on.** Between two remotes, the archive streams from one to the other through your computer without being stored on it.
On this host, copying *from* it works for members of the `terraria` group. Copying *to* it, and moving away from it, needs `sudo`, like `import` and `delete` do.
### Security
- **Encrypted and pinned.** Traffic is HTTPS (TLS 1.2 or newer). The daemon's certificate is self-signed, the token carries its fingerprint, and every connection checks it, so nothing in between can pose as the daemon. If the fingerprint ever changes, `terractl` refuses to connect and says so.
- **Keys, not passwords.** Joining trades the token for a random key for that computer. The daemon keeps only hashes of keys and tokens, in `/var/lib/terractl/trust.json` (root only).
- **A trusted computer is an admin of the host's servers.** It can do what `sudo terractl` can: create, delete, import, start and stop instances, and install mods or server files, which run as the `terraria` user. It can't change the daemon or run anything other than `terractl`. Give tokens only to computers you'd trust with `sudo terractl`.
- **Revoking.** `sudo terractl daemon trust list` shows the trusted computers, and `sudo terractl daemon trust remove laptop` takes one's access away immediately.
- **Firewall.** The daemon doesn't need to be reachable from the internet. Limit port 7776 to your LAN or VPN, or set `TERRACTL_LISTEN` to a LAN or VPN address.
The daemon runs as root, because it creates and removes instances and starts services, but with systemd's protections it can't write to `/usr`, `/boot`, `/etc` or home folders. Its certificate, key and trust list are in `/var/lib/terractl/`. Files in transit are kept in `/var/lib/terractl/transfers/`, and removed once used, after 6 hours unused, or when the daemon restarts.
---
## Backups
Everything worth keeping is in `data/`, `instance.conf` and `serverconfig.txt`, which is exactly what `export` saves. A nightly backup kept for 14 days, as a root cron job (`sudo crontab -e`):
@@ -635,19 +783,27 @@ The servers always run as the `terraria` user. Administration is simplest with `
| Command | Who can run it | Notes |
|---|---|---|
| `new` | root (or `terraria`) | Run as root, it sets ownership of the new instance to `terraria` itself. |
| `list` | root, or members of the `terraria` group | `/var/lib/terraria` is not readable by other users. |
| `list`, `info` | root, or members of the `terraria` group | `/var/lib/terraria` is not readable by other users. |
| `start`, `stop`, `restart` | root, or members of the `terraria` group | Through systemd, where the polkit rule allows the group (see `systemctl start/stop/restart` below). |
| `enable`, `disable` | root only | Checked by the script. systemd can't restrict these to Terraria servers, so the group can't be allowed them. |
| Adding zips to `dist/` | root, or members of the `terraria` group | |
| `attach` | root, or members of the `terraria` group | Writes to the console pipe and reads the servers' journal. |
| `cmd` | root, or members of the `terraria` group | Same as `attach`. |
| `watch` | root, or members of the `terraria` group | Reads the servers' journal only. |
| `reinstall` | root (or `terraria`) | |
| `mods <name>` (list) | root, or members of the `terraria` group | |
| `mods add/enable/disable/remove` | root (or `terraria`) | Run as root, it sets ownership of the files it installs to `terraria`. |
| `delete` | root only | Checked by the script. |
| `delete`, `rename` | root only | Checked by the script. |
| `export` | root, or members of the `terraria` group | Reads the instance, and asks a running server to save. |
| `import` | root (or `terraria`) | Run as root, it gives the new instance to `terraria`. |
| `prepare`, `run` | Normally only the service | Run as root, they automatically switch to the `terraria` user, so no files end up owned by root. Any other user is refused. |
| `stop` | Only the service | Needs the server's process ID from systemd. |
| `systemctl start/stop/enable` | root | |
| `service-stop` | Only the service | Needs the server's process ID from systemd. |
| `daemon enable`, `daemon disable`, `daemon trust ...` | root only | |
| `daemon status` | anyone | Shows the trusted computers only with `sudo`. |
| `remote ...`, `--host ...` | anyone | Uses your own remotes. On the remote, the daemon runs the command as root. |
| `copy`, `move` | depends on this host's part | Copying from this host: the group is enough. Copying to it, or moving away from it: root. |
| `systemctl start/stop/restart terraria@...` | root, or members of the `terraria` group | Allowed by the polkit rule `50-terractl.rules`. Needs `polkitd` 0.106 or newer, so not on Ubuntu 22.04, where it needs `sudo`. Starting a name that has no instance creates a vanilla one with default settings. |
| `systemctl enable/disable terraria@...` | root | Same as `terractl enable`/`disable`. |
| `journalctl --namespace=terraria ...` | root, or members of the `terraria` group | The servers' output only; see [Logs](#logs). |
| `journalctl -u terraria@...` (system journal) | root, or members of `systemd-journal` or `adm` | systemd's start/stop messages. |
@@ -682,14 +838,14 @@ Editing an existing file with `sudo nano` keeps its owner, so it's fine.
Stop the service first. `terractl` won't let two copies of the same server run at once: if the service is running, `run` refuses and tells you to stop it.
```bash
sudo systemctl stop terraria@calamity
sudo terractl stop calamity
sudo terractl run calamity # automatically runs as 'terraria'
```
The server runs in your terminal and you can type commands into it directly. Type `exit` to stop it, then start the service again:
```bash
sudo systemctl start terraria@calamity
sudo terractl start calamity
```
### How running servers are protected
@@ -702,7 +858,7 @@ While a server runs, whether systemd started it or you did, it holds a lock on t
- **`delete` stops the service**, but refuses to go on if a copy started by hand is still running.
- **`list` shows a copy started by hand** as `running (outside systemd)`.
`terractl stop` is only for the service's own use. Typed by hand, it tells you to use `systemctl stop terraria@<name>` instead.
`terractl stop` only stops the service. A copy started by hand is stopped by typing `exit` in its terminal.
---
@@ -741,7 +897,7 @@ Run `terractl mods <name>` to check for mods that are enabled but missing. Then
Two instances share a port. Check the ports with `sudo terractl list`. A second copy of the *same* instance can't be the cause, because `terractl` prevents it.
**"already running outside systemd" or "is active; stop it first".**
The instance's server is already running, so the command was refused to protect it. Stop the server first. If it was started with `terractl run` in another terminal, type `exit` there; otherwise use `sudo systemctl stop terraria@<name>`.
The instance's server is already running, so the command was refused to protect it. Stop the server first. If it was started with `terractl run` in another terminal, type `exit` there; otherwise use `terractl stop <name>`.
**The service starts but you can't connect.**
Check the port in `serverconfig.txt`, your firewall (TCP), and router port forwarding.
@@ -750,11 +906,20 @@ Check the port in `serverconfig.txt`, your firewall (TCP), and router port forwa
Both commands print a note explaining why. If you were just added to the `terraria` group, log out and back in. If you're already in it, the servers' journal hasn't been set up for the group yet; that happens automatically when a server starts, or run `sudo terractl journal-access`. Servers that were running before upgrading to 0.0.11 keep logging to the system journal until they're restarted.
**The server isn't restarting after it stopped.**
It exited cleanly, probably via `exit` in the console, so systemd leaves it stopped. Start it again with `systemctl start`.
It exited cleanly, probably via `exit` in the console, so systemd leaves it stopped. Start it again with `terractl start`.
**The world was created without mod content.**
It was generated before the mods were added. Delete the world files in `data/Worlds/` (or point `world=` at a new name) and restart to generate a new one.
**`can't reach nas at 10.0.10.77:7776`.**
The daemon isn't running on that host (check with `terractl daemon status` there), a firewall blocks the port, or the host's address changed. After an address change, remove the remote and add it again with a new token.
**`isn't the one 'nas' was added with: its certificate changed`.**
The daemon has a different certificate than when you added it. That's expected if `/var/lib/terractl` was deleted there, or the package was purged and installed again; remove the remote and add it again. If nothing like that happened, don't work around it: something may be intercepting the connection.
**`doesn't trust this computer (any more)`.**
Its access was revoked with `daemon trust remove`, or that computer was added again under the same name elsewhere. Get a new token.
---
## Tab completion
@@ -765,7 +930,8 @@ It was generated before the mods were added. Delete the world files in `data/Wor
- instance names;
- `vanilla`/`tmodloader` for `new`;
- common server console commands for `cmd` (`save`, `say`, `playing`, `kick`...);
- the options of `reinstall`, `delete` and `export`;
- the options of `enable`, `disable`, `watch`, `reinstall`, `delete`, `export`, `copy` and `move`;
- remote names after `--host`, and `<remote>:<name>` for `copy` and `move`. Instance names on a remote are asked from its daemon, so they can take a moment;
- `.tar.gz` files for `export` and `import`;
- `mods` actions. For `mods add` it completes `.tmod` files and folders; for `mods enable` and `mods remove`, the installed mods; for `mods disable`, the enabled ones.
@@ -779,8 +945,13 @@ Completion runs as *you*, even when you type `sudo`. Instance and mod names ther
```
terractl new <name> [vanilla|tmodloader] [port] Create an instance (not started)
terractl list Show instances, type, port, state
terractl list Show instances, type, port, boot, state
terractl info [<name>] This host's paths and zips, or an instance's details
terractl start|stop|restart <name> Start, stop (saves the world) or restart
terractl enable <name> [--now] Start at boot (--now: and start now)
terractl disable <name> [--now] Don't start at boot (--now: and stop now)
terractl attach <name> Interactive console (Ctrl-D detaches)
terractl watch <name> [-n <lines>] [-q] Follow the output, read-only (Ctrl-C quits)
terractl cmd <name> <command...> Send one console command
terractl reinstall <name> [--clean] Re-unpack server/ on next start
(--clean also re-downloads .NET)
@@ -789,15 +960,30 @@ terractl mods <name> add <folder|file.tmod>... Install and enable mods (and M
terractl mods <name> enable|disable <Mod>... Change which mods load
terractl mods <name> remove <Mod>... Uninstall mods
terractl delete <name> [-y] [--no-backup] Stop, disable, back up and remove
terractl rename <name> <new name> Rename (restarted if it was running)
terractl export <name> [file.tar.gz] [--with-server]
Archive an instance (configs, worlds, mods)
terractl import <name> <file.tar.gz> Create an instance from an archive
terractl --version Show the installed version
Other hosts:
terractl --host <remote> <command> [args] Run a command on a remote
terractl copy [<remote>:]<name> [<remote>:][<new name>] [--with-server]
Copy an instance, here or between hosts
terractl move [<remote>:]<name> [<remote>:][<new name>] [--with-server]
Move or rename an instance
terractl remote add <name> <token> Add a remote (token from 'daemon trust add')
terractl remote add <name> <address> [<token>] ... at an address the token doesn't list
terractl remote list | remove <name> Show or forget remotes
terractl daemon enable|disable|status Let other hosts manage this one
terractl daemon trust add|remove <name> Create a token for a computer, or revoke one
terractl daemon trust list Show trusted computers and unused tokens
Used by the service:
terractl prepare <name> Create dirs/configs, unpack zip if needed
terractl run <name> Run the server
terractl stop <name> <pid> Save and shut down via the console (service only)
terractl service-stop <name> <pid> Save and shut down via the console (service only)
terractl daemon serve Run the daemon (terractld.service)
```
```
Binary file not shown.
+1949
View File
File diff suppressed because it is too large. Load diff
+8 -3
View File
@@ -5,8 +5,8 @@ Priority: optional
Architecture: all
Installed-Size: @INSTALLED_SIZE@
Maintainer: Your Name <you@example.com>
Depends: unzip, systemd
Recommends: curl, libicu76 | libicu74 | libicu72 | libicu70 | libicu-dev
Depends: unzip, systemd, python3, openssl
Recommends: curl, libicu76 | libicu74 | libicu72 | libicu70 | libicu-dev, polkitd
Conflicts: terraria-instance
Replaces: terraria-instance
Suggests: bash-completion
@@ -18,6 +18,11 @@ Description: run multiple Terraria and tModLoader dedicated servers with systemd
Server software is not included. Place the official server zips in
/var/lib/terraria/dist and they are unpacked into each instance on demand.
.
curl and ICU are only needed for tModLoader instances.
.
Hosts can also be managed from other hosts (terractl --host, copy, move)
through the optional terractld daemon.
.
curl and ICU are only needed for tModLoader instances. polkitd lets members
of the terraria group start, stop and restart servers without sudo.
.
This package replaces terraria-instance (the same tool under its old name).
+5
View File
@@ -19,6 +19,11 @@ case "$1" in
for unit in $running; do echo " sudo systemctl restart $unit"; done
fi
fi
# The daemon (if it's on) runs the new version right away; it only serves
# other hosts, so restarting it doesn't affect the game servers
if [ -d /run/systemd/system ]; then
systemctl try-restart terractld.service || true
fi
# Running instances are not restarted; they pick up changes on their next start.
;;
esac
+3
View File
@@ -6,6 +6,9 @@ case "$1" in
# Forget which instances were enabled at boot
rm -f /etc/systemd/system/multi-user.target.wants/terraria@*.service
rm -rf /etc/systemd/system/terraria@.service.d
# The daemon: whether it's on, its certificate and the computers it trusts
rm -f /etc/systemd/system/multi-user.target.wants/terractld.service
rm -rf /etc/systemd/system/terractld.service.d /var/lib/terractl
cat >&2 <<MSG
terraria-server-manager purged. Worlds and instances in /var/lib/terraria and the
'terraria' user were kept. Remove them manually if they are no longer needed:
+1
View File
@@ -7,6 +7,7 @@ set -e
case "$1" in
remove|deconfigure)
if [ -d /run/systemd/system ]; then
systemctl stop terractld.service || true
units=$(systemctl list-units --plain --no-legend --all 'terraria@*.service' | awk '{print $1}')
if [ -n "$units" ]; then
echo "Stopping Terraria instances: $units"
+31
View File
@@ -0,0 +1,31 @@
[Unit]
Description=terractl daemon: lets other hosts manage this host's Terraria servers
Documentation=file://@DOCDIR@/README.md
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
# Listens on TERRACTL_LISTEN from /etc/default/terractl (default: all addresses,
# port 7776). Its certificate and the list of trusted computers are kept in
# /var/lib/terractl. Manage it with: terractl daemon enable|disable|status|trust
ExecStart=@BINDIR@/terractl daemon serve
Restart=on-failure
RestartSec=5
StateDirectory=terractl
StateDirectoryMode=0755
# Runs as root: it creates, deletes, starts and stops instances for trusted
# computers, like sudo terractl does. Everything else is locked down.
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
[Install]
WantedBy=multi-user.target
+1 -1
View File
@@ -18,7 +18,7 @@ ExecStartPre=@BINDIR@/terractl prepare %i
# Execs the vanilla or tModLoader server depending on instance.conf
ExecStart=@BINDIR@/terractl run %i
# Sends "exit" to the console (saves the world) and waits for shutdown
ExecStop=@BINDIR@/terractl stop %i $MAINPID
ExecStop=@BINDIR@/terractl service-stop %i $MAINPID
StandardInput=socket
StandardOutput=journal