0

I have a backup script that is essentially:

acquire_data | gzip -9 | gpg --batch -e -r me@example.com | upload-to-cloud

The problem is if acquire_data, or gpg fails, then upload-to-cloud will see the EOF and happily upload an incomplete backup. As an example gpg will fail if the filesystem with the user's home directory is, is full.

I want to pipe it, not store to a temporary file, because it's a lot of data that may not fit in the local server's free space.

I might be able to do something like:

set -o pipefail
mkfifo fifo
upload-to-cloud < fifo &
UPLOADER=$!
((acquire_data | gzip -9 | gpg […]) || kill $UPLOADER) > fifo
wait $UPLOADER  # since I need the exit status

But I think that has a race condition. It's not guaranteed that the upload-to-cloud program will receive the signal before it reads an EOF. And adding a sleep seems wrong. Really stdin of upload-to-cloud need never be closed.

I want upload-to-cloud to die before it handles the EOF because then it won't finalize the upload, and the partial upload will be correctly discarded.

There's this similar question, except it talks about killing an earlier part if a later part fails, which is safer since it doesn't have a problem of the race condition.

What's the best way to do this?

Thomas
  • 4,208
  • 2
  • 29
  • 31
  • 1
    What about `((acquire_data | gzip -9 | gpg […]) || (kill $UPLOADER; wait $UPLOADER)) > fifo`? Here, I think, you are sure that EOF will not be hit until the process is killed. – anishsane Aug 01 '19 at 16:32
  • @anishsane that `wait` I think won't work because parentheses start a subshell and you can't wait for `$UPLOADER` since it's not a child process. – Thomas Aug 02 '19 at 17:33
  • Fair point..... – anishsane Aug 03 '19 at 08:26

1 Answers1

4

Instead of running this all as one pipeline, split off upload-to-cloud into a separate process substitution which can be independently signaled, and for which your parent shell script holds a descriptor (and thus can control the timing of reaching EOF on its stdin).

Note that upload-to-cloud needs to be written to delete content it already uploaded in the event of an unclean exit for this to work as you intend.

Assuming you have a suitably recent version of bash:

#!/usr/bin/env bash

# dynamically allocate a file descriptor; assign it to a process substitution
# store the PID of that process substitution in upload_pid
exec {upload_fd}> >(exec upload-to-cloud); upload_pid=$!

# make sure we recorded an upload_pid that refers to a process that is actually running
if ! kill -0 "$upload_pid"; then
  # if this happens without any other obvious error message, check that we're bash 4.4
  echo "ERROR: upload-to-cloud not started, or PID not stored" >&2
fi

shopt -s pipefail
if acquire_data | gzip -9 | gpg --batch -e -r me@example.com >&"$upload_fd"; then
  exec {upload_fd}>&-  # close the pipeline writing up upload-to-cloud gracefully...
  wait "$upload_pid"   # ...and wait for it to exit
  exit                 # ...then ourselves exiting with the exit status of upload-to-cloud
                       # (which was returned by wait, became $?, thus exit's default).
else
  retval=$?            # store the exit status of the failed pipeline component
  kill "$upload_pid"   # kill the backgrounded process of upload-to-cloud
  wait "$upload_pid"   # let it handle that SIGTERM...
  exit "$retval"       # ...and exit the script with the exit status we stored earlier.
fi

Without a new enough bash to be able to store the PID for a process substitution, the line establishing the process substitution might change to:

mkfifo upload_to_cloud.fifo
upload-to-cloud <upload_to_cloud.fifo & upload_pid=$!
exec {upload_fd}>upload_to_cloud.fifo
rm -f upload_to_cloud.fifo

...after which the rest of the script should work unmodified.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • In my case the cleanup part is actually not needed. If the uploader doesn't finalize the upload (which it'll do when it gets EOF) then the data is automatically discarded. That way the uploader doesn't need "delete" access in its cloud api key, which is safer. Yes, this substitution should work, as it'll make me in control of closing the fd. Thank you! – Thomas Aug 01 '19 at 16:05
  • Playing around with this a bit it seems, like your comment implies, to require bash 4.4 (I have 4.3). The thing that's failing if I remove the kill -0 is that I can't wait for $upload_pid since it's a grandchild pid with a bash child in between. Is it fixable? – Thomas Aug 01 '19 at 16:53
  • `kill -0` isn't the 4.4 feature; rather, having `$!` be updated when starting a process substitution is the 4.4 feature (unless I'm misremembering; referring back to docs now), whereas previously it only applied to background commands started with `&`. That said, one thing you can do to take care of the specific grandchild situation is to make it `>(exec upload-to-cloud)` – Charles Duffy Aug 01 '19 at 16:57
  • 1
    ...with 4.3, yes, you'd be needing a FIFO-based approach; see addendum. – Charles Duffy Aug 01 '19 at 16:59