Terminate sudo python script when the terminal closes
Question: How can I tell if the terminal running my python script was closed? I want to
safely end my python script if the user closes the terminal. I can catch
SIGHUP with a handler, but not when the script is run as sudo. When I start
the script with sudo and close the terminal, the python script keeps running.

Example script:

    
    
    import signal
    import time
    import sys
    
    def handler(signum, frame):
        fd = open ("tmp.txt", "a")
        fd.write(str(signum) + " handled\n")
        fd.close()
        sys.exit(0)
    
    
    signal.signal(signal.SIGHUP, handler)
    signal.signal(signal.SIGINT, handler)
    signal.signal(signal.SIGTERM, handler)
    
    time.sleep(50)
    

Sometimes the script will execute the handler when run as sudo, but more often
it doesn't. The script always writes to the file when ran without sudo. I am
running it on a Raspberry Pi. I see the same thing in a LXTerminal and a
gnome-terminal. This example script will end after 50 seconds, but my lengthy
code runs in an infinite loop

The ultimate goal is to have a .desktop launcher on a Raspberry Pi to do
bluetooth scanning and find devices. The bluetooth scanning requires sudo
because it use 4.0 BLE. I'm not sure why bluez requires sudo but it does. When
type sudo on the pi, it never asks for a password which is fine with me. The
problem is that after closing the terminal, the scan process is still running.
The scanning is done by a python script that runs in a terminal.


Answer: sudo is designed for the SIGHUP semantics you get when it's a child of some
other process on the tty. In that case, all processes get their own SIGHUP
from the kernel when the parent exits.

