3

I have a script like this (written for /bin/sh on OpenBSD) which first updates a local copy of some CVS repositories using rsync, and then updates the checked-out version of these on my machine. The script is run as root through OpenBSD's doas (a sudo "equivalent" on OpenBSD):

#!/bin/sh

int_handler () {
    echo 'Wait...' >&2
    quit=1
}

quit=0
trap int_handler INT

for r in CVSROOT src xenocara ports; do
    su cvsuser -c 'rsync --archive --itemize-changes --delete --omit-dir-times \
        "rsync://anoncvs.eu.openbsd.org/OpenBSD-cvs/$r/" \
        "/extra/cvs/$r/"'
done

trap - INT

[ "$quit" -eq 1 ] && exit

# rest of script updates checked-out CVS repositories from local copy

The idea is that I may press Ctrl+C while the rsync loop is running to later skip the second part of the script (which I may not always want to run, depending on what has been updated in the repositories) without interrupting the loop and its su+rsync processes.

This does not work and the rsync process receives the signal and terminates (and the next iteration of the for loop continues). rsync says (when pressing Ctrl+C multiple times until the script terminates):

^Crsync error: received SIGINT, SIGTERM, or SIGHUP (code 20) at rsync.c(642) [generator=3.1.3]
rsync error: received SIGINT, SIGTERM, or SIGHUP (code 20) at io.c(504) [receiver=3.1.3]
Wait...
rsync: [receiver] write error: Broken pipe (32)
rsync error: received SIGINT, SIGTERM, or SIGHUP (code 20) at io.c(1633) [sender=3.1.3]
^Crsync error: received SIGINT, SIGTERM, or SIGHUP (code 20) at rsync.c(642) [generator=3.1.3]rsync error: received SIGINT, SIGTERM, or SIGHUP (code 20) at io.c(504) [receiver=3.1.3
]

rsync: [receiver] write error: Broken pipe (32)
Wait...
^Crsync error: received SIGINT, SIGTERM, or SIGHUP (code 20) at rsync.c(642) [generator=3.1.3]
rsync error: received SIGUSR1 (code 19) at main.c(1440) [receiver=3.1.3]
Wait...

According to an answer to the question "How to make ctrl+c /not/ interrupt the while-loop?", at least when running bash, one should be able to ignore the INT signal, which will make child processes also ignore that signal. This is not ideal, but I'd be happy to try.

So, second try in the light of that:

#!/usr/local/bin/bash

trap '' INT

for r in CVSROOT src xenocara ports; do
    su cvsuser -c 'rsync --archive --itemize-changes --delete --omit-dir-times \
        "rsync://anoncvs.eu.openbsd.org/OpenBSD-cvs/$r/" \
        "/extra/cvs/$r/"'
done

trap - INT

read -p 'Press enter or interrupt...' junk

# rest of script updates checked-out CVS repositories from local copy

This likewise allows the rsync process to be interrupted by Ctrl+C.

Ok, so maybe it's the su that resets the signal mask? A third try:

#!/usr/local/bin/bash

trap '' INT

for r in CVSROOT src xenocara ports; do
    su -s /usr/local/bin/bash cvsuser -c \
        'trap "" INT; rsync --archive --itemize-changes --delete --omit-dir-times \
        "rsync://anoncvs.eu.openbsd.org/OpenBSD-cvs/$r/" \
        "/extra/cvs/$r/"'
done

trap - INT

read -p 'Press enter or interrupt...' junk

# rest of script updates checked-out CVS repositories from local copy

Nope, no joy. The rsync process is still interruptible in the same way.

Is there a way to get the script working the way I (initially) intended?

bash on my machine is release 4.4.23. The /bin/sh is the native pdksh variant on OpenBSD running in POSIX mode.

5

So, since you don't really want an interrupt, how about disabling it on the terminal level, with stty intr ""? That would make the ^C appear in the input buffer as any ordinary character, and we can read it from there.

This works for me on Linux:

#!/bin/bash

t=$(stty -g)
stty intr ""
echo please hold
sleep 5
stty "$t"

# short timeout, just check if there was any input earlier
read -n1 -t0.1 a
[[ "$a" = $'\003' ]] && echo "you hit ^C"

