Kill next command in pipeline on failure - bash

I have a streaming backup script which I'm running as follows:
./backup_script.sh | aws s3 cp - s3://bucket/path/to/backup
The aws command streams stdin to cloud storage in an atomic way. If the process is interrupted without an EOF, the upload is aborted.
I want the aws process to be killed if ./backup_script.sh exits with a non-zero exit code.
Any bash trick for doing this?
EDIT:
You can test your solution with this script:
#!/usr/bin/env python
import signal
import sys
import functools
def signal_handler(signame, signum, frame):
print "Got {}".format(signame)
sys.exit(0)
signal.signal(signal.SIGTERM, functools.partial(signal_handler, 'TERM'))
signal.signal(signal.SIGINT, functools.partial(signal_handler, 'INT'))
for i in sys.stdin:
pass
print "Got EOF"
Example:
$ grep --bla | ./sigoreof.py
grep: unrecognized option `--bla'
usage: grep [-abcDEFGHhIiJLlmnOoqRSsUVvwxZ] [-A num] [-B num] [-C[num]]
[-e pattern] [-f file] [--binary-files=value] [--color=when]
[--context[=num]] [--directories=action] [--label] [--line-buffered]
[--null] [pattern] [file ...]
Got EOF
I want ./sigoreof.py to be terminated with a signal.

Adopting/correcting a solution originally given by #Dummy00001:
mkfifo aws.fifo
exec 3<>aws.fifo # open the FIFO read/write *in the shell itself*
aws s3 cp - s3://bucket/path/to/backup <aws.fifo 3>&- & aws_pid=$!
rm aws.fifo # everyone who needs a handle already has one; can remove the directory entry
if ./backup_script.sh >&3 3>&-; then
exec 3>&- # success: close the FIFO and let AWS exit successfully
wait "$aws_pid"
else
kill "$aws_pid" # send a SIGTERM...
wait "$aws_pid" # wait for the process to die...
exec 3>&- # only close the write end *after* the process is dead
fi
Important points:
The shell opens the FIFO r/w to avoid blocking (opening for write only would block for a reader; this could also be avoided by invoking the reader [that is, the s3 command] in the background prior to the exec opening the write side).
The write end of the FIFO is held by the script itself, so the read end never hits end-of-file until after the script intentionally closes it.
The aws command's handle on the write end of the FIFO is explicitly closed (3<&-), so it doesn't hold itself open (in which case the exec 3>&- done in the parent would not successfully allow it to finish reading and exit).

backup_script.sh should have a non-zero exit status if there is an error, so you script should look something like:
if ./backup_script.sh > output.txt; then
aws s3 cp output.txt s3://bucket/path/to/backup
fi
rm -f output.txt
A pipe isn't really appropriate here.
If you really need to conserve disk space locally, you'll have to "reverse" the upload; either remove the uploaded file in the event of an error in backup_script.sh, or upload to a temporary location, then move that to the final path once you've determined that the backup has succeeded.
(For simplicity, I'm ignoring the fact that by letting aws exit on its own in the event of an error, you may be uploading more of the partial backup than you need to. See Charles Duffy's answer for a more bandwidth-efficient approach.)
After starting the backup process with
mkfifo data
./backup_script.sh > data & writer_pid=$!
use one of the following to upload the data.
# Upload and remove if there was an error
aws s3 cp - s3://bucket/path/to/backup < data &
if ! wait $writer_pid; then
aws s3 rm s3://bucket/path/to/backup
fi
or
# Upload to a temporary file and move it into place
# once you know the backup succeeded.
aws s3 cp - s3://bucket/path/to/backup.tmp < data &
if wait $writer_pid; then
aws s3 mv s3://bucket/path/to/backup.tmp s3://bucket/path/to/backup
else
aws s3 rm s3://bucket/path/to/backup
fi

A short script which uses process substitution instead of named pipes would be:
#!/bin/bash
exec 4> >( ./second-process.sh )
./first-process.sh >&4 &
if ! wait $! ; then echo "error in first process" >&2; kill 0; wait; fi
It works much like with a fifo, basically using the fd as the information carrier for the IPC instead of a file name.
Two remarks: I wasn't sure whether it's necessary to close fd 4 ; I would assume that upon script exit the shell closes all open files.
And I couldn't figure out how to obtain the PID of the process in the process substitution (anybody? at least on my cygwin the usual $! didn't work.) Therefore I resorted to killing all processes in the group, which may not be desirable (but I'm not entirely sure about the semantics).

I think you need to spawn both processes from a third one and either use the named pipe approach from Lynch in the post mentioned by #tourism (further below in the answers); or keep piping directly but re-write backup_script.sh such that it stays alive in the error case, keeping stdout open. backup_script.sh would have to signal the error condition to the calling process (e.g. by sending a SIGUSR to the parent process ID), which in turn first kills the aws process (leading to an atomic abort) and only then backup_script.sh, unless it exited already because of the broken pipe.

I had a similar situation: a shell script contained a pipeline that used one of its own functions and that function wanted to be able to effect termination. A simple contrived example that finds and displays a file:
#!/bin/sh
a() { find . -maxdepth 1 -name "$1" -print -quit | grep . || exit 101; }
a "$1" | cat
echo done
Here, the function a needs to be able to effect termination which it tries to do by calling exit. However, when invoked through a pipeline (line 3), it only terminates its own (subshell) process. In the example, the done message still appears.
One way to work around this is to detect when in a subshell and send a signal to the parent:
#!/bin/sh
die() { [[ $$ == $(exec sh -c 'echo $PPID') ]] && exit $1 || kill $$; }
a() { find . -maxdepth 1 -name "$1" -print -quit | grep . || die 101; }
a "$1" | cat
echo done
When in a subshell the $$ is the pid of the parent and the construct $(exec sh -c 'echo $PPID') is a shell-agnostic way to obtain the pid of the subprocess. If using bash then this can be replaced by $BASHPID.
If the subprocess pid and $$ differ then it sends a SIGTERM signal to the parent (kill $$) instead of calling exit.
The given exit status (101) isn't propagated by kill so the script exits with a status of 143 (which is 128+15 where 15 is the id of SIGTERM).

Related

Kill last program in pipe if any previous command fails

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?
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.

Make script terminate when background zenity dialog is closed

I have written a fairly simple script here that is meant to display a text info dialog using zenity and continuously read data from a remote TCP connection and display it in the dialog. This works... However I would like for the entire script to terminate if I close the zenity dialog.
Is there a way to do this? I don't think I can check for anything in the while loop, because the script could be stalled on reading the data from the remote TCP connection.
#!/bin/bash
on_exit() {
zenity --display=:0 --error --text="Script has exited." &
}
# Options
while getopts "a:p:t:" OPTION; do case "$OPTION" in
a) address="$OPTARG";;
p) port="$OPTARG";;
t) title="$OPTARG";;
esac; done
exec &> >(zenity --display=:0 --text-info --title=$title || exit)
# doesn't make a difference? ↑
# also tried &&
trap "on_exit" EXIT
while read data < /dev/tcp/$address/$port; do
echo $data
# ...
# do some other stuff with the information
# ...
done
Note: This is going to be run on IGEL Linux. I don't have the option of installing additional packages. So, ideally the solution I'm looking for is native to Bash.
Update
I only had to make this modification to continue using exec. Or #BachLien's answer using named pipes also works.
PID=$$
exec &> >(zenity --display=:0 --text-info --title=$title; kill $PID)
I do not have zenity installed, so I tried this script to illustrate the idea.
The program terminal+cat (emmulating zenity) is executed by the function _dspMsg which runs in background (child process); cat continuously displays messages from a file ($m) which is a named pipe; the parent process is killed when terminal+cat exits.
In the mean while, another cat proccess writes messsages into pipe $m (emmulating TPC infomation feeds); it would be killed when when _dspMsg exits.
#!/bin/bash
# 1) named pipe
m=`mktemp -u /tmp/msg-XXXX=` # get a temporary filename (for named pipe)
mkfifo "$m" # create that named pipe
trap "echo END; rm $m" EXIT # remove that file when exit
# 2) zenity
_dspMsg(){ # continuously display messages
urxvt -e bash -c "cat <$m" # terminal+cat is used in place of zenity
kill $1 # kill parent pid
} # to be run in background
_dspMsg $$ & # $$ = proccess id
# 3) TCP info feeds
cat >>"$m" # feeding messages using cat
# cat is used in placed of TCP data feed
Note:
A named pipe is used as a way of communicating between parent and child processes.
To test that script, you may need to change urxvt to xterm, iTerm, or any other terminal emmulator available in your computer.
So, maybe it is what you need (untested):
#!/bin/bash
while getopts "a:p:t:" OPTION; do case "$OPTION" in
a) address="$OPTARG";;
p) port="$OPTARG";;
t) title="$OPTARG";;
esac; done
m=`mktemp -u /tmp/msg-XXXX=`
mkfifo "$m"
trap "zenity --display=:0 --error --text='Script has exited.' & rm $m" EXIT
_dspMsg(){
zenity --display=:0 --text-info --title="$title" <"$m"
kill $1
}
_dspMsg $$ &
while read data < /dev/tcp/$address/$port; do
echo $data >>"$m"
done
You can pipe all of the output of the while loop into zenity, getting rid of the need for exec &>.
while read data < "/dev/tcp/$address/$port"; do
echo "$data"
done | zenity --display=:0 --text-info --title="$title"

Quit from pipe in bash

For following bash statement:
tail -Fn0 /tmp/report | while [ 1 ]; do echo "pre"; exit; echo "past"; done
I got "pre", but didn't quit to the bash prompt, then if I input something into /tmp/report, I could quit from this script and get into bash prompt.
I think that's reasonable. the 'exit' make the 'while' statement quit, but the 'tail' still alive. If something input into /tmp/report, the 'tail' will output to pipe, then 'tail' will detect the pipe is close, then 'tail' quits.
Am I right? If not, would anyone provide a correct interpretation?
Is it possible to add anything into 'while' statement to quit from the whole pipe statement immediately? I know I could save the pid of tail into a temporary file, then read this file in the 'while', then kill the tail. Is there a simpler way?
Let me enlarge my question. If use this tail|while in a script file, is it possible to fulfill following items simultaneously?
a. If Ctrl-C is inputed or signal the main shell process, the main shell and various subshells and background processes spawned by the main shell will quit
b. I could quit from tail|while only at a trigger case, and preserve other subprocesses keep running
c. It's better not use temporary file or pipe file.
You're correct. The while loop is executing in a subshell because its input is redirected, and exit just exits from that subshell.
If you're running bash 4.x, you may be able to achieve what you want with a coprocess.
coproc TAIL { tail -Fn0 /tmp/report.txt ;}
while [ 1 ]
do
echo "pre"
break
echo "past"
done <&${TAIL[0]}
kill $TAIL_PID
http://www.gnu.org/software/bash/manual/html_node/Coprocesses.html
With older versions, you can use a background process writing to a named pipe:
pipe=/tmp/tail.$$
mkfifo $pipe
tail -Fn0 /tmp/report.txt >$pipe &
TAIL_PID=$!
while [ 1 ]
do
echo "pre"
break
echo "past"
done <$pipe
kill $TAIL_PID
rm $pipe
You can (unreliably) get away with killing the process group:
tail -Fn0 /tmp/report | while :
do
echo "pre"
sh -c 'PGID=$( ps -o pgid= $$ | tr -d \ ); kill -TERM -$PGID'
echo "past"
done
This may send the signal to more processes than you want. If you run the above command in an interactive terminal you should be okay, but in a script it is entirely possible (indeed likely) the the process group will include the script running the command. To avoid sending the signal, it would be wise to enable monitoring and run the pipeline in the background to ensure that a new process group is formed for the pipeline:
#!/bin/sh
# In Posix shells that support the User Portability Utilities option
# this includes bash & ksh), executing "set -m" turns on job control.
# Background processes run in a separate process group. If the shell
# is interactive, a line containing their exit status is printed to
# stderr upon their completion.
set -m
tail -Fn0 /tmp/report | while :
do
echo "pre"
sh -c 'PGID=$( ps -o pgid= $$ | tr -d \ ); kill -TERM -$PGID'
echo "past"
done &
wait
Note that I've replaced the while [ 1 ] with while : because while [ 1 ] is poor style. (It behaves exactly the same as while [ 0 ]).

executing bash loop while command is running

I want to build a bash script that executes a command and in the meanwhile performs other stuff, with the possibility of killing the command if the script is killed. Say, executes a cp of a large file and in the meanwhile prints the elapsed time since copy started, but if the script is killed it kills also the copy.
I don't want to use rsync, for 2 reasons: 1) is slow and 2) I want to learn how to do it, it could be useful.
I tried this:
until cp SOURCE DEST
do
#evaluates time, stuff, commands, file dimensions, not important now
#and echoes something
done
but it doesn't execute the do - done block, as it is waiting that the copy ends. Could you please suggest something?
until is the opposite of while. It's nothing to do with doing stuff while another command runs. For that you need to run your task in the background with &.
cp SOURCE DEST &
pid=$!
# If this script is killed, kill the `cp'.
trap "kill $pid 2> /dev/null" EXIT
# While copy is running...
while kill -0 $pid 2> /dev/null; do
# Do stuff
...
sleep 1
done
# Disable the trap on a normal exit.
trap - EXIT
kill -0 checks if a process is running. Note that it doesn't actually signal the process and kill it, as the name might suggest. Not with signal 0, at least.
There are three steps involved in solving your problem:
Execute a command in the background, so it will keep running while your script does something else. You can do this by following the command with &. See the section on Job Control in the Bash Reference Manual for more details.
Keep track of that command's status, so you'll know if it is still running. You can do this with the special variable $!, which is set to the PID (process identifier) of the last command you ran in the background, or empty if no background command was started. Linux creates a directory /proc/$PID for every process that is running and deletes it when the process exits, so you can check for the existence of that directory to find out if the background command is still running. You can learn more than you ever wanted to know about /proc from the Linux Documentation Project's File System Hierarchy page or Advanced Bash-Scripting Guide.
Kill the background command if your script is killed. You can do this with the trap command, which is a bash builtin command.
Putting the pieces together:
# Look for the 4 common signals that indicate this script was killed.
# If the background command was started, kill it, too.
trap '[ -z $! ] || kill $!' SIGHUP SIGINT SIGQUIT SIGTERM
cp $SOURCE $DEST & # Copy the file in the background.
# The /proc directory exists while the command runs.
while [ -e /proc/$! ]; do
echo -n "." # Do something while the background command runs.
sleep 1 # Optional: slow the loop so we don't use up all the dots.
done
Note that we check the /proc directory to find out if the background command is still running, because kill -0 will generate an error if it's called when the process no longer exists.
Update to explain the use of trap:
The syntax is trap [arg] [sigspec …], where sigspec … is a list of signals to catch, and arg is a command to execute when any of those signals is raised. In this case, the command is a list:
'[ -z $! ] || kill $!'
This is a common bash idiom that takes advantage of the way || is processed. An expression of the form cmd1 || cmd2 will evaluate as successful if either cmd1 OR cmd2 succeeds. But bash is clever: if cmd1 succeeds, bash knows that the complete expression must also succeed, so it doesn't bother to evaluate cmd2. On the other hand, if cmd1 fails, the result of cmd2 determines the overall result of the expression. So an important feature of || is that it will execute cmd2 only if cmd1 fails. That means it's a shortcut for the (invalid) sequence:
if cmd1; then
# do nothing
else
cmd2
fi
With that in mind, we can see that
trap '[ -z $! ] || kill $!' SIGHUP SIGINT SIGQUIT SIGTERM
will test whether $! is empty (which means the background task was never executed). If that fails, which means the task was executed, it kills the task.
here is the simplest way to do that using ps -p :
[command_1_to_execute] &
pid=$!
while ps -p $pid &>/dev/null; do
[command_2_to_be_executed meanwhile command_1 is running]
sleep 10
done
This will run every 10 seconds the command_2 if the command_1 is still running in background .
hope this will help you :)
What you want is to do two things at once in shell. The usual way to do that is with a job. You can start a background job by ending the command with an ampersand.
copy $SOURCE $DEST &
You can then use the jobs command to check its status.
Read more:
Gnu Bash Job Control

