Two mechanical sweeps, no behaviour change on a single-root install. The 14 `[[ "$p" == "$containers_dir"* ]]` prefix tests that decide manager-vs-container-user elevation become pathIsContainerData, so a file on a second storage root is no longer misclassified as manager-owned — which would have written it with the wrong owner and failed later, far from the cause. The 65 references to the WebUI's own tree become webuiDir(), which is pinned to the primary root by design. Two traps found while doing it: run_privileged.sh is sourced directly by init.sh without paths.sh, so it needs a fallback. Defining one named pathIsContainerData was wrong: generate_function_manifest.sh indexes top-level definitions, and the resulting autoload stub would have shadowed the real multi-root implementation with the primary-only fallback — silently classifying every file on a second disk as manager-owned, which is exactly the bug this sweep exists to prevent. Renamed to _runCfgIsContainerPath, which delegates when the real one is loaded. setup_lock.sh built its path in a top-level assignment, so it was evaluated at source time and needed the file flagged eager. Made it a function instead: the path resolves on call, and the file drops off LP_EAGER_FILES entirely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
59 lines
2.5 KiB
Bash
59 lines
2.5 KiB
Bash
#!/bin/bash
|
|
|
|
# A containers-root wipe (fresh install, or a relocation of the containers root)
|
|
# removes an app's project directory but NOT the container the daemon still holds
|
|
# in its own state. Those containers are stranded: their compose file, config and
|
|
# bind-mount sources are gone, so they can never start correctly again.
|
|
#
|
|
# They are not inert. `restart: unless-stopped` makes the daemon retry them on
|
|
# every restart, and Docker materialises each missing bind-mount source before
|
|
# handing off to runc — creating an empty DIRECTORY even where the mount is a
|
|
# file. That is how a fresh install ends up with <app>.config as a directory: the
|
|
# install restarts the rootless daemon, the daemon resurrects the previous
|
|
# install's container, and the stubs it leaves behind collide with the install's
|
|
# own file copies moments later.
|
|
#
|
|
# Sweep them once the daemon is up, so the next restart has nothing to resurrect.
|
|
# Scoped to containers whose project directory sits under the LibrePortal
|
|
# containers root — unrelated containers on the host are never touched.
|
|
dockerRemoveStrandedContainers()
|
|
{
|
|
local silent_flag="${1:-silent}"
|
|
|
|
[[ -d "$containers_dir" ]] || return 0
|
|
|
|
# `systemctl restart` returns once the unit is active, which can be before
|
|
# dockerd finishes initialising. Give the socket a bounded moment to answer.
|
|
local attempt=0
|
|
until runFileOp docker info >/dev/null 2>&1; do
|
|
attempt=$((attempt + 1))
|
|
if [[ "$attempt" -ge 15 ]]; then
|
|
[[ "$silent_flag" == "loud" ]] && isNotice "Docker not responding — skipping stranded container sweep."
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
local listing
|
|
listing=$(runFileOp docker ps -a --format '{{.Names}} {{.Label "com.docker.compose.project.working_dir"}}' 2>/dev/null)
|
|
[[ -z "$listing" ]] && return 0
|
|
|
|
local removed=0 name work_dir
|
|
while IFS=$'\t' read -r name work_dir; do
|
|
[[ -z "$name" || -z "$work_dir" ]] && continue
|
|
# Only LibrePortal's own container tree, and only when the project
|
|
# directory is provably gone.
|
|
pathIsContainerData "$work_dir" || continue
|
|
[[ -d "$work_dir" ]] && continue
|
|
|
|
if runFileOp docker rm -f "$name" >/dev/null 2>&1; then
|
|
removed=$((removed + 1))
|
|
[[ "$silent_flag" == "loud" ]] && isNotice "Removed stranded container '$name' (project dir $work_dir is gone)."
|
|
fi
|
|
done <<< "$listing"
|
|
|
|
if [[ "$removed" -gt 0 ]]; then
|
|
isSuccessful "Removed $removed stranded container(s) left without a project directory"
|
|
fi
|
|
}
|