From 66cdb5be0df2a2aa12b73e7e9103a7f62d30b40f Mon Sep 17 00:00:00 2001 From: librelad Date: Fri, 21 Aug 2026 01:04:18 +0100 Subject: [PATCH] fix(backup): report the real reason a live capture fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- scripts/backup/files/backup_files.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/scripts/backup/files/backup_files.sh b/scripts/backup/files/backup_files.sh index bc6b1c8..8674675 100644 --- a/scripts/backup/files/backup_files.sh +++ b/scripts/backup/files/backup_files.sh @@ -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