fix(backup): report the real reason a live capture fails

Two discoveries from exercising the capture path live:

- docker exec prints its 'executable file not found' OCI error to STDOUT
  (a docker quirk), i.e. into the tar pipe — so the exit code (126/127)
  is the only trustworthy no-tar signal, and the host-side 'not a tar
  archive' noise is a symptom, not the cause. Detect on the code and say
  plainly that the image has no tar.
- The two pipe halves shared one stderr file through an O_TRUNC fd and
  an O_APPEND fd, racing and overwriting each other's lines — one file
  per half, concatenated after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-21 01:04:18 +01:00
parent facf764c4b
commit 66cdb5be0d

View File

@ -89,11 +89,15 @@ backupFilesCapture()
# docker must go through runFileOp: on rootless installs the daemon
# socket lives in the install user's runtime dir and only the wrapper
# sets DOCKER_HOST for it — bare `docker` can't connect at all. Keep
# stderr in a scratch file so a failure can say why.
# stderr so a failure can say why — one file per pipe half: sharing a
# file mixes an O_TRUNC fd with an O_APPEND one and the halves race,
# overwriting each other's lines.
local err_file capture_status
err_file=$(mktemp "${TMPDIR:-/tmp}/lp-capture-XXXXXX.err")
runFileOp docker exec "$container" tar -C "$cpath" -cf - . 2>"$err_file" | runFileOp tar -xf - -C "$stage" 2>>"$err_file"
runFileOp docker exec "$container" tar -C "$cpath" -cf - . 2>"$err_file.docker" | runFileOp tar -xf - -C "$stage" 2>"$err_file.host"
capture_status=("${PIPESTATUS[@]}")
cat "$err_file.docker" "$err_file.host" > "$err_file" 2>/dev/null
rm -f "$err_file.docker" "$err_file.host"
if [[ ${capture_status[0]} -eq 0 && ${capture_status[1]} -eq 0 ]]; then
# The capture preserves the app's ownership (e.g. www-data, 0640),
# which the backup user still couldn't read. Hand the staging tree to
@ -119,7 +123,11 @@ backupFilesCapture()
isSuccessful "captured $subdir ($(du -sh "$stage" 2>/dev/null | cut -f1))"
else
if grep -q "executable file not found" "$err_file" 2>/dev/null; then
# docker exec exits 126/127 when the binary can't start — and its
# "executable file not found" message goes to STDOUT (a docker
# quirk), i.e. into the pipe, so the exit code is the only
# trustworthy signal.
if [[ ${capture_status[0]} -eq 126 || ${capture_status[0]} -eq 127 ]]; then
isError "capture of $subdir from $container failed — the image has no tar, so live capture cannot work for this app"
else
isError "capture of $subdir from $container failed:"
@ -189,10 +197,12 @@ restoreFilesRehydratePreStart()
# docker goes through runFileOp for the rootless socket, same as capture.
local err_file restore_status
err_file=$(mktemp "${TMPDIR:-/tmp}/lp-rehydrate-XXXXXX.err")
runFileOp tar -C "$stage" -cf - . 2>"$err_file" | runFileOp docker run --rm -i \
runFileOp tar -C "$stage" -cf - . 2>"$err_file.host" | runFileOp docker run --rm -i \
-v "$app_dir:/parent" "$backup_files_helper_image" \
sh -c "rm -rf '/parent/$subdir' && mkdir -p '/parent/$subdir' && tar -C '/parent/$subdir' -xf - && chown -R $uid:$gid '/parent/$subdir'" 2>>"$err_file"
sh -c "rm -rf '/parent/$subdir' && mkdir -p '/parent/$subdir' && tar -C '/parent/$subdir' -xf - && chown -R $uid:$gid '/parent/$subdir'" 2>"$err_file.docker"
restore_status=("${PIPESTATUS[@]}")
cat "$err_file.host" "$err_file.docker" > "$err_file" 2>/dev/null
rm -f "$err_file.host" "$err_file.docker"
if [[ ${restore_status[0]} -eq 0 && ${restore_status[1]} -eq 0 ]]; then
isSuccessful "restored $subdir"
else