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