Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a3b4b8eb9 | ||
|
|
fc2c08d47b | ||
|
|
8a509f1398 | ||
|
|
7adf5c2999 | ||
|
|
96f7876729 | ||
|
|
ef63609db9 |
No files matched your search
@@ -0,0 +1,246 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Guidance for AI agents (and people) working on this repository.
|
||||||
|
|
||||||
|
## What this is
|
||||||
|
|
||||||
|
`terraria-server-manager` runs any number of vanilla Terraria and tModLoader
|
||||||
|
dedicated servers on one Linux host, each as its own systemd service
|
||||||
|
(`terraria@<name>.service`), managed with the `terractl` command. It ships as a
|
||||||
|
Debian package. Server software is never bundled: official zips go in
|
||||||
|
`/var/lib/terraria/dist` and are unpacked into each instance on demand.
|
||||||
|
|
||||||
|
The user-facing documentation is `docs/guide.md` (installed as the package's
|
||||||
|
README). Read the sections relevant to a change before making it; the guide is
|
||||||
|
the specification of how things behave.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
| Path | What it is |
|
||||||
|
|---|---|
|
||||||
|
| `bin/terractl` | The main tool, in bash. Everything on the local host. |
|
||||||
|
| `lib/terractl-remote` | Python (standard library only, 3.9+): `--host`, `remote`, `daemon`, `copy`, `move`. Its daemon does everything by running `terractl` on its own host. |
|
||||||
|
| `completions/bash/terractl` | Bash completion. |
|
||||||
|
| `systemd/` | `terraria@.service`/`.socket` (one per instance), `terractld.service` (daemon for other hosts), `terraria-backup@.service`/`.timer` (scheduled backups). |
|
||||||
|
| `config/` | `default/terractl` (user overrides, a conffile), `tmpfiles.d`, `sysusers.d`, `polkit` rule, `journald@terraria.conf` (a conffile). |
|
||||||
|
| `packaging/deb/` | `control`, `conffiles`, maintainer scripts (`postinst`, `prerm`, `postrm`). |
|
||||||
|
| `packaging/gitea-release.py` | Publishes a release with that version's CHANGELOG section as notes. |
|
||||||
|
| `.gitea/workflows/release.yaml` | On a `v*` tag: `make deb`, publish the release and the apt repository. |
|
||||||
|
| `tools/windows/export-tmod-mods.ps1` | Helper for players to export their enabled tModLoader mods. |
|
||||||
|
| `docs/guide.md`, `README.md`, `CHANGELOG.md`, `VERSION` | Docs and release metadata. |
|
||||||
|
|
||||||
|
Placeholders `@VERSION@`, `@BINDIR@`, `@DOCDIR@` and `@PKGLIBDIR@` in
|
||||||
|
`bin/terractl` and the unit files are filled in by `make install`/`make deb`.
|
||||||
|
A copy run straight from the repo is a "development copy": it detects the
|
||||||
|
unfilled placeholders and uses `lib/terractl-remote` from the source tree.
|
||||||
|
|
||||||
|
## Build and check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make check # bash -n, sh -n on maintainer scripts, Python syntax, shellcheck, pyflakes
|
||||||
|
make deb # runs check, builds build/terraria-server-manager_<version>_all.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
**`make check` must pass with shellcheck installed** (`apt install shellcheck
|
||||||
|
python3-pyflakes`). The release workflow runs it, so a shellcheck warning blocks
|
||||||
|
a release. Fix warnings rather than disabling them; a `# shellcheck disable=`
|
||||||
|
needs a comment saying why (see the existing ones).
|
||||||
|
|
||||||
|
There is no automated test suite. See [Testing](#testing).
|
||||||
|
|
||||||
|
## How the system fits together
|
||||||
|
|
||||||
|
These invariants run through the whole code base. Keep them.
|
||||||
|
|
||||||
|
- **Servers run as the unprivileged `terraria` user**, sandboxed by
|
||||||
|
`terraria@.service` (`ProtectSystem=strict`, `ProtectHome=yes`, writable only
|
||||||
|
`ReadWritePaths=/var/lib/terraria/instances`, `UMask=0027`).
|
||||||
|
- **Nothing in an instance may end up owned by root.** Commands run as root
|
||||||
|
`chown` what they create (`fix_owner`, `chown -R` after `new`/`import`).
|
||||||
|
`prepare` and `run` started as root switch to `terraria` with `runuser`
|
||||||
|
(re-running `$SELF`, which is resolved before the script's `cd /`).
|
||||||
|
- **Permission model:** root can do everything; members of the `terraria` group
|
||||||
|
can read instances, start/stop/restart servers (polkit rule), use the console
|
||||||
|
(`attach`, `cmd`, `watch`), read the servers' own journal (log namespace
|
||||||
|
`terraria`), add zips to `dist/` and read backups. Group members can read but
|
||||||
|
never change instance files. Every new command must fit this model and be
|
||||||
|
added to the permissions table in the guide.
|
||||||
|
- **`instance.conf` is sourced by bash.** Anything written into it (e.g. `ZIP=`)
|
||||||
|
must be validated first (`safe_zip_name`). Instance names are validated by
|
||||||
|
`valid_name` and can't start with `.`.
|
||||||
|
- **One server per instance at a time:** the lock on `<instance>/.lock` (fd 9)
|
||||||
|
is held for the server's lifetime. Commands that replace `server/` or delete
|
||||||
|
things under a running server must check `instance_running`.
|
||||||
|
- **`server/` is replaceable, `data/` is precious.** Unpacking happens in a temp
|
||||||
|
folder and only replaces `server/` once complete; `server/.installed` records
|
||||||
|
`<type> <zip>`. Nothing may delete or rewrite worlds without a backup.
|
||||||
|
- **Changes that can lose data back up first** and leave things as they were on
|
||||||
|
failure (see `cmd_upgrade`: validate everything, stop, back up, change, roll
|
||||||
|
back the pin and restart on failure).
|
||||||
|
- **Files outside `/var/lib/terraria`:** only the documented drop-ins in
|
||||||
|
`/etc/systemd/system` (`terraria-backup@<name>.{timer,service}.d/terractl.conf`
|
||||||
|
from `backup enable`, `<unit>.d/terractl-locations.conf` from `doctor --fix`).
|
||||||
|
`postrm` purge removes them. Never overwrite admin files or unit files.
|
||||||
|
- **The daemon (`terractld`) runs `terractl` as root** with `TERRACTL_VIA` set.
|
||||||
|
Only commands listed in `EXEC_COMMANDS` in `lib/terractl-remote` can be run
|
||||||
|
remotely. It runs with `ProtectSystem=full` and `ProtectHome=true`, plus write
|
||||||
|
access to `/etc/systemd/system` only.
|
||||||
|
- **Locations are configurable** (`TERRARIA_BASE`, `TERRARIA_DIST`,
|
||||||
|
`TERRARIA_BACKUPS` in `/etc/default/terractl`), but unit files can't follow
|
||||||
|
them; `location_warnings` and `doctor` detect mismatches. Never hard-code
|
||||||
|
`/var/lib/terraria/...` in `bin/terractl`: use `$BASE_DIR`, `$DIST_DIR`,
|
||||||
|
`$BACKUP_DIR`.
|
||||||
|
|
||||||
|
## Code conventions
|
||||||
|
|
||||||
|
### bash (`bin/terractl`)
|
||||||
|
|
||||||
|
- `set -euo pipefail`. Be careful with pipelines that can legitimately fail
|
||||||
|
(`grep` with no match, `grep -q` closing a pipe early): capture to a variable
|
||||||
|
first, or add `|| true`.
|
||||||
|
- Errors: `die "message"` (exits 1); notes: `log "message"` (stderr). Both
|
||||||
|
prefix `terractl:`. Normal output goes to stdout.
|
||||||
|
- **Messages say what to do next.** When suggesting a command, use
|
||||||
|
`hint root|group <command> [args]`, which adds `sudo` only when needed and
|
||||||
|
shows `terractl --host <remote> ...` when run through the daemon.
|
||||||
|
- Commands are `cmd_<name>` functions; sub-actions are `<command>_<action>`
|
||||||
|
(e.g. `backup_enable`). Helpers are lower-case with underscores. Sections are
|
||||||
|
separated by `# ---- name ----` banner comments.
|
||||||
|
- Every non-obvious function gets a short comment above it saying what it does
|
||||||
|
and why; match the existing density (comments explain intent and edge cases,
|
||||||
|
not syntax).
|
||||||
|
- Option parsing: a `while`/`case` loop, `die "unknown option '$1'"` for
|
||||||
|
anything unexpected, and a `usage: terractl ...` message for wrong arguments.
|
||||||
|
- Root checks: `[[ $EUID -eq 0 ]] || die "'<command>' needs root: sudo terractl ..."`.
|
||||||
|
- `local` for all function variables. Globals are upper-case.
|
||||||
|
- `EXTRACT_TMP` is the single temp-folder slot that `cleanup` (the EXIT trap)
|
||||||
|
removes. Code that must survive a `die` in a helper runs it in a subshell
|
||||||
|
with its own `trap cleanup EXIT` (see `make_backup`).
|
||||||
|
|
||||||
|
### Python (`lib/terractl-remote`)
|
||||||
|
|
||||||
|
- Standard library only, Python 3.9+. `make check` runs pyflakes on it.
|
||||||
|
- It never touches instances directly: it runs `terractl` (locally, or via the
|
||||||
|
daemon on the other host) so checks and ownership stay in one place.
|
||||||
|
|
||||||
|
### Shell in packaging
|
||||||
|
|
||||||
|
- Maintainer scripts are POSIX `sh`, idempotent, and must never fail an
|
||||||
|
install or removal because systemd isn't running (`[ -d /run/systemd/system ]`
|
||||||
|
guards, `|| true` on best-effort steps).
|
||||||
|
|
||||||
|
## Adding or changing a command: checklist
|
||||||
|
|
||||||
|
1. `bin/terractl`: the `cmd_*` function, the `usage` text, and the dispatch at
|
||||||
|
the bottom. Commands that don't take an instance name first (like `list`,
|
||||||
|
`doctor`, `backup`, `fetch`) are dispatched in the first `case`, before
|
||||||
|
`valid_name "$1"`.
|
||||||
|
2. `lib/terractl-remote`: add it to `EXEC_COMMANDS` if it should work with
|
||||||
|
`--host` (and a handler in `run_on_remote` if it moves files to or from the
|
||||||
|
caller's computer, like `export`, `import` and `mods add`).
|
||||||
|
3. `completions/bash/terractl`: the command list (`commands=`), and its
|
||||||
|
arguments/options. Local-only commands go in the `else` branch's list.
|
||||||
|
Instance names on a remote come from `_terractl_remote_instances`.
|
||||||
|
4. `docs/guide.md`:
|
||||||
|
- the section describing it (with examples using the `calamity` instance);
|
||||||
|
- the **Permissions** table;
|
||||||
|
- the **Tab completion** list, if completion changed;
|
||||||
|
- the **Command reference**;
|
||||||
|
- **Troubleshooting** and the `doctor` description, if relevant;
|
||||||
|
- **Components** and **Directory layout** for new files or folders.
|
||||||
|
5. `README.md` features list, for user-visible features.
|
||||||
|
6. `CHANGELOG.md`: an entry in the newest section (see [Changelog](#changelog)).
|
||||||
|
7. New installed files: `Makefile` `install` **and** `uninstall`, unit files
|
||||||
|
through `$(SUBST)` if they contain placeholders, `packaging/deb/postrm`
|
||||||
|
(purge) for anything created at runtime, and `conffiles` for files in
|
||||||
|
`/etc` that users edit.
|
||||||
|
8. If it affects what can go wrong on a host, consider a check in `doctor`
|
||||||
|
(`doc_problem warn|FAIL "<what>" "<fix command>" [<function that fixes it>]`;
|
||||||
|
only pass a fix function for fixes that are safe to apply unattended).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Agents usually can't run as root, use systemd, or install the package. The
|
||||||
|
pattern used so far:
|
||||||
|
|
||||||
|
- Make a scratch folder (outside the repo) with `inst/`, `dist/`, `backups/`
|
||||||
|
and point `TERRARIA_BASE`, `TERRARIA_DIST`, `TERRARIA_BACKUPS` at them, and
|
||||||
|
`TERRARIA_USER` at your own user.
|
||||||
|
- Fake root in a **copy** of the script:
|
||||||
|
`sed -e 's/\$EUID/${FAKE_EUID:-$EUID}/g' bin/terractl > scratch/terractl`
|
||||||
|
with `FAKE_EUID=0` exported, and a `runuser` stub on `PATH` that drops to the
|
||||||
|
"service user" with `exec env -u FAKE_EUID "$@"` after the `--`. (Replacing
|
||||||
|
`$EUID` with a literal `0` makes `prepare` re-run itself forever.)
|
||||||
|
- Stub `systemctl` on `PATH` that keeps unit state in files (`is-active`,
|
||||||
|
`is-enabled`, `is-failed`, `start`/`stop`/`enable`/`disable`, and `show -p`
|
||||||
|
for the properties used). Point `SYSTEMD_ETC` at a scratch folder in the copy.
|
||||||
|
- Build test zips with Python's `zipfile` (`zip` may not be installed): vanilla
|
||||||
|
needs `<ver>/Linux/TerrariaServer.bin.x86_64`, tModLoader needs
|
||||||
|
`start-tModLoaderServer.sh`.
|
||||||
|
- `fetch` can be tested against the real sites; downloads are 40-60 MB.
|
||||||
|
- Don't `pkill -f` a pattern that appears in your own command line.
|
||||||
|
- Always run `make check`, and `make deb` when packaging changed. For unit
|
||||||
|
files, run `systemd-analyze verify` on the **built** copies under
|
||||||
|
`build/deb-root` (the sources contain placeholders).
|
||||||
|
|
||||||
|
Say clearly what was tested with stubs and what wasn't (real root, real
|
||||||
|
systemd, real servers); the maintainer tests on a real host before releasing.
|
||||||
|
|
||||||
|
## Documentation style
|
||||||
|
|
||||||
|
- Plain, direct English; short sentences; British spelling where it differs.
|
||||||
|
- Explain what happens and why, then show the command. Examples use the
|
||||||
|
instance `calamity` and the remote `nas`.
|
||||||
|
- Bold lead-in phrases for key points (`**Downgrades need --downgrade.**`).
|
||||||
|
- Command reference and usage text are aligned two-column blocks; keep the
|
||||||
|
columns aligned when adding lines.
|
||||||
|
- Documented behaviour must match the code; update both in the same change.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
`CHANGELOG.md` is for users, and each version's section is published as-is as
|
||||||
|
its release notes on the Gitea release page (`gitea-release.py`). Write it so it
|
||||||
|
reads well there.
|
||||||
|
|
||||||
|
- **Headings:** `## <version> - unreleased` while in progress,
|
||||||
|
`## <version> - <YYYY-MM-DD>` once released. `gitea-release.py` takes the
|
||||||
|
first word after `## ` as the version, so keep exactly that shape.
|
||||||
|
- **Where entries go:** in the newest section if it's unreleased. If the newest
|
||||||
|
section is already released (dated), start `## <next version> - unreleased`
|
||||||
|
at the top **and** bump `VERSION` to match, in the same commit.
|
||||||
|
- **Released sections are history:** don't edit them, except to fix a typo.
|
||||||
|
- **Unreleased changes are edited, not appended to.** If something in the open
|
||||||
|
section changes before release (renamed, moved, fixed), update its existing
|
||||||
|
entry so the section describes the final state, rather than adding a
|
||||||
|
"fixed X" line about something users never had.
|
||||||
|
- **What an entry says:** the command and options as the user types them
|
||||||
|
(`` `sudo terractl backup enable <name> [--every <when>]` ``), what it does
|
||||||
|
and what they'll see, defaults, and anything they must do themselves
|
||||||
|
(restart servers, run a command after upgrading, a new dependency). Full
|
||||||
|
sentences; say "now" for changed behaviour and "before, ..." when the old
|
||||||
|
behaviour could have caught people out.
|
||||||
|
- **Order:** new features first, then changed behaviour, then fixes; the most
|
||||||
|
important first within each. A large feature gets one bullet with
|
||||||
|
sub-bullets for its parts (see `upgrade` and `fetch` in 0.0.12).
|
||||||
|
- **Leave out** refactors, test-only changes, code style and doc rewording,
|
||||||
|
unless the docs gained something users should know about (e.g. a new guide
|
||||||
|
section on a setup that didn't work before).
|
||||||
|
- Every user-visible change gets an entry in the same commit as the change.
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
- `VERSION` holds the version (`0.0.N`) and must match the newest changelog
|
||||||
|
section (see [Changelog](#changelog)).
|
||||||
|
- To release: set the date, commit, then tag **`v<version>`** (annotated,
|
||||||
|
message `terraria-server-manager <version>`). The workflow refuses a tag that
|
||||||
|
doesn't match `VERSION`. Pushing the tag builds and publishes the release and
|
||||||
|
the apt repository, so only push when asked.
|
||||||
|
|
||||||
|
## Git
|
||||||
|
|
||||||
|
- Commit subjects are short, lower-case and imperative-ish
|
||||||
|
(`add upgrade command; reinstall keeps the same version`), with a body
|
||||||
|
listing the changes for anything non-trivial.
|
||||||
|
- Everything is LF (`.gitattributes`); `build/`, `*.deb` and `__pycache__/` are
|
||||||
|
ignored.
|
||||||
|
- Commit, tag or push only when asked.
|
||||||
+26
-1
@@ -1,7 +1,32 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Unreleased
|
## 0.0.12 - 2026-10-02
|
||||||
|
|
||||||
|
- New `fetch` command to download server zips into `dist/`, named with their version:
|
||||||
|
- `terractl fetch tmodloader [<version>]` downloads from tModLoader's GitHub releases (default: the latest stable) and checks it against the SHA-256 checksum GitHub publishes. `terractl fetch vanilla [<version>]` downloads from terraria.org (default: its newest; older versions by number, `1449` or `1.4.4.9`).
|
||||||
|
- `--list` shows the versions available (tModLoader: stable and preview), and which are already downloaded. `--upgrade <name>` switches an instance to the zip with `upgrade` right after.
|
||||||
|
- Every zip is checked (complete, and the right kind) before it's put in `dist/`; zips already there aren't downloaded again. If a site doesn't have the version or can't be reached, it says so and where to download it by hand.
|
||||||
|
- Members of the `terraria` group can use it without `sudo` (except `--upgrade`). Needs `curl`.
|
||||||
|
- Scheduled backups, with a systemd timer per instance (`terraria-backup@<name>.timer`):
|
||||||
|
- `sudo terractl backup enable <name> [--every <when>] [--keep <number>]` (default: daily, keeping the newest 14). `--every` takes `hourly`, `daily`, `weekly`, a time such as `04:30`, or any systemd calendar expression. Missed backups are made after the machine starts again.
|
||||||
|
- `backup now <name>` makes one immediately, `backup disable <name>` stops the schedule (existing backups are kept), and `backup list [<name>]` shows schedules, the last and next run, and the backups.
|
||||||
|
- Each backup saves the world first if the server is running (as `export` does) and goes in `/var/lib/terraria/backups/<name>/` (`TERRARIA_BACKUPS` in `/etc/default/terractl`), readable by the `terraria` group. Only scheduled backups are pruned.
|
||||||
|
- `rename` takes the schedule along, `delete` turns it off, and purging the package removes the schedules (not the backups).
|
||||||
|
- New `doctor` command: `terractl doctor [<name>] [--fix]` checks the system (user, programs including ICU and curl, folders, zips, the systemd units and old copies in `/etc` that replace them, sandbox access to moved folders, journal access, polkit, the daemon, leftovers of deleted instances) and each instance (file ownership and modes, its server zip, paths in `serverconfig.txt`, missing mods, shared ports, failed starts and backups). Each problem comes with the command that fixes it. `--fix` applies the safe fixes: `fix-perms`, folder and zip permissions, journal access, the `ReadWritePaths=` drop-in for moved folders, and boot links or backup schedules of instances that are gone. It exits with status 1 if a problem remains.
|
||||||
|
- The daemon (`terractld.service`) may now write to `/etc/systemd/system`, so `backup enable` and `doctor --fix` also work through `--host`.
|
||||||
|
- Warnings about moved folders now also suggest `terractl doctor --fix`.
|
||||||
|
- New `upgrade` command to change an instance's server version, up or down:
|
||||||
|
- `terractl upgrade <name>` lists the zips in `dist/` for its type, marking the installed, pinned and newest ones. The `terraria` group can use it without `sudo`.
|
||||||
|
- `sudo terractl upgrade <name> <zip>|latest` checks the zip first (right type, readable by `terraria`, contains the server, passes `unzip -t`), stops the server (saving the world), backs up its worlds and configs to `/var/lib/terraria/backups/<name>/`, pins `ZIP=`, unpacks the zip right away and starts the server again if it was running. If unpacking fails, the instance is left as it was. `--no-backup` skips the backup.
|
||||||
|
- Going to an older version needs `--downgrade`, because worlds saved by a newer version can't be opened by an older one.
|
||||||
|
- Bash completion offers the instance's zips. Works with `--host`.
|
||||||
|
- `reinstall` now always unpacks the **same** version again. Before, an instance with a blank `ZIP=` picked the newest zip in `dist/`, so `reinstall` could silently upgrade it. It now pins the zip the instance runs first, and refuses if that zip is gone from `dist/`. It also keeps the record of the installed version (`server/.installed`) until the new unpack is done.
|
||||||
|
- `list`, `new`, `import` and `enable` warn when `TERRARIA_BASE` or `TERRARIA_DIST` is somewhere the services' sandbox doesn't allow (the instances folder not in `ReadWritePaths=`, or either folder under `/home`), and print the exact drop-in to add. The daemon (`terractld.service`) is checked too when it's on. A server that can't create its folder now says why in its log, instead of only "Read-only file system".
|
||||||
|
- The guide's "Changing locations" now covers bind mounts (the simplest way to use another disk) as well as drop-ins, and a new section explains keeping instances or zips under `/home`.
|
||||||
|
- `journal-access` is offered by tab completion and listed in the guide's command reference.
|
||||||
|
- `upgrade`, and `prepare`/`run` started as root, check first that the `terraria` user can run the terractl being used. A copy under `/root` is refused with an explanation, before `upgrade` stops the server, instead of failing with `env: Permission denied`.
|
||||||
|
- `terractl` run by a relative path (e.g. `sudo ./bin/terractl prepare <name>` from a source tree) no longer fails to switch to the `terraria` user.
|
||||||
|
- New `fix-perms` command: `sudo terractl fix-perms <name>` gives every file of an instance back to the `terraria` user and reapplies the usual modes (group can read, others nothing; server binaries executable), e.g. after copying worlds in as root. Symlinks are not followed. Works with `--host` too.
|
||||||
- `list` warns about zips in `dist/` that the `terraria` user can't read (e.g. copied in with `scp -p` from a private file), and `info` lists them as `unreadable_zip=`. A server that fails to install because of one now says how to fix it (`sudo chmod 644 <zip>`) instead of just "cannot read", and a missing zip is reported as not found.
|
- `list` warns about zips in `dist/` that the `terraria` user can't read (e.g. copied in with `scp -p` from a private file), and `info` lists them as `unreadable_zip=`. A server that fails to install because of one now says how to fix it (`sudo chmod 644 <zip>`) instead of just "cannot read", and a missing zip is reported as not found.
|
||||||
|
|
||||||
## 0.0.11 - 2026-09-30
|
## 0.0.11 - 2026-09-30
|
||||||
|
|||||||
@@ -64,7 +64,10 @@ install:
|
|||||||
install -m 755 lib/terractl-remote $(DESTDIR)$(PKGLIBDIR)/terractl-remote
|
install -m 755 lib/terractl-remote $(DESTDIR)$(PKGLIBDIR)/terractl-remote
|
||||||
$(SUBST) systemd/terraria@.service > $(DESTDIR)$(UNITDIR)/terraria@.service
|
$(SUBST) systemd/terraria@.service > $(DESTDIR)$(UNITDIR)/terraria@.service
|
||||||
$(SUBST) systemd/terractld.service > $(DESTDIR)$(UNITDIR)/terractld.service
|
$(SUBST) systemd/terractld.service > $(DESTDIR)$(UNITDIR)/terractld.service
|
||||||
chmod 644 $(DESTDIR)$(UNITDIR)/terraria@.service $(DESTDIR)$(UNITDIR)/terractld.service
|
$(SUBST) systemd/terraria-backup@.service > $(DESTDIR)$(UNITDIR)/terraria-backup@.service
|
||||||
|
$(SUBST) systemd/terraria-backup@.timer > $(DESTDIR)$(UNITDIR)/terraria-backup@.timer
|
||||||
|
chmod 644 $(DESTDIR)$(UNITDIR)/terraria@.service $(DESTDIR)$(UNITDIR)/terractld.service \
|
||||||
|
$(DESTDIR)$(UNITDIR)/terraria-backup@.service $(DESTDIR)$(UNITDIR)/terraria-backup@.timer
|
||||||
install -m 644 systemd/terraria@.socket $(DESTDIR)$(UNITDIR)/
|
install -m 644 systemd/terraria@.socket $(DESTDIR)$(UNITDIR)/
|
||||||
install -m 644 config/sysusers.d/terractl.conf $(DESTDIR)$(SYSUSERSDIR)/
|
install -m 644 config/sysusers.d/terractl.conf $(DESTDIR)$(SYSUSERSDIR)/
|
||||||
install -m 644 config/tmpfiles.d/terractl.conf $(DESTDIR)$(TMPFILESDIR)/
|
install -m 644 config/tmpfiles.d/terractl.conf $(DESTDIR)$(TMPFILESDIR)/
|
||||||
@@ -97,10 +100,14 @@ install:
|
|||||||
uninstall:
|
uninstall:
|
||||||
@if [ -z "$(DESTDIR)" ] && [ -d /run/systemd/system ]; then \
|
@if [ -z "$(DESTDIR)" ] && [ -d /run/systemd/system ]; then \
|
||||||
systemctl disable --now terractld.service 2>/dev/null || true; \
|
systemctl disable --now terractld.service 2>/dev/null || true; \
|
||||||
|
for t in $$(systemctl list-unit-files --plain --no-legend 'terraria-backup@*.timer' | awk '{print $$1}'); do \
|
||||||
|
systemctl disable --now "$$t" 2>/dev/null || true; \
|
||||||
|
done; \
|
||||||
fi
|
fi
|
||||||
rm -f $(DESTDIR)$(BINDIR)/terractl \
|
rm -f $(DESTDIR)$(BINDIR)/terractl \
|
||||||
$(DESTDIR)$(UNITDIR)/terraria@.service $(DESTDIR)$(UNITDIR)/terraria@.socket \
|
$(DESTDIR)$(UNITDIR)/terraria@.service $(DESTDIR)$(UNITDIR)/terraria@.socket \
|
||||||
$(DESTDIR)$(UNITDIR)/terractld.service \
|
$(DESTDIR)$(UNITDIR)/terractld.service \
|
||||||
|
$(DESTDIR)$(UNITDIR)/terraria-backup@.service $(DESTDIR)$(UNITDIR)/terraria-backup@.timer \
|
||||||
$(DESTDIR)$(SYSUSERSDIR)/terractl.conf $(DESTDIR)$(TMPFILESDIR)/terractl.conf \
|
$(DESTDIR)$(SYSUSERSDIR)/terractl.conf $(DESTDIR)$(TMPFILESDIR)/terractl.conf \
|
||||||
$(DESTDIR)$(COMPLETIONDIR)/terractl $(DESTDIR)$(POLKITDIR)/50-terractl.rules
|
$(DESTDIR)$(COMPLETIONDIR)/terractl $(DESTDIR)$(POLKITDIR)/50-terractl.rules
|
||||||
rm -rf $(DESTDIR)$(DOCDIR) $(DESTDIR)$(DATADIR) $(DESTDIR)$(PKGLIBDIR)
|
rm -rf $(DESTDIR)$(DOCDIR) $(DESTDIR)$(DATADIR) $(DESTDIR)$(PKGLIBDIR)
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ sudo terractl attach calamity
|
|||||||
|
|
||||||
- **Many servers, one host.** Each instance is `terraria@<name>.service`, with its own directory, port, world and mods.
|
- **Many servers, one host.** Each instance is `terraria@<name>.service`, with its own directory, port, world and mods.
|
||||||
- **Vanilla and tModLoader side by side**, chosen per instance.
|
- **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.
|
- **No manual installs.** `terractl fetch` downloads official Terraria and tModLoader server zips (or drop them into `/var/lib/terraria/dist` yourself); they're unpacked on demand, and each instance can be pinned to a specific version. `terractl upgrade` checks a new zip, backs up the worlds and switches version in one step.
|
||||||
- **Safe shutdowns.** Stopping a server saves the world first.
|
- **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; `terractl watch` just shows the output.
|
- **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.
|
- **Mod management.** `terractl mods` installs, enables and removes mods, with correct file ownership.
|
||||||
- **Tab completion** for commands, instance names, mods and console commands.
|
- **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.
|
- **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.
|
||||||
|
- **Scheduled backups.** `terractl backup enable <name>` backs an instance up daily (or on any schedule) with a systemd timer, saving the world first and keeping the newest 14.
|
||||||
|
- **Self-check.** `terractl doctor` checks the whole setup, explains what's wrong with the command to fix it, and `--fix` fixes what it safely can.
|
||||||
- **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.
|
- **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.
|
- **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 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.
|
- **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.
|
||||||
|
|||||||
+1220
-11
File diff suppressed because it is too large.
Load diff
+71
-10
@@ -4,16 +4,36 @@
|
|||||||
# Installed to <prefix>/share/bash-completion/completions/terractl, where the
|
# Installed to <prefix>/share/bash-completion/completions/terractl, where the
|
||||||
# bash-completion package loads it automatically. Also works with 'sudo terractl'.
|
# bash-completion package loads it automatically. Also works with 'sudo terractl'.
|
||||||
|
|
||||||
# Instances directory: default, or TERRARIA_BASE from /etc/default/terractl.
|
# A setting from /etc/default/terractl, or its default: _terractl_setting <name> <default>.
|
||||||
# The file is parsed rather than sourced, so nothing runs in your shell.
|
# The file is parsed rather than sourced, so nothing runs in your shell.
|
||||||
_terractl_base() {
|
_terractl_setting() {
|
||||||
local base=/var/lib/terraria/instances value
|
local result=$2 value
|
||||||
if [[ -r /etc/default/terractl ]]; then
|
if [[ -r /etc/default/terractl ]]; then
|
||||||
value=$(sed -n 's/^[[:space:]]*TERRARIA_BASE=//p' /etc/default/terractl | tail -n 1)
|
value=$(sed -n "s/^[[:space:]]*$1=//p" /etc/default/terractl | tail -n 1)
|
||||||
value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
|
value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
|
||||||
if [[ -n $value ]]; then base=$value; fi
|
if [[ -n $value ]]; then result=$value; fi
|
||||||
fi
|
fi
|
||||||
printf '%s\n' "$base"
|
printf '%s\n' "$result"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Instances directory
|
||||||
|
_terractl_base() { _terractl_setting TERRARIA_BASE /var/lib/terraria/instances; }
|
||||||
|
|
||||||
|
# Server zips an instance can use, by its TYPE in instance.conf (all zips if
|
||||||
|
# that can't be read). On a remote, all of its zips, from its daemon.
|
||||||
|
_terractl_zips() {
|
||||||
|
if [[ -n $2 ]]; then
|
||||||
|
timeout 5 terractl --host "$2" info 2>/dev/null </dev/null | sed -n 's/^dist_zip=//p'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local dist type pattern='*.zip' f
|
||||||
|
dist=$(_terractl_setting TERRARIA_DIST /var/lib/terraria/dist)
|
||||||
|
type=$(sed -n 's/^TYPE=\([a-z]*\).*/\1/p' "$(_terractl_base)/$1/instance.conf" 2>/dev/null | tail -n 1)
|
||||||
|
case $type in
|
||||||
|
vanilla) pattern='terraria-server-*.zip' ;;
|
||||||
|
tmodloader) pattern='tModLoader*.zip' ;;
|
||||||
|
esac
|
||||||
|
while IFS= read -r f; do printf '%s\n' "${f##*/}"; done < <(compgen -G "$dist/$pattern")
|
||||||
}
|
}
|
||||||
|
|
||||||
# Instance names. Reading the directory needs root or the 'terraria' group; if
|
# Instance names. Reading the directory needs root or the 'terraria' group; if
|
||||||
@@ -32,6 +52,11 @@ _terractl_instances() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Instance names here, or on remote <host> if one is given
|
||||||
|
_terractl_any_instances() {
|
||||||
|
if [[ -n $1 ]]; then _terractl_remote_instances "$1"; else _terractl_instances; fi
|
||||||
|
}
|
||||||
|
|
||||||
# Remotes this user added with 'terractl remote add'
|
# Remotes this user added with 'terractl remote add'
|
||||||
_terractl_remotes() {
|
_terractl_remotes() {
|
||||||
local f=${TERRACTL_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/terractl}/remotes.json
|
local f=${TERRACTL_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/terractl}/remotes.json
|
||||||
@@ -105,7 +130,7 @@ _terractl() {
|
|||||||
cword=$COMP_CWORD
|
cword=$COMP_CWORD
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local commands="new list info start stop restart enable disable attach watch cmd mods reinstall delete rename export import"
|
local commands="new list info start stop restart enable disable attach watch cmd mods upgrade reinstall fix-perms fetch backup doctor 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"
|
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
|
# terractl --host <remote> <command> ...: the same as on this host, except
|
||||||
@@ -121,7 +146,7 @@ _terractl() {
|
|||||||
cword=$((cword - 2))
|
cword=$((cword - 2))
|
||||||
commands+=" --help --version"
|
commands+=" --help --version"
|
||||||
else
|
else
|
||||||
commands+=" copy move remote daemon prepare run --host --help --version"
|
commands+=" copy move remote daemon prepare run journal-access --host --help --version"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if (( cword == 1 )); then
|
if (( cword == 1 )); then
|
||||||
@@ -129,9 +154,9 @@ _terractl() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local command=${words[1]}
|
local command=${words[1]} prev=${words[cword-1]}
|
||||||
case $command in
|
case $command in
|
||||||
list|help|-h|--help|version|-V|--version)
|
list|journal-access|help|-h|--help|version|-V|--version)
|
||||||
return ;;
|
return ;;
|
||||||
new)
|
new)
|
||||||
# new <new name> [vanilla|tmodloader] [port]: the name is new, so nothing to offer
|
# new <new name> [vanilla|tmodloader] [port]: the name is new, so nothing to offer
|
||||||
@@ -156,6 +181,36 @@ _terractl() {
|
|||||||
_terractl_reply "enable disable status token revoke"
|
_terractl_reply "enable disable status token revoke"
|
||||||
fi
|
fi
|
||||||
return ;;
|
return ;;
|
||||||
|
doctor)
|
||||||
|
# doctor [<name>] [--fix]
|
||||||
|
if [[ $cur == -* ]]; then _terractl_reply "--fix"; else _terractl_reply "$(_terractl_any_instances "$host")"; fi
|
||||||
|
return ;;
|
||||||
|
fetch)
|
||||||
|
# fetch vanilla|tmodloader [<version>|latest] [--list] [--upgrade <name>]
|
||||||
|
if (( cword == 2 )); then
|
||||||
|
_terractl_reply "vanilla tmodloader"
|
||||||
|
elif [[ $prev == --upgrade ]]; then
|
||||||
|
_terractl_reply "$(_terractl_any_instances "$host")"
|
||||||
|
elif [[ $cur == -* ]]; then
|
||||||
|
_terractl_reply "--list --upgrade"
|
||||||
|
else
|
||||||
|
_terractl_reply "latest"
|
||||||
|
fi
|
||||||
|
return ;;
|
||||||
|
backup)
|
||||||
|
# backup enable|disable|now|list <name> [--every <when>] [--keep <number>]
|
||||||
|
if (( cword == 2 )); then
|
||||||
|
_terractl_reply "enable disable now list"
|
||||||
|
elif (( cword == 3 )); then
|
||||||
|
_terractl_reply "$(_terractl_any_instances "$host")"
|
||||||
|
elif [[ ${words[2]} == enable ]]; then
|
||||||
|
case $prev in
|
||||||
|
--every) _terractl_reply "daily weekly hourly monthly" ;;
|
||||||
|
--keep) ;;
|
||||||
|
*) _terractl_reply "--every --keep" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
return ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if (( cword == 2 )); then
|
if (( cword == 2 )); then
|
||||||
@@ -173,6 +228,12 @@ _terractl() {
|
|||||||
if (( cword == 3 )); then _terractl_reply "$console"; fi ;;
|
if (( cword == 3 )); then _terractl_reply "$console"; fi ;;
|
||||||
reinstall)
|
reinstall)
|
||||||
if (( cword == 3 )); then _terractl_reply "--clean"; fi ;;
|
if (( cword == 3 )); then _terractl_reply "--clean"; fi ;;
|
||||||
|
upgrade)
|
||||||
|
if [[ $cur == -* ]]; then
|
||||||
|
_terractl_reply "--downgrade --no-backup"
|
||||||
|
else
|
||||||
|
_terractl_reply "latest $(_terractl_zips "$instance" "$host")"
|
||||||
|
fi ;;
|
||||||
enable|disable)
|
enable|disable)
|
||||||
if (( cword == 3 )); then _terractl_reply "--now"; fi ;;
|
if (( cword == 3 )); then _terractl_reply "--now"; fi ;;
|
||||||
watch)
|
watch)
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
# Overrides for terractl. Uncomment to change.
|
# Overrides for terractl. Uncomment to change.
|
||||||
# Changes apply to new commands and to each instance's next start.
|
# Changes apply to new commands and to each instance's next start.
|
||||||
|
|
||||||
# Where instances live. If you change this, the service must also be allowed
|
# Where instances live. To use another disk, it's simplest to mount it at the
|
||||||
# to write there: sudo systemctl edit terraria@.service
|
# default location instead. If you change this, the service must also be
|
||||||
|
# allowed to write there: sudo systemctl edit terraria@.service
|
||||||
# [Service]
|
# [Service]
|
||||||
# ReadWritePaths=/your/new/path
|
# ReadWritePaths=/your/new/path
|
||||||
|
# Under /home it needs more than that. 'terractl list' warns about what's
|
||||||
|
# missing; see "Changing locations" in the guide.
|
||||||
#TERRARIA_BASE=/var/lib/terraria/instances
|
#TERRARIA_BASE=/var/lib/terraria/instances
|
||||||
|
|
||||||
# Where server zips are read from
|
# Where server zips are read from. Outside /home, no drop-in is needed.
|
||||||
#TERRARIA_DIST=/var/lib/terraria/dist
|
#TERRARIA_DIST=/var/lib/terraria/dist
|
||||||
|
|
||||||
|
# Where 'terractl backup' (and 'upgrade', before switching) puts backups
|
||||||
|
#TERRARIA_BACKUPS=/var/lib/terraria/backups
|
||||||
|
|
||||||
# Seconds to wait for a server to save and exit on stop (keep below 90)
|
# Seconds to wait for a server to save and exit on stop (keep below 90)
|
||||||
#TERRARIA_STOP_TIMEOUT=80
|
#TERRARIA_STOP_TIMEOUT=80
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ d /var/lib/terraria 0750 terraria terraria -
|
|||||||
d /var/lib/terraria/instances 0750 terraria terraria -
|
d /var/lib/terraria/instances 0750 terraria terraria -
|
||||||
# setgid + group-writable: members of the 'terraria' group can upload zips
|
# setgid + group-writable: members of the 'terraria' group can upload zips
|
||||||
d /var/lib/terraria/dist 2775 root terraria -
|
d /var/lib/terraria/dist 2775 root terraria -
|
||||||
|
# Backups ('terractl backup', and 'upgrade' before switching): readable by the
|
||||||
|
# group, like the instances themselves
|
||||||
|
d /var/lib/terraria/backups 2750 root terraria -
|
||||||
|
|
||||||
# The servers' journal (LogNamespace=terraria): readable by the terraria group,
|
# The servers' journal (LogNamespace=terraria): readable by the terraria group,
|
||||||
# including files journald creates later (default ACL). 'A+' only acts on paths
|
# including files journald creates later (default ACL). 'A+' only acts on paths
|
||||||
|
|||||||
+251
-22
@@ -52,7 +52,8 @@ Everything is installed by the `terraria-server-manager` package:
|
|||||||
| `/usr/bin/terractl` | Helper script. The service calls it to set up, run and stop servers; you use it for everyday management. |
|
| `/usr/bin/terractl` | Helper script. The service calls it to set up, run and stop servers; you use it for everyday management. |
|
||||||
| `/usr/lib/terraria-server-manager/terractl-remote` | The part of `terractl` that manages other hosts (`--host`, `remote`, `daemon`, `copy`, `move`), in Python. |
|
| `/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)). |
|
| `/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). |
|
| `/usr/lib/systemd/system/terraria-backup@.timer`, `terraria-backup@.service` | Scheduled backups of an instance. Off until you turn them on with `terractl backup enable` (see [Backups](#backups)). |
|
||||||
|
| `/etc/default/terractl` | Optional overrides (locations of instances, zips and backups, stop timeout, the daemon's address). |
|
||||||
| `/etc/systemd/journald@terraria.conf` | Size and retention of the servers' own journal (see [Logs](#logs)). |
|
| `/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/sysusers.d/terractl.conf` | Defines the `terraria` system user. |
|
||||||
| `/usr/lib/tmpfiles.d/terractl.conf` | Creates `/var/lib/terraria` and its subdirectories. |
|
| `/usr/lib/tmpfiles.d/terractl.conf` | Creates `/var/lib/terraria` and its subdirectories. |
|
||||||
@@ -76,6 +77,7 @@ On stop, `terractl service-stop` sends `exit` through the pipe so the world is s
|
|||||||
├── dist/ Server zips (you add these; never modified)
|
├── dist/ Server zips (you add these; never modified)
|
||||||
│ ├── terraria-server-1458.zip
|
│ ├── terraria-server-1458.zip
|
||||||
│ └── tModLoader-v2026.07.3.0.zip
|
│ └── tModLoader-v2026.07.3.0.zip
|
||||||
|
├── backups/<name>/ Backups from 'terractl backup' and 'upgrade' (group-readable)
|
||||||
└── instances/
|
└── instances/
|
||||||
├── <name>/ One directory per instance
|
├── <name>/ One directory per instance
|
||||||
│ ├── instance.conf Which server type and zip to use
|
│ ├── instance.conf Which server type and zip to use
|
||||||
@@ -198,16 +200,86 @@ The version comes from the `VERSION` file. Set `Maintainer:` in `packaging/deb/c
|
|||||||
|
|
||||||
### Changing locations
|
### Changing locations
|
||||||
|
|
||||||
`/etc/default/terractl` can move the instances or zips directory. If you move the instances directory, the service also needs permission to write to the new location:
|
The servers run in a systemd sandbox: they can write only to `/var/lib/terraria/instances`, they can read but not change the rest of the system, and they can't see `/home` at all. These rules are in the unit files, which can't read `/etc/default/terractl`. So if the instances or zips go somewhere else, the services must be told as well.
|
||||||
|
|
||||||
|
**The simplest way: mount the storage at the usual place.** To keep instances on another disk, mount it (or a folder on it) at `/var/lib/terraria/instances`, and change no settings at all. For a folder on a disk that's already mounted, add a bind mount to `/etc/fstab`:
|
||||||
|
|
||||||
|
```
|
||||||
|
/srv/bigdisk/terraria /var/lib/terraria/instances none bind 0 0
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl stop 'terraria@*' # if servers are running
|
||||||
|
sudo cp -a /var/lib/terraria/instances/. /srv/bigdisk/terraria/ # copy existing instances over
|
||||||
|
sudo mount /var/lib/terraria/instances
|
||||||
|
sudo chown terraria:terraria /var/lib/terraria/instances
|
||||||
|
sudo chmod 0750 /var/lib/terraria/instances
|
||||||
|
```
|
||||||
|
|
||||||
|
The old copies stay hidden underneath the mount. Once everything works, you can free that space: `sudo umount /var/lib/terraria/instances`, delete the old instance folders, and `sudo mount /var/lib/terraria/instances` again.
|
||||||
|
|
||||||
|
The same works for `dist/`. Use a mount, not a symbolic link: systemd follows a link to where it really points, and that place isn't covered by the sandbox rules.
|
||||||
|
|
||||||
|
**Or change the setting, and add a drop-in.** Set `TERRARIA_BASE` and/or `TERRARIA_DIST` in `/etc/default/terractl`, create the folders with the same owners and modes as the default ones, and let the servers write to the new instances folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo install -d -o terraria -g terraria -m 0750 /srv/terraria/instances
|
||||||
|
sudo install -d -o root -g terraria -m 2775 /srv/terraria/dist
|
||||||
|
sudo systemctl edit terraria@.service
|
||||||
|
```
|
||||||
|
```ini
|
||||||
|
[Service]
|
||||||
|
ReadWritePaths=/srv/terraria/instances
|
||||||
|
```
|
||||||
|
|
||||||
|
A zips folder outside `/home` needs no drop-in, because the servers only read it.
|
||||||
|
|
||||||
|
`terractl list`, `new`, `import` and `enable` check that the services can reach both folders, and print the exact drop-in to add if they can't. A server that can't create its folder says so in its log, with the same advice. If the daemon for other hosts (`terractld.service`) is on, it's checked too; it only needs a drop-in for folders under `/home` (see below).
|
||||||
|
|
||||||
|
### Instances or zips under `/home`
|
||||||
|
|
||||||
|
This works, but needs more changes, because the services are set up to never touch home folders (`ProtectHome=yes`). That hides `/home`, `/root` and `/run/user` from them completely. Prefer a folder outside `/home`, or a bind mount (above), if you can.
|
||||||
|
|
||||||
|
If they must be under `/home`:
|
||||||
|
|
||||||
|
1. **Let the services see `/home`, read-only, and write to the instances folder.** For the servers:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl edit terraria@.service
|
sudo systemctl edit terraria@.service
|
||||||
```
|
```
|
||||||
```ini
|
```ini
|
||||||
[Service]
|
[Service]
|
||||||
ReadWritePaths=/your/new/path
|
ProtectHome=read-only
|
||||||
|
ReadWritePaths=/home/you/terraria/instances
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If the daemon for other hosts is on, it also needs to write to the zips folder, because copying an instance here can bring its server zip along:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl edit terractld.service
|
||||||
|
```
|
||||||
|
```ini
|
||||||
|
[Service]
|
||||||
|
ProtectHome=read-only
|
||||||
|
ReadWritePaths=/home/you/terraria/instances /home/you/terraria/dist
|
||||||
|
```
|
||||||
|
|
||||||
|
`ProtectHome=read-only` lets the servers see all home folders. Normal file permissions still apply, so as the `terraria` user they can only read what other users can.
|
||||||
|
|
||||||
|
2. **Let the `terraria` user into the folders above.** On Ubuntu (21.04 and later) and some other systems, home folders are private (mode `750`), so the `terraria` user can't even pass through `/home/you`. Give it just that, without letting it list or read anything there:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo setfacl -m u:terraria:x /home/you
|
||||||
|
```
|
||||||
|
|
||||||
|
Do the same for every folder between `/home/you` and the instances or zips folder that isn't open to others. `sudo -u terraria ls /home/you/terraria/instances` tells you whether it worked.
|
||||||
|
|
||||||
|
3. **Create the folders** with the right owners and modes, and set `TERRARIA_BASE` and/or `TERRARIA_DIST` in `/etc/default/terractl`, as above.
|
||||||
|
|
||||||
|
4. **Restart running servers** so they pick up the drop-in: `sudo terractl restart <name>`. If the daemon is on: `sudo systemctl restart terractld`.
|
||||||
|
|
||||||
|
`terractl list` stops warning once the drop-ins match. Members of the `terraria` group also need to get through `/home/you` to use `list`, `info` and tab completion without `sudo`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Migrating from the manual install
|
## Migrating from the manual install
|
||||||
@@ -255,6 +327,28 @@ The existing `terraria` user keeps `/opt/terraria` as its home directory. That's
|
|||||||
|
|
||||||
## Server zips
|
## Server zips
|
||||||
|
|
||||||
|
Servers are unpacked from the official zips in `/var/lib/terraria/dist`. The easiest way to get them there is `fetch`, which downloads them and names them with their version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terractl fetch tmodloader # the latest stable tModLoader
|
||||||
|
terractl fetch tmodloader 2026.07.3.0 # a specific version, e.g. the one your players have
|
||||||
|
terractl fetch vanilla # the latest Terraria server
|
||||||
|
terractl fetch vanilla 1449 # a specific version (1449 or 1.4.4.9)
|
||||||
|
terractl fetch tmodloader --list # recent tModLoader versions, stable and preview
|
||||||
|
```
|
||||||
|
|
||||||
|
tModLoader comes from its GitHub releases, and its download is checked against the checksum GitHub publishes. Vanilla comes from terraria.org, which only lists its newest version, but older ones can be fetched by number. Every zip is checked (complete, and the right kind) before it's put in `dist/`. Zips already there aren't downloaded again.
|
||||||
|
|
||||||
|
`--upgrade <name>` also switches an instance to the zip it just fetched, with `upgrade` (see [Upgrading](#upgrading-or-downgrading-an-instance)):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo terractl fetch tmodloader --upgrade calamity
|
||||||
|
```
|
||||||
|
|
||||||
|
`fetch` needs `curl` and internet access. Members of the `terraria` group can use it without `sudo` (except `--upgrade`). If a site changes its download address and `fetch` stops working, it says so; download the zip by hand as below.
|
||||||
|
|
||||||
|
### Adding zips by hand
|
||||||
|
|
||||||
Put the official downloads in `/var/lib/terraria/dist`:
|
Put the official downloads in `/var/lib/terraria/dist`:
|
||||||
|
|
||||||
| Type | File name pattern | Where to get it |
|
| Type | File name pattern | Where to get it |
|
||||||
@@ -468,7 +562,7 @@ EXTRA_ARGS=() # extra server arguments, as a bash array
|
|||||||
| Setting | Effect |
|
| Setting | Effect |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `TYPE` | Which server software runs. Changing it re-unpacks `server/` from the other kind of zip on the next start. `data/` and `serverconfig.txt` are kept. |
|
| `TYPE` | Which server software runs. Changing it re-unpacks `server/` from the other kind of zip on the next start. `data/` and `serverconfig.txt` are kept. |
|
||||||
| `ZIP` | Pins a specific zip from `/var/lib/terraria/dist` (a file name, or an absolute path). Changing it re-unpacks `server/` on the next start. If left blank, the newest matching zip is used at first install and **never upgraded automatically**. |
|
| `ZIP` | Pins a specific zip from `/var/lib/terraria/dist` (a file name, or an absolute path). Changing it re-unpacks `server/` on the next start; `terractl upgrade` sets it for you (see [Upgrading](#upgrading-or-downgrading-an-instance)). If left blank, the newest matching zip is used at first install and **never upgraded automatically**. |
|
||||||
| `EXTRA_ARGS` | Extra command-line arguments, e.g. `EXTRA_ARGS=(-disableannouncementbox)`. |
|
| `EXTRA_ARGS` | Extra command-line arguments, e.g. `EXTRA_ARGS=(-disableannouncementbox)`. |
|
||||||
|
|
||||||
This file also marks the directory as an instance: `list` ignores directories without one, and `delete` refuses to remove them.
|
This file also marks the directory as an instance: `list` ignores directories without one, and `delete` refuses to remove them.
|
||||||
@@ -563,17 +657,59 @@ Stable tModLoader and the latest vanilla Terraria can be based on different Terr
|
|||||||
|
|
||||||
### Upgrading (or downgrading) an instance
|
### Upgrading (or downgrading) an instance
|
||||||
|
|
||||||
1. Put the new zip in `/var/lib/terraria/dist`, with the version in its name.
|
Get the new zip with `terractl fetch` (or put it in `/var/lib/terraria/dist` by hand, with the version in its name; see [Server zips](#server-zips)). To see which zips an instance can use and which one it runs:
|
||||||
2. Set `ZIP=` to that file name in the instance's `instance.conf`.
|
|
||||||
3. Restart the instance:
|
|
||||||
|
|
||||||
```bash
|
```
|
||||||
sudo systemctl restart terraria@calamity
|
$ terractl upgrade calamity
|
||||||
|
'calamity' (tmodloader) runs: tModLoader-v2026.06.1.0.zip
|
||||||
|
ZIP NOTE
|
||||||
|
tModLoader-v2026.06.1.0.zip installed, pinned
|
||||||
|
tModLoader-v2026.07.3.0.zip newest
|
||||||
|
Change version with: sudo terractl upgrade calamity <zip>|latest
|
||||||
```
|
```
|
||||||
|
|
||||||
The helper sees that the zip has changed and re-unpacks `server/`. Nothing in `data/` is touched. To move back, set `ZIP=` to the old file.
|
Then switch to it, by name or with `latest` (the newest zip of the instance's type):
|
||||||
|
|
||||||
**Pin `ZIP=` on modded instances.** Pinned instances never change version by accident, and you can see at a glance which version each one is on. New tModLoader versions can break older mods, so only upgrade a modded server once its mods support the new version.
|
```bash
|
||||||
|
sudo terractl upgrade calamity tModLoader-v2026.07.3.0.zip
|
||||||
|
sudo terractl upgrade calamity latest
|
||||||
|
```
|
||||||
|
|
||||||
|
`upgrade` applies the change right away, in this order:
|
||||||
|
|
||||||
|
1. **Checks the zip** before changing anything: it must be in `dist/`, match the instance's type, be readable by the `terraria` user, contain the server, and pass `unzip -t`. A damaged or half-downloaded zip is refused here, not on the next start.
|
||||||
|
2. **Stops the server** if it's running. Stopping saves the world.
|
||||||
|
3. **Backs up** the worlds and configs (not `server/`) to `/var/lib/terraria/backups/<name>/<name>-<date>-<time>-before-<old zip>.tar.gz`. Use `--no-backup` to skip this. These backups are never deleted automatically, not even by scheduled backups' `--keep`.
|
||||||
|
4. **Pins `ZIP=`** in `instance.conf` to the new zip, so the version can't change by accident afterwards.
|
||||||
|
5. **Unpacks the new zip** into `server/`. For tModLoader, the .NET runtime and logs are kept, as always. If this fails, the old pin is put back and the old `server/` is still in place, so the instance is exactly as it was.
|
||||||
|
6. **Starts the server again** if it was running.
|
||||||
|
|
||||||
|
Nothing in `data/` is changed by `upgrade` itself. But once the new version saves a world, older versions can't open it.
|
||||||
|
|
||||||
|
**Downgrades need `--downgrade`.** `upgrade` compares the version in the zip names, and refuses to go to an older one without it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo terractl upgrade calamity tModLoader-v2026.06.1.0.zip --downgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
Downgrading only changes the server software. If worlds have already been saved by the newer version, restore them from the backup as well (see [Undoing an upgrade](#undoing-an-upgrade)).
|
||||||
|
|
||||||
|
**Pin `ZIP=` on modded instances.** `upgrade` always pins, and pinned instances never change version by accident. New tModLoader versions can break older mods, so only upgrade a modded server once its mods support the new version. After upgrading, check that the mods load with `terractl watch calamity`.
|
||||||
|
|
||||||
|
You can still change version by hand: set `ZIP=` in `instance.conf` and restart. That skips the checks and the backup, and a bad zip only shows up when the server fails to start.
|
||||||
|
|
||||||
|
### Undoing an upgrade
|
||||||
|
|
||||||
|
To go back to the old version *and* the worlds as they were before the upgrade, replace the instance with the backup `upgrade` made. The backup is in the same format as `export`, so `import` restores it, pinned to the version it was taken with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terractl backup list calamity
|
||||||
|
sudo terractl delete calamity -y # keeps its own backup in .deleted/, just in case
|
||||||
|
sudo terractl import calamity /var/lib/terraria/backups/calamity/calamity-20261002-213553-before-tModLoader-v2026.06.1.0.tar.gz
|
||||||
|
sudo terractl enable calamity --now
|
||||||
|
```
|
||||||
|
|
||||||
|
Anything played since the upgrade is lost this way. The backups are never deleted automatically; remove old ones with `sudo rm`.
|
||||||
|
|
||||||
### Re-unpacking the same version
|
### Re-unpacking the same version
|
||||||
|
|
||||||
@@ -581,14 +717,16 @@ If `server/` gets damaged or you want a clean copy:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo terractl reinstall calamity
|
sudo terractl reinstall calamity
|
||||||
sudo systemctl restart terraria@calamity
|
sudo terractl restart calamity
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`reinstall` unpacks the **same** zip again on the next start; it never changes version. If `ZIP=` is blank, it first pins the zip the instance runs, because otherwise the next start would pick the newest zip in `dist/`. If that zip is no longer in `dist/`, it says so and changes nothing: put the zip back, or use `upgrade`.
|
||||||
|
|
||||||
For tModLoader, the downloaded .NET runtime and `tModLoader-Logs/` are kept. If you suspect .NET itself is damaged, use `--clean` to delete them too, so .NET is downloaded again on the next start:
|
For tModLoader, the downloaded .NET runtime and `tModLoader-Logs/` are kept. If you suspect .NET itself is damaged, use `--clean` to delete them too, so .NET is downloaded again on the next start:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo terractl reinstall calamity --clean
|
sudo terractl reinstall calamity --clean
|
||||||
sudo systemctl restart terraria@calamity
|
sudo terractl restart calamity
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -777,19 +915,42 @@ On this host, copying *from* it works for members of the `terraria` group. Copyi
|
|||||||
- **Revoking.** `sudo terractl daemon status` shows the trusted computers, and `sudo terractl daemon revoke laptop` takes one's access away immediately.
|
- **Revoking.** `sudo terractl daemon status` shows the trusted computers, and `sudo terractl daemon revoke 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.
|
- **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.
|
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. The one exception is `/etc/systemd/system`, where `backup enable` and `doctor --fix` write their drop-ins. 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
|
## 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`):
|
Everything worth keeping is in `data/`, `instance.conf` and `serverconfig.txt`, which is exactly what a backup saves. `terractl backup` makes them on a schedule, and keeps a set number:
|
||||||
|
|
||||||
```cron
|
```bash
|
||||||
30 4 * * * /usr/bin/terractl export calamity /var/backups/terraria/calamity-$(date +\%F).tar.gz && find /var/backups/terraria -name 'calamity-*.tar.gz' -mtime +14 -delete
|
sudo terractl backup enable calamity # daily, keeping the newest 14
|
||||||
|
sudo terractl backup enable calamity --every 04:30 --keep 30 # every day at 4:30, keep 30
|
||||||
|
sudo terractl backup enable calamity --every weekly --keep 0 # weekly, keep them all
|
||||||
|
terractl backup list # schedules and backups of all instances
|
||||||
|
sudo terractl backup now calamity # one now
|
||||||
|
sudo terractl backup disable calamity # stop (keeps existing backups)
|
||||||
```
|
```
|
||||||
|
|
||||||
Create the folder first with `sudo mkdir -p /var/backups/terraria`. In a crontab, `%` has a special meaning and must be written as `\%`, as above. Terraria also keeps a `.bak` copy of each world next to the world file, which is useful if the latest save is damaged.
|
`--every` takes `hourly`, `daily`, `weekly`, `monthly`, a time such as `04:30`, or anything else systemd's `OnCalendar=` accepts (`'Sun 03:00'`, `'*-*-* 00/6:00'`; see `man systemd.time`). Running `backup enable` again changes the schedule.
|
||||||
|
|
||||||
|
How it works:
|
||||||
|
|
||||||
|
- **One systemd timer per instance**, `terraria-backup@<name>.timer`. A backup missed while the machine was off is made soon after it starts again. Backups start up to 10 minutes after the scheduled time, so several instances don't all start at once.
|
||||||
|
- **The world is saved first** if the server is running, as `export` does, so the backup has the current state. The server keeps running.
|
||||||
|
- **Backups go in `/var/lib/terraria/backups/<name>/`**, as `<name>-<date>-<time>.tar.gz`. Members of the `terraria` group can read them, as they can the instances themselves (`serverconfig.txt` can contain the server password). Change the folder with `TERRARIA_BACKUPS` in `/etc/default/terractl`.
|
||||||
|
- **Only scheduled backups are deleted** when there are more than `--keep`. The ones `upgrade` makes before changing version (`...-before-<zip>.tar.gz`) and ones you put there yourself are kept.
|
||||||
|
- **`rename` takes the schedule along**, and `delete` turns it off. Existing backups stay where they are in both cases.
|
||||||
|
|
||||||
|
To restore one, import it under a new name, or replace the instance with it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo terractl import calamity-restored /var/lib/terraria/backups/calamity/calamity-20261002-040512.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
If a scheduled backup fails, `terractl backup list` and `terractl doctor` say so; the details are in `journalctl -u terraria-backup@<name>`.
|
||||||
|
|
||||||
|
Backups on the same disk don't protect against the disk failing. Copy `/var/lib/terraria/backups` elsewhere now and then, or put it on another disk (with `TERRARIA_BACKUPS`, or a mount). Terraria also keeps a `.bak` copy of each world next to the world file, which is useful if the latest save is damaged.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -807,7 +968,14 @@ The servers always run as the `terraria` user. Administration is simplest with `
|
|||||||
| `attach` | root, or members of the `terraria` group | Writes to the console pipe and reads the servers' journal. |
|
| `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`. |
|
| `cmd` | root, or members of the `terraria` group | Same as `attach`. |
|
||||||
| `watch` | root, or members of the `terraria` group | Reads the servers' journal only. |
|
| `watch` | root, or members of the `terraria` group | Reads the servers' journal only. |
|
||||||
|
| `upgrade <name>` (list) | root, or members of the `terraria` group | |
|
||||||
|
| `upgrade <name> <zip>` | root only | Checked by the script. Stops and starts the server, and unpacks the zip as `terraria`. |
|
||||||
| `reinstall` | root (or `terraria`) | |
|
| `reinstall` | root (or `terraria`) | |
|
||||||
|
| `fix-perms` | root only | Checked by the script. |
|
||||||
|
| `fetch` | root, or members of the `terraria` group | Writes to `dist/`, which the group may. `--upgrade` needs root. |
|
||||||
|
| `backup list` | root, or members of the `terraria` group | |
|
||||||
|
| `backup enable`, `disable`, `now` | root only | Checked by the script. |
|
||||||
|
| `doctor` | anyone | Without root, the checks that need it are skipped. `--fix` needs root. |
|
||||||
| `mods <name>` (list) | root, or members of the `terraria` group | |
|
| `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`. |
|
| `mods add/enable/disable/remove` | root (or `terraria`) | Run as root, it sets ownership of the files it installs to `terraria`. |
|
||||||
| `delete`, `rename` | root only | Checked by the script. |
|
| `delete`, `rename` | root only | Checked by the script. |
|
||||||
@@ -837,9 +1005,11 @@ Members of the `terraria` group can read server files (useful for backups) but c
|
|||||||
The helper commands don't leave root-owned files behind, and `mods add` handles mods and ModConfigs. The remaining risk is **copying or creating other files in an instance directory as root**, such as worlds. The server can't write to root-owned files, so saving fails or tModLoader errors. After copying anything in, run:
|
The helper commands don't leave root-owned files behind, and `mods add` handles mods and ModConfigs. The remaining risk is **copying or creating other files in an instance directory as root**, such as worlds. The server can't write to root-owned files, so saving fails or tModLoader errors. After copying anything in, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo chown -R terraria:terraria /var/lib/terraria/instances/<name>
|
sudo terractl fix-perms <name>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This gives every file of the instance back to the `terraria` user and resets the modes to what the service creates: the `terraria` group can read everything but change nothing, and other users get no access. Files that were executable stay executable (and the server binaries are made executable again); setuid bits are removed. Symbolic links are left alone, so nothing outside the instance is changed. It's safe while the server is running.
|
||||||
|
|
||||||
Alternatively, copy files as the `terraria` user to begin with:
|
Alternatively, copy files as the `terraria` user to begin with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -872,6 +1042,7 @@ While a server runs, whether systemd started it or you did, it holds a lock on t
|
|||||||
- **A second `run` is refused**, and so is starting the service while a copy started by hand is running. In that case the service keeps retrying every 10 seconds and starts by itself once the other copy exits.
|
- **A second `run` is refused**, and so is starting the service while a copy started by hand is running. In that case the service keeps retrying every 10 seconds and starts by itself once the other copy exits.
|
||||||
- **`prepare` is refused**, so `server/` can't be replaced under the running server.
|
- **`prepare` is refused**, so `server/` can't be replaced under the running server.
|
||||||
- **`reinstall --clean` is refused**, so `.NET` can't be deleted under it. Plain `reinstall` is allowed, because it only takes effect at the next start.
|
- **`reinstall --clean` is refused**, so `.NET` can't be deleted under it. Plain `reinstall` is allowed, because it only takes effect at the next start.
|
||||||
|
- **`upgrade` stops the service first**, and refuses if a copy started by hand is running.
|
||||||
- **`delete` stops the service**, but refuses to go on if a copy started by hand is still running.
|
- **`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)`.
|
- **`list` shows a copy started by hand** as `running (outside systemd)`.
|
||||||
|
|
||||||
@@ -881,6 +1052,44 @@ While a server runs, whether systemd started it or you did, it holds a lock on t
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Start with `doctor`
|
||||||
|
|
||||||
|
`terractl doctor` checks the whole setup and explains anything that's wrong, with the command that fixes it:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo terractl doctor
|
||||||
|
terractl 0.0.12 on nas
|
||||||
|
|
||||||
|
System
|
||||||
|
ok user and group 'terraria'
|
||||||
|
ok programs (including ICU and curl for tModLoader)
|
||||||
|
ok folders /var/lib/terraria/instances and /var/lib/terraria/dist
|
||||||
|
ok server zips: 3 in /var/lib/terraria/dist
|
||||||
|
FAIL /etc/systemd/system/terraria@.service replaces the package's terraria@.service (left over from a manual setup?)
|
||||||
|
fix: rm /etc/systemd/system/terraria@.service && systemctl daemon-reload
|
||||||
|
ok the services can use /var/lib/terraria/instances and /var/lib/terraria/dist
|
||||||
|
ok the 'terraria' group can read the servers' journal
|
||||||
|
|
||||||
|
Instances
|
||||||
|
FAIL calamity: 3 files aren't owned by terraria, so the server can't change them (saving can fail)
|
||||||
|
fix: terractl fix-perms calamity
|
||||||
|
ok pvp (vanilla, port 7778, terraria-server-1458.zip)
|
||||||
|
|
||||||
|
2 problem(s), 0 warning(s).
|
||||||
|
terractl doctor --fix fixes 1 of them; the others need the steps shown.
|
||||||
|
```
|
||||||
|
|
||||||
|
It checks:
|
||||||
|
|
||||||
|
- **The system:** the `terraria` user and group; `unzip`, and ICU and `curl` if there are tModLoader instances; the instances, zips and backups folders; unreadable zips; the systemd units, including old copies in `/etc/systemd/system` that replace the package's; whether the services may use the folders set in `/etc/default/terractl` (see [Changing locations](#changing-locations)); the servers' journal; polkit; the daemon, if it's on; and servers or backups that are still scheduled for instances that no longer exist.
|
||||||
|
- **Each instance:** files the server can't change, or that others can read; its server zip (missing, wrong type, or a `ZIP=` changed by hand that hasn't been applied yet); paths in `serverconfig.txt` that point somewhere else (e.g. after copying it by hand); enabled mods that aren't installed; instances sharing a port; and failed starts or backups.
|
||||||
|
|
||||||
|
`--fix` fixes what's safe to fix by itself, and says what it did: file ownership and modes (`fix-perms`), folder and zip permissions, the servers' journal access, the drop-in for moved folders, and boot links or backup schedules for instances that are gone. Everything else, such as deleting unit files or opening up home folders, is left for you, with the steps shown. `terractl doctor <name>` checks only that instance (and the system).
|
||||||
|
|
||||||
|
Without `sudo`, `doctor` skips the checks that need root and says so. It exits with status 1 if it finds a problem, so it can be used in scripts and monitoring.
|
||||||
|
|
||||||
|
### Common problems
|
||||||
|
|
||||||
**tModLoader stops with `Aborted` right after "Launched Using Local Dotnet".**
|
**tModLoader stops with `Aborted` right after "Launched Using Local Dotnet".**
|
||||||
The ICU library is missing, so .NET aborts at startup. Fix it with `sudo apt install libicu-dev`, then restart the instance. To confirm, check that `ldconfig -p | grep -c libicu` prints more than 0.
|
The ICU library is missing, so .NET aborts at startup. Fix it with `sudo apt install libicu-dev`, then restart the instance. To confirm, check that `ldconfig -p | grep -c libicu` prints more than 0.
|
||||||
|
|
||||||
@@ -897,8 +1106,11 @@ sudo cp -a /var/lib/terraria/instances/other/server/dotnet /var/lib/terraria/ins
|
|||||||
sudo systemctl start terraria@<name>
|
sudo systemctl start terraria@<name>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**The server won't start: "Read-only file system" or "No such file or directory" for its folder.**
|
||||||
|
The instances folder was moved (`TERRARIA_BASE`) to where the service isn't allowed to write, or under `/home`. The log names the drop-in to add, and so does `terractl list`. See [Changing locations](#changing-locations).
|
||||||
|
|
||||||
**The server can't save, or you see "Permission denied" errors.**
|
**The server can't save, or you see "Permission denied" errors.**
|
||||||
Some files are owned by root. Run `sudo chown -R terraria:terraria /var/lib/terraria/instances/<name>`.
|
Some files are owned by root, or have the wrong modes. Run `sudo terractl fix-perms <name>`.
|
||||||
|
|
||||||
**Players can't join a tModLoader server with a version-mismatch message.**
|
**Players can't join a tModLoader server with a version-mismatch message.**
|
||||||
The player's tModLoader version differs from the server's. See [Versions and upgrades](#versions-and-upgrades).
|
The player's tModLoader version differs from the server's. See [Versions and upgrades](#versions-and-upgrades).
|
||||||
@@ -947,7 +1159,9 @@ Its access was revoked with `daemon revoke`, or the host's `/var/lib/terractl` w
|
|||||||
- instance names;
|
- instance names;
|
||||||
- `vanilla`/`tmodloader` for `new`;
|
- `vanilla`/`tmodloader` for `new`;
|
||||||
- common server console commands for `cmd` (`save`, `say`, `playing`, `kick`...);
|
- common server console commands for `cmd` (`save`, `say`, `playing`, `kick`...);
|
||||||
- the options of `enable`, `disable`, `watch`, `reinstall`, `delete`, `export`, `copy` and `move`;
|
- the options of `enable`, `disable`, `watch`, `upgrade`, `reinstall`, `delete`, `export`, `copy` and `move`;
|
||||||
|
- server zips for `upgrade`, only those of the instance's type (on a remote, all of its zips);
|
||||||
|
- `vanilla`/`tmodloader` and the options for `fetch`, `backup` actions and schedules (`--every daily` etc.), and `--fix` for `doctor`;
|
||||||
- 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;
|
- 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`;
|
- `.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.
|
- `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.
|
||||||
@@ -970,8 +1184,21 @@ terractl disable <name> [--now] Don't start at boot (--now: an
|
|||||||
terractl attach <name> Interactive console (Ctrl-D detaches)
|
terractl attach <name> Interactive console (Ctrl-D detaches)
|
||||||
terractl watch <name> [-n <lines>] [-q] Follow the output, read-only (Ctrl-C quits)
|
terractl watch <name> [-n <lines>] [-q] Follow the output, read-only (Ctrl-C quits)
|
||||||
terractl cmd <name> <command...> Send one console command
|
terractl cmd <name> <command...> Send one console command
|
||||||
terractl reinstall <name> [--clean] Re-unpack server/ on next start
|
terractl upgrade <name> Show the zips it can use, and which it runs
|
||||||
|
terractl upgrade <name> <zip>|latest [--downgrade] [--no-backup]
|
||||||
|
Change server version now (backs up first)
|
||||||
|
terractl reinstall <name> [--clean] Re-unpack the same version on next start
|
||||||
(--clean also re-downloads .NET)
|
(--clean also re-downloads .NET)
|
||||||
|
terractl fix-perms <name> Reset the owner and modes of all its files
|
||||||
|
terractl fetch vanilla|tmodloader [<version>] [--upgrade <name>]
|
||||||
|
Download a server zip (default: the latest)
|
||||||
|
terractl fetch vanilla|tmodloader --list Versions available to download
|
||||||
|
terractl backup enable <name> [--every <when>] [--keep <n>]
|
||||||
|
Back up on a schedule (default: daily, keep 14)
|
||||||
|
terractl backup disable <name> Stop scheduled backups (keeps existing ones)
|
||||||
|
terractl backup now <name> Back up now
|
||||||
|
terractl backup list [<name>] Schedules and backups
|
||||||
|
terractl doctor [<name>] [--fix] Check the setup; --fix fixes what it safely can
|
||||||
terractl mods <name> [list] Show installed mods
|
terractl mods <name> [list] Show installed mods
|
||||||
terractl mods <name> add <folder|file.tmod>... Install and enable mods (and ModConfigs)
|
terractl mods <name> add <folder|file.tmod>... Install and enable mods (and ModConfigs)
|
||||||
terractl mods <name> enable|disable <Mod>... Change which mods load
|
terractl mods <name> enable|disable <Mod>... Change which mods load
|
||||||
@@ -1000,6 +1227,8 @@ Used by the service:
|
|||||||
terractl prepare <name> Create dirs/configs, unpack zip if needed
|
terractl prepare <name> Create dirs/configs, unpack zip if needed
|
||||||
terractl run <name> Run the server
|
terractl run <name> Run the server
|
||||||
terractl service-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 journal-access Let the terraria group read the servers' journal
|
||||||
|
(runs before every start; safe to run by hand)
|
||||||
terractl daemon serve Run the daemon (terractld.service)
|
terractl daemon serve Run the daemon (terractld.service)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -325,7 +325,7 @@ def host_addresses(listen_host, port):
|
|||||||
# --------------------------------------------------------------- daemon ----
|
# --------------------------------------------------------------- daemon ----
|
||||||
|
|
||||||
EXEC_COMMANDS = {"list", "info", "new", "start", "stop", "restart", "enable", "disable", "cmd", "watch",
|
EXEC_COMMANDS = {"list", "info", "new", "start", "stop", "restart", "enable", "disable", "cmd", "watch",
|
||||||
"mods", "reinstall", "delete", "rename", "export", "import",
|
"mods", "reinstall", "upgrade", "fix-perms", "doctor", "backup", "fetch", "delete", "rename", "export", "import",
|
||||||
"-V", "--version", "version", "-h", "--help", "help"}
|
"-V", "--version", "version", "-h", "--help", "help"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ case "$1" in
|
|||||||
# Forget which instances were enabled at boot
|
# Forget which instances were enabled at boot
|
||||||
rm -f /etc/systemd/system/multi-user.target.wants/terraria@*.service
|
rm -f /etc/systemd/system/multi-user.target.wants/terraria@*.service
|
||||||
rm -rf /etc/systemd/system/terraria@.service.d
|
rm -rf /etc/systemd/system/terraria@.service.d
|
||||||
|
# Scheduled backups (the backups themselves stay in /var/lib/terraria/backups)
|
||||||
|
rm -f /etc/systemd/system/timers.target.wants/terraria-backup@*.timer
|
||||||
|
rm -rf /etc/systemd/system/terraria-backup@*.timer.d /etc/systemd/system/terraria-backup@*.service.d
|
||||||
# The daemon: whether it's on, its certificate and the computers it trusts
|
# 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 -f /etc/systemd/system/multi-user.target.wants/terractld.service
|
||||||
rm -rf /etc/systemd/system/terractld.service.d /var/lib/terractl
|
rm -rf /etc/systemd/system/terractld.service.d /var/lib/terractl
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ case "$1" in
|
|||||||
remove|deconfigure)
|
remove|deconfigure)
|
||||||
if [ -d /run/systemd/system ]; then
|
if [ -d /run/systemd/system ]; then
|
||||||
systemctl stop terractld.service || true
|
systemctl stop terractld.service || true
|
||||||
|
systemctl stop 'terraria-backup@*.timer' 'terraria-backup@*.service' || true
|
||||||
units=$(systemctl list-units --plain --no-legend --all 'terraria@*.service' | awk '{print $1}')
|
units=$(systemctl list-units --plain --no-legend --all 'terraria@*.service' | awk '{print $1}')
|
||||||
if [ -n "$units" ]; then
|
if [ -n "$units" ]; then
|
||||||
echo "Stopping Terraria instances: $units"
|
echo "Stopping Terraria instances: $units"
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ StateDirectoryMode=0755
|
|||||||
# computers, like sudo terractl does. Everything else is locked down.
|
# computers, like sudo terractl does. Everything else is locked down.
|
||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
ProtectSystem=full
|
ProtectSystem=full
|
||||||
|
# 'backup enable' and 'doctor --fix' write drop-ins for the Terraria units here
|
||||||
|
ReadWritePaths=/etc/systemd/system
|
||||||
ProtectHome=true
|
ProtectHome=true
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
PrivateDevices=true
|
PrivateDevices=true
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Back up Terraria server (%i)
|
||||||
|
Documentation=file://@DOCDIR@/README.md
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
# Saves the world first if the server is running, then archives the instance
|
||||||
|
# (without server/) into /var/lib/terraria/backups/%i/ and removes the oldest
|
||||||
|
# scheduled backups beyond the number to keep. Set up with:
|
||||||
|
# terractl backup enable %i [--every <when>] [--keep <number>]
|
||||||
|
ExecStart=@BINDIR@/terractl backup now %i
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=idle
|
||||||
|
|
||||||
|
# Runs as root, like 'sudo terractl backup now'
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=full
|
||||||
|
PrivateTmp=true
|
||||||
|
PrivateDevices=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectControlGroups=true
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Scheduled backups of Terraria server (%i)
|
||||||
|
Documentation=file://@DOCDIR@/README.md
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
# 'terractl backup enable %i --every <when>' sets the schedule in a drop-in
|
||||||
|
OnCalendar=daily
|
||||||
|
# Catch up on a backup missed while the machine was off
|
||||||
|
Persistent=true
|
||||||
|
RandomizedDelaySec=10min
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
Reference in new issue
Block a user