'Terminate sudo python script when the terminal closes

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.



Solution 1:[1]

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

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

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1