How to suppress Terminated message after killing in bash?

How can you suppress the Terminated message that comes up after you kill a
process in a bash script?
I tried set +bm, but that doesn't work.
I know another solution involves calling exec 2> /dev/null, but is that
reliable? How do I reset it back so that I can continue to see stderr?
In order to silence the message, you must be redirecting stderr at the time the message is generated. Because the kill command sends a signal and doesn't wait for the target process to respond, redirecting stderr of the kill command does you no good. The bash builtin wait was made specifically for this purpose.
Here is very simple example that kills the most recent background command. (Learn more about $! here.)
kill $!
wait $! 2>/dev/null
Because both kill and wait accept multiple pids, you can also do batch kills. Here is an example that kills all background processes (of the current process/script of course).
kill $(jobs -rp)
wait $(jobs -rp) 2>/dev/null
I was led here from bash: silently kill background function process.
The short answer is that you can't. Bash always prints the status of foreground jobs. The monitoring flag only applies for background jobs, and only for interactive shells, not scripts.
see notify_of_job_status() in jobs.c.
As you say, you can redirect so standard error is pointing to /dev/null but then you miss any other error messages. You can make it temporary by doing the redirection in a subshell which runs the script. This leaves the original environment alone.
(script 2> /dev/null)
which will lose all error messages, but just from that script, not from anything else run in that shell.
You can save and restore standard error, by redirecting a new filedescriptor to point there:
exec 3>&2 # 3 is now a copy of 2
exec 2> /dev/null # 2 now points to /dev/null
script # run script with redirected stderr
exec 2>&3 # restore stderr to saved
exec 3>&- # close saved version
But I wouldn't recommend this -- the only upside from the first one is that it saves a sub-shell invocation, while being more complicated and, possibly even altering the behavior of the script, if the script alters file descriptors.
EDIT:
For more appropriate answer check answer given by Mark Edgar
Solution: use SIGINT (works only in non-interactive shells)
Demo:
cat > silent.sh <<"EOF"
sleep 100 &
kill -INT $!
sleep 1
EOF
sh silent.sh
http://thread.gmane.org/gmane.comp.shells.bash.bugs/15798
Maybe detach the process from the current shell process by calling disown?
The Terminated is logged by the default signal handler of bash 3.x and 4.x. Just trap the TERM signal at the very first of child process:
#!/bin/sh
## assume script name is test.sh
foo() {
trap 'exit 0' TERM ## here is the key
while true; do sleep 1; done
}
echo before child
ps aux | grep 'test\.s[h]\|slee[p]'
foo &
pid=$!
sleep 1 # wait trap is done
echo before kill
ps aux | grep 'test\.s[h]\|slee[p]'
kill $pid ## no need to redirect stdin/stderr
sleep 1 # wait kill is done
echo after kill
ps aux | grep 'test\.s[h]\|slee[p]'
Is this what we are all looking for?
Not wanted:
$ sleep 3 &
[1] 234
<pressing enter a few times....>
$
$
[1]+ Done sleep 3
$
Wanted:
$ (set +m; sleep 3 &)
<again, pressing enter several times....>
$
$
$
$
$
As you can see, no job end message. Works for me in bash scripts as well, also for killed background processes.
'set +m' disables job control (see 'help set') for the current shell. So if you enter your command in a subshell (as done here in brackets) you will not influence the job control settings of the current shell. Only disadvantage is that you need to get the pid of your background process back to the current shell if you want to check whether it has terminated, or evaluate the return code.
This also works for killall (for those who prefer it):
killall -s SIGINT (yourprogram)
suppresses the message... I was running mpg123 in background mode.
It could only silently be killed by sending a ctrl-c (SIGINT) instead of a SIGTERM (default).
disown did exactly the right thing for me -- the exec 3>&2 is risky for a lot of reasons -- set +bm didn't seem to work inside a script, only at the command prompt
Had success with adding 'jobs 2>&1 >/dev/null' to the script, not certain if it will help anyone else's script, but here is a sample.
while true; do echo $RANDOM; done | while read line
do
echo Random is $line the last jobid is $(jobs -lp)
jobs 2>&1 >/dev/null
sleep 3
done
Another way to disable job notifications is to place your command to be backgrounded in a sh -c 'cmd &' construct.
#!/bin/bash
# ...
pid="`sh -c 'sleep 30 & echo ${!}' | head -1`"
kill "$pid"
# ...
# or put several cmds in sh -c '...' construct
sh -c '
sleep 30 &
pid="${!}"
sleep 5
kill "${pid}"
'
I found that putting the kill command in a function and then backgrounding the function suppresses the termination output
function killCmd() {
kill $1
}
killCmd $somePID &
Simple:
{ kill $! } 2>/dev/null
Advantage? can use any signal
ex:
{ kill -9 $PID } 2>/dev/null

Resources