warn when the services can't reach moved folders

- list, new, import and enable check TERRARIA_BASE and TERRARIA_DIST
  against the units' ProtectSystem/ProtectHome/ReadWritePaths
  (terraria@ always, terractld when it's on) and print the drop-in
  to add. prepare explains why it can't create an instance folder.
- Guide: bind mounts and drop-ins under "Changing locations", and a
  new section for instances or zips under /home.
- journal-access in tab completion and the command reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-02 21:51:29 +00:00
1 parent 96f7876729
commit 7adf5c2999
5 files changed
+195 -9

No files matched your search

+107 -2
View File
@@ -311,8 +311,11 @@ instance_running() {
}
cmd_prepare() {
local name=$1 dir; dir=$(inst_dir "$name")
mkdir -p "$dir/data/Worlds"
local name=$1 dir err; dir=$(inst_dir "$name")
if ! err=$(mkdir -p "$dir/data/Worlds" 2>&1); then
location_warnings
die "can't create $dir/data/Worlds: ${err##*: }"
fi
take_lock "$name" # never re-unpack server/ under a running server
[[ -f $dir/instance.conf ]] || write_instance_conf "$name" vanilla
load_conf "$name"
@@ -833,6 +836,7 @@ cmd_import() {
echo "imported '$name' into $dir"
fi
echo "Review $dir/serverconfig.txt, then: $(hint root enable "$name" --now)"
location_warnings
}
# ---------------------------------------------------------------- mods ----
@@ -1017,6 +1021,7 @@ EOF
$(hint root enable "$name" --now)
$(hint group attach "$name")
EOF
location_warnings
}
# Whether the server starts at boot. Needs root: systemd doesn't let polkit
@@ -1035,6 +1040,7 @@ cmd_enable() {
# 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"))"
location_warnings
systemctl enable -q "$unit"
if [[ $now -eq 1 ]]; then
systemctl start "$unit"
@@ -1088,6 +1094,104 @@ zip_warning() {
log "fix it with$where: sudo chmod 644 ${bad[*]@Q}"
}
# ------------------------------------------------------------ locations ----
# The services' sandboxes (ProtectSystem=, ProtectHome=, ReadWritePaths=) are
# set in their unit files, which can't follow /etc/default/terractl. If
# TERRARIA_BASE or TERRARIA_DIST is moved somewhere a unit can't reach, it needs
# a drop-in; these checks say so, with the drop-in to add.
# Is <path> <dir>, or inside it?
path_in() { [[ $1 == "$2" || $1 == "$2"/* ]]; }
# Why <unit> can't use <path> (need: rw or ro): 'home' (ProtectHome hides it),
# 'home-ro' (ProtectHome=read-only, not writable) or 'system' (ProtectSystem
# makes it read-only). Prints nothing if the unit can use it, or if systemd or
# the unit isn't there to ask.
unit_path_problem() {
local unit=$1 path=$2 need=$3 key value load="" ps=no ph=no p covered=0
local -a rw=()
command -v systemctl >/dev/null || return 0
while IFS='=' read -r key value; do
case $key in
LoadState) load=$value ;;
ProtectSystem) ps=$value ;;
ProtectHome) ph=$value ;;
ReadWritePaths) read -ra rw <<< "$value" ;;
esac
done < <(systemctl show -p LoadState,ProtectSystem,ProtectHome,ReadWritePaths "$unit" 2>/dev/null)
[[ $load == loaded ]] || return 0
path=$(readlink -m "$path") # where it really is, as systemd sees it
for p in "${rw[@]}"; do
p=${p#-} # '-': ignored if missing
if path_in "$path" "$(readlink -m "$p")"; then covered=1; fi
done
if path_in "$path" /home || path_in "$path" /root || path_in "$path" /run/user; then
case $ph in
no) ;;
read-only) if [[ $need == rw && $covered -eq 0 ]]; then echo home-ro; fi ;;
*) echo home ;;
esac
return 0
fi
[[ $need == rw && $covered -eq 0 ]] || return 0
case $ps in
strict) echo system ;;
yes|full)
if path_in "$path" /usr || path_in "$path" /boot || path_in "$path" /efi ||
{ [[ $ps == full ]] && path_in "$path" /etc; }; then
echo system
fi ;;
esac
}
# Warns about one unit: location_warning <unit> <name to edit> <who it breaks>
# <dist access needed>
location_warning() {
local unit=$1 edit=$2 effect=$3 dist_need=$4 what path need problem home=0 where=""
local -a lines=() rw=()
for what in TERRARIA_BASE TERRARIA_DIST; do
if [[ $what == TERRARIA_BASE ]]; then path=$BASE_DIR need=rw; else path=$DIST_DIR need=$dist_need; fi
problem=$(unit_path_problem "$unit" "$path" "$need")
case $problem in
home)
lines+=("$edit hides $path ($what) under ProtectHome=, so $effect")
home=1
if [[ $need == rw ]]; then rw+=("$path"); fi ;;
home-ro|system)
lines+=("$edit can't write to $path ($what), so $effect")
if [[ $problem == home-ro ]]; then home=1; fi
rw+=("$path") ;;
esac
done
[[ ${#lines[@]} -gt 0 ]] || return 0
if [[ -n ${TERRACTL_VIA:-} ]]; then where=" (on $(hostname 2>/dev/null || echo that host))"; fi
printf 'terractl: warning: %s\n' "${lines[@]}" >&2
{
echo " Fix it with a drop-in$where: sudo systemctl edit $edit"
echo " [Service]"
if [[ $home -eq 1 ]]; then echo " ProtectHome=read-only"; fi
for path in "${rw[@]}"; do echo " ReadWritePaths=$path"; done
if [[ $home -eq 1 ]]; then
echo " The '$SVC_USER' user also needs access to every folder above it; see"
echo " \"Instances or zips under /home\" in the guide."
else
echo " See \"Changing locations\" in the guide."
fi
} >&2
}
# The servers always; the daemon only if it's in use. The daemon writes zips
# into dist/ when instances are copied to this host, so it needs both writable.
location_warnings() {
location_warning terraria@terractl-check.service terraria@.service \
"servers can't start" ro
if systemctl is-enabled --quiet terractld.service 2>/dev/null ||
systemctl is-active --quiet terractld.service 2>/dev/null; then
location_warning terractld.service terractld.service \
"other hosts can't copy, move or import instances here" rw
fi
}
cmd_list() {
local d n port boot state
printf '%-20s %-11s %-6s %-9s %s\n' INSTANCE TYPE PORT BOOT STATE
@@ -1105,6 +1209,7 @@ cmd_list() {
) 2>/dev/null || printf '%-20s %s\n' "$n" "(cannot read config - try sudo)"
done
zip_warning
location_warnings
}
# ------------------------------------------------------------ versions ----