`xterm -e sudo cmd` runs sudo directly on the pseudo-terminal. This produces
different SIGHUP semantics than sudo is expecting. Only sudo receives a SIGHUP
from the kernel, and doesn't relay it because it's expecting that it gets a
SIGHUP from the kernel only when its child process also got its own (because
of something sudo's parent (e.g. bash) does).

I [reported the issue upstream](http://bugzilla.sudo.ws/show_bug.cgi?id=719),
and **it's now marked as fixed in sudo 1.8.15 and onwards**.

## Workaround:

    
    
    xterm -e 'sudo ./sig-counter; true'
    
    # or for uses that don't implicitly use a shell:
    xterm -e sh -c 'sudo some-cmd; true'
    

If your `-c` argument is a single command, bash optimizes by execing it.
Tacking another command (the trivial `true` in this case), gets bash to stick
around and run sudo as a child. I tested, and with this method, sig-counter
gets one SIGHUP from the kernel when you close xterm. (It should be the same
for any other terminal emulator.)

I've tested this, and it works with bash and dash. Source included for a
handy-dandy signal-receiving-without-exiting program which you can strace to
see all the signals it receives.

* * *

Some parts of the rest of this answer may be slightly out of sync. I went
through a few theories and testing methods before figuring out the sudo as
controlling process vs. sudo as child of a shell difference.

* * *

[POSIX
says](http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html)
that `close()` on the master end of a pseudo-terminal causes this: "a SIGHUP
signal shall be sent to the controlling process, if any, for which the slave
side of the pseudo-terminal is the controlling terminal."

The POSIX wording for `close()` implies there can be only one processing
process that has the pty as its controlling terminal.

When bash is the controlling process for the slave side of a pty, it does
something that causes all other processes to receive a SIGHUP. This is the
semantics sudo is expecting.

`ssh localhost`, then abort the connection with `~.` or kill your ssh client.

    
    
    $ ssh localhost
    ssh$ sudo ~/.../sig-counter  # without exec
       # on session close: gets a SIGHUP and a SIGCONT from the kernel
    
    $ ssh localhost
    ssh$ exec sudo ~/src/experiments-sys/sig-counter
       # on session close: gets only a SIGCONT SI_USER relayed from sudo
    
    $ ssh -t localhost sudo ~/src/experiments-sys/sig-counter
       # on session close: gets only a SIGCONT SI_USER relayed from sudo
    
    $ xterm -e sudo ./sig-counter
               # on close: gets only a SIGCONT SI_USER relayed from sudo
    

* * *

Testing this was tricky, because `xterm` also sends a SIGHUP on its own,
before exiting and closing the pty. Other terminal emulators (gnome-terminal,
konsole) may or may not do this. I had to write a signal-testing program
myself to not just die after the first SIGHUP.

Unless xterm is running as root, it can't send signals to sudo, so sudo only
gets the signals from the kernel. (Because it is the controlling process for
the tty, and the process running under sudo isn't.)

The `sudo` man page says:

> Unless the command is being run in a new pty, the SIGHUP, SIGINT and SIGQUIT
> signals are not relayed unless they are sent by a user process, not the
> kernel. Otherwise, the command would receive SIGINT twice every time the
> user entered control-C.

It looks to me like sudo's double-signal avoidance logic for SIGHUP was
designed for running as a child of an interactive shell. When there's no
interactive shell involved (after `exec sudo` from an interactive shell, or
when there was no shell involved in the first place), only the parent process
(sudo) gets a SIGHUP.

sudo's behaviour is good for SIGINT and SIGQUIT, even in an xterm with no
shell involved: after pressing ^C or ^\ in the xterm, `sig-counter` receives
exactly one SIGINT or SIGQUIT. `sudo` receives one and doesn't relay it.
`si_code=SI_KERNEL` in both processes.

* * *

Tested on Ubuntu 15.04, `sudo --version`: 1.8.9p5. `xterm -v`: XTerm(312).

    
    
    ###### No sudo
    $ pkill sig-counter; xterm -e ./sig-counter &
    
    $ strace -p $(pidof sig-counter)
    Process 19446 attached
       quit xterm (ctrl-left click -> quit)
    rt_sigtimedwait(~[TERM RTMIN RT_1], {si_signo=SIGHUP, si_code=SI_USER, si_pid=19444, si_uid=1000}, NULL, 8) = 1  # from xterm
    rt_sigtimedwait(~[TERM RTMIN RT_1], {si_signo=SIGHUP, si_code=SI_KERNEL}, NULL, 8) = 1    # from the kernel
    rt_sigtimedwait(~[TERM RTMIN RT_1], {si_signo=SIGCONT, si_code=SI_KERNEL}, NULL, 8) = 18   # from the kernel
       sig-counter is still running, because it only exits on SIGTERM
    
     #### with sudo, attaching to sudo and sig-counter after the fact
     # Then send SIGUSR1 to sudo
     # Then quit xterm
    
     $ sudo pkill sig-counter; xterm -e sudo ./sig-counter &
     $ sudo strace -p 20398  # sudo's pid
    restart_syscall(<... resuming interrupted call ...>) = ? 
    ERESTART_RESTARTBLOCK (Interrupted by signal)
    --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=20540, si_uid=0} ---
    write(7, "\n", 1)                       = 1   # FD 7 is the write end of a pipe. sudo's FD 6 is the other end.  Some kind of deadlock-avoidance?
    rt_sigreturn()                          = -1 EINTR (Interrupted system call)
    poll([{fd=6, events=POLLIN}], 1, 4294967295) = 1 ([{fd=6, revents=POLLIN}])
    read(6, "\n", 1)                        = 1
    kill(20399, SIGUSR1)                    = 0   ##### Passes it on to child
    read(6, 0x7fff67d916ab, 1)              = -1 EAGAIN (Resource temporarily unavailable)
    poll([{fd=6, events=POLLIN}], 1, 4294967295
    
         ####### close xterm
    --- SIGHUP {si_signo=SIGHUP, si_code=SI_KERNEL} ---
    rt_sigreturn()                          = -1 EINTR (Interrupted system call)
    --- SIGCONT {si_signo=SIGCONT, si_code=SI_KERNEL} ---   ### sudo gets both SIGHUP and SIGCONT
    write(7, "\22", 1)                      = 1
    rt_sigreturn()                          = -1 EINTR (Interrupted system call)
    poll([{fd=6, events=POLLIN}], 1, 4294967295) = 1 ([{fd=6, revents=POLLIN}])
    read(6, "\22", 1)                       = 1
    kill(20399, SIGCONT)                    = 0   ## but only passes on SIGCONT
    read(6, 0x7fff67d916ab, 1)              = -1 EAGAIN (Resource temporarily unavailable)
    poll([{fd=6, events=POLLIN}], 1, 4294967295
    ## keeps running after xterm closes
    
     $ sudo strace -p $(pidof sig-counter)  # in another window
    rt_sigtimedwait(~[RTMIN RT_1], {si_signo=SIGUSR1, si_code=SI_USER, si_pid=20398, si_uid=0}, NULL, 8) = 10
    rt_sigtimedwait(~[RTMIN RT_1], {si_signo=SIGCONT, si_code=SI_USER, si_pid=20398, si_uid=0}, NULL, 8) = 18
    ## keeps running after xterm closes
    

The command running under `sudo` only sees a SIGCONT when the xterm closes.

Note that clicking the window-manager's close button on xterm's titlebar just
makes xterm send a SIGHUP manually. Often this will cause the process inside
xterm to close, in which case xterm exits after that. Again, this is just
xterm's behaviour.

* * *

This is what `bash` does when it gets SIGHUP, producing the behaviour `sudo`
expects:

    
    
    Process 26121 attached
    wait4(-1, 0x7ffc9b8c78c0, WSTOPPED|WCONTINUED, NULL) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
    --- SIGHUP {si_signo=SIGHUP, si_code=SI_KERNEL} ---
    --- SIGCONT {si_signo=SIGCONT, si_code=SI_KERNEL} ---
       ... write .bash history ...
    kill(4294941137, SIGHUP)                = -1 EPERM (Operation not permitted)  # This is kill(-26159), which signals all processes in that process group
    rt_sigprocmask(SIG_BLOCK, [CHLD TSTP TTIN TTOU], [CHLD], 8) = 0
    ioctl(255, SNDRV_TIMER_IOCTL_SELECT or TIOCSPGRP, [26121]) = -1 ENOTTY (Inappropriate ioctl for device) # tcsetpgrp()
    rt_sigprocmask(SIG_SETMASK, [CHLD], NULL, 8) = 0
    setpgid(0, 26121)                       = -1 EPERM (Operation not permitted)
    rt_sigaction(SIGHUP, {SIG_DFL, [], SA_RESTORER, 0x7f3b25ebf2f0}, {0x45dec0, [HUP INT ILL TRAP ABRT BUS FPE USR1 SEGV USR2 PIPE ALRM TERM XCPU XFSZ VTALRM SYS], SA_RESTORER, 0x7f3b25ebf2f0}, 8) = 0
    kill(26121, SIGHUP)                     = 0 ## exit in a way that lets bash's parent see that SIGHUP killed it.
    --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=26121, si_uid=1000} ---
    +++ killed by SIGHUP +++
    

I'm not sure which part of this gets the job done. Probably the actual exiting
is the trick, or something it did before launching the command, since `kill`
and `tcsetpgrp()` both failed.

* * *

My first attempt at trying it myself was:

    
    
    xterm -e sudo strace -o /dev/pts/11 sleep 60
    

(where pts/11 is another terminal.) `sleep` exits after the first SIGHUP, so
testing without sudo just shows the SIGHUP sent manually by xterm.

sig-counter.c:

    
    
    // sig-counter.c.
    // http://stackoverflow.com/questions/32511170/terminate-sudo-python-script-when-the-terminal-closes
    // gcc -Wall -Os -std=gnu11 sig-counter.c -o sig-counter
    #include <stdio.h>
    #include <signal.h>
    #include <unistd.h>
    #include <errno.h>
    
    #define min(x, y) ({                \
        typeof(x) _min1 = (x);          \
        typeof(y) _min2 = (y);          \
        (void) (&_min1 == &_min2);      \
        _min1 < _min2 ? _min1 : _min2; })
    
    int sigcounts[64];
    static const int sigcount_size = sizeof(sigcounts)/sizeof(sigcounts[0]);
    
    void handler(int sig_num)
    {
        sig_num = min(sig_num, sigcount_size);
        sigcounts[sig_num]++;
    }
    
    int main(void)
    {
        sigset_t sigset;
        sigfillset(&sigset);
        // sigdelset(&sigset, SIGTERM);
    
        if (sigprocmask(SIG_BLOCK, &sigset, NULL))
            perror("sigprocmask: ");
    
        const struct timespec timeout = { .tv_sec = 60 };
        int sig;
        do {
            // synchronously receive signals, instead of installing a handler
            siginfo_t siginfo;
            int ret = sigtimedwait(&sigset, &siginfo, &timeout);
            if (-1 == ret) {
                if (errno == EAGAIN) break; // exit after 60 secs with no signals
                else continue;
            }
            sig = siginfo.si_signo;
    //      switch(siginfo.si_code) {
    //      case SI_USER:  // printf some stuff about the signal... just use strace
    
            handler(sig);
        } while (sig != SIGTERM );
    
        //sigaction(handler, ...);
        //sleep(60);
        for (int i=0; i<sigcount_size ; i++) {
            if (sigcounts[i]) {
                printf("counts[%d] = %d\n", i, sigcounts[i]);
            }
        }
    }
    

My first attempt at this was perl, but installing a signal handler wasn't
stopping perl from exitting on SIGHUP after the signal handler returned. I saw
the message appear right before xterm closed.

    
    
    cmd=perl\ -e\ \''use strict; use warnings; use sigtrap qw/handler signal_handler normal-signals/; sleep(60); sub signal_handler { print "Caught a signal $!"; }'\';
    xterm -e "$cmd" &
    

Apparently perl signal handling is fairly complicated because perl has to
[defer them until it's not in the middle of something that doesn't do proper
locking](http://perldoc.perl.org/perlipc.html#Deferred-Signals-%28Safe-
Signals%29).

Unix syscalls in C is the "default" way to do systems programming, so that
takes out any possible confusion. strace is often a cheap way to avoid
actually writing logging / printing code for playing around with stuff. :P