(I can't test on OpenBSD, but I suppose the terminal setup stuff is standard.)

  • This works. It's just unfortunate that I'd have to use bash to do it (catching the ^C at the end). But, when testing, it seems as if I can use a plain read stuff and then compare with if [ "$stuff" = '^C' ] where ^C is a literal Ctrl+C inserted with Ctrl+V Ctrl+C in Vim. So, that works. – Kusalananda Jun 16 '18 at 11:23
  • But then again, I could just have fed any predetermined string into the terminal (not just ^C), read it after the loop and quit conditionally. So that kinda makes the stty exercise unnecessary in a sense. – Kusalananda Jun 16 '18 at 11:26
  • @Kusalananda, yeah, and this can't give feedback for the ^C immediately (like your Wait... message). Having read -t would be useful though, even if you do change the skip key to enter instead. – ilkkachu Jun 16 '18 at 11:44
3

rsync installs a handler on SIGINT even if SIGINT was ignored upon start. So if you don't want ^C to stop rsync (I'd hate that myself as I hate it when the emergency button does not work), you'd have to make sure SIGINT is not delivered to rsync.

So either tell the terminal driver not to send a SIGINT to the foreground process group upon ^C like in @ikkachu's answer, or put the process running that rsync command out of the foreground process group.

Like everything related to signal handling, whether that works or not varies from shell to shell (and the version thereof sometimes).

Your best bet is stick with a known implementation which you know works and hope it will still work in newer versions in a few years time. Like with zsh:

#! /usr/bin/env zsh

# enable job control (run pipelines in different process groups)
set -o monitor

interrupted() false
trap 'echo Wait...; interrupted() true' INT

# run in background so it won't get the SIGINT upon ^C
# as it won't be in the foreground process group. Note that with
# version 5.1.1 at least, running it without & will also run in
# in a new process group, and not make it the foreground process
# group of the terminal as the shell is not interactive. However
# the trap would be run asynchronously.
rsync... &

while (($#jobstates)) {wait}

interrupted && exit

echo rest of the script
2

On Linux you could use the command setsid to run the rsync in a separate session. OpenBSD does not seem to have this as a command, though the system call exists. You could write your own C version of the command, or try this bit of perl if you have it:

#!/usr/bin/perl
use POSIX qw(setsid);
POSIX::setsid();
exec @ARGV;

If you name this script mysetsid you would use it as a prefix to the su command, eg: ./mysetsid su cvsuser ... in the first version of your script. Note, the interrupt is only seen by the handler function once the current rsync command finishes.

1

Bash implementation

It's pretty clear from experimentation that rsync behaves like other tools such as ping and do not inherit signals from the calling Bash parent.

So you have to get a little creative with this and do something like the following:

$ cat rsync.bash
#!/bin/sh

 set -m
 trap '' SIGINT SIGTERM EXIT
 rsync -avz LargeTestFile.500M root@host.mydom.com:/tmp/. &
 wait

 echo FIN

Now when I run it:

$ ./rsync.bash
X11 forwarding request failed
building file list ... done
LargeTestFile.500M
^C^C^C^C^C^C^C^C^C^C
sent 509984 bytes  received 42 bytes  92732.00 bytes/sec
total size is 524288000  speedup is 1027.96
FIN

And we can see the file did fully transfer:

$ ll -h | grep Large
-rw-------. 1  501 games 500M Jul  9 21:44 LargeTestFile.500M

How it works

The trick here is we're telling Bash via set -m to disable job controls on any background jobs within it. We're then backgrounding the rsync and then running a wait command which will wait on the last run command, rsync, until it's complete.

We then guard the entire script with the trap '' SIGINT SIGTERM EXIT.

References

  • This should work in any sh, right? Not just bash. – Kusalananda Jul 10 '18 at 6:17
  • 1
    @Kusalananda - I don't know much about zsh, the switching for the set -m is different, but otherwise I believe the approach is pretty darn close to Stephane's. I would accept his answer of the ones here, IMO. BTW, I had to do all the SIGs I showed, doing just INT wasn't sufficient. – slm Jul 10 '18 at 6:19
  • 1
    @Kusalananda - I thought of merging them, but I generally don't like to do that to his or Gilles' just out of hand. – slm Jul 10 '18 at 6:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.