首页 文章

终端关闭时终止sudo python脚本

提问于
浏览
3

如何判断运行我的python脚本的终端是否已关闭?如果用户关闭终端,我想安全地结束我的python脚本 . 我可以使用处理程序捕获SIGHUP,但不能在脚本以sudo身份运行时捕获 . 当我用sudo启动脚本并关闭终端时,python脚本继续运行 .

示例脚本:

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)

有时,脚本将在以sudo身份运行时执行处理程序,但更常见的情况是不执行 . 在没有sudo的情况下运行时脚本总是写入文件 . 我在Raspberry Pi上运行它 . 我在LXTerminal和gnome-terminal中看到了相同的东西 . 这个示例脚本将在50秒后结束,但是我的冗长代码在无限循环中运行

最终目标是在Raspberry Pi上安装一个.desktop启动器来进行蓝牙扫描和查找设备 . 蓝牙扫描需要sudo,因为它使用4.0 BLE . 我不确定为什么bluez需要sudo,但确实如此 . 在pi上输入sudo时,它从不要求输入密码,这对我来说很好 . 问题是关闭终端后,扫描过程仍在运行 . 扫描由在终端中运行的python脚本完成 .

1 回答

  • 6

    sudo是为TIGHUP语义而设计的,当它是tty上某个其他进程的子进程时你会得到它 . 在这种情况下,当父进程退出时,所有进程都从内核获得自己的SIGHUP .

    xterm -e sudo cmd 直接在伪终端上运行sudo . 这产生了与sudo期望的不同的SIGHUP语义 . 只有sudo从内核接收一个SIGHUP,并且只有当它的子进程也有它自己的时候才会从内核获得一个SIGHUP(因为sudo的父进程(例如bash)会这样做) .

    reported the issue upstreamit's now marked as fixed in sudo 1.8.15 and onwards .

    解决方法:

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

    如果 -c 参数是单个命令,则bash通过执行它来进行优化 . 解决另一个命令(在这种情况下是微不足道的 true ),让bash坚持并在孩提时间运行sudo . 我测试过,使用这种方法,当你关闭xterm时,sig-counter从内核中获取一个SIGHUP . (对于任何其他终端仿真器,它应该是相同的 . )

    我测试了这个,它适用于bash和dash . 源代码包括一个方便的花花公子信号接收 - 不退出程序,您可以串行查看它收到的所有信号 .


    本答案其余部分的某些部分可能略微不同步 . 在找出sudo作为控制过程与sudo作为shell差异的孩子之前,我经历了一些理论和测试方法 .


    POSIX says,伪终端主端的 close() 导致:"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."

    close() 的POSIX措辞意味着只有一个处理过程将pty作为其控制终端 .

    当bash是pty的slave端的控制进程时,它会导致所有其他进程接收SIGHUP . 这是sudo期待的语义 .

    ssh localhost ,然后中止与 ~. 的连接或终止您的ssh客户端 .

    $ 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
    

    测试这个很棘手,因为在退出和关闭pty之前 xterm 也会自己发送一个SIGHUP . 其他终端仿真器(gnome-terminal,konsole)可能会也可能不会这样做 . 我不得不自己写一个信号测试程序,不仅仅是在第一次SIGHUP之后就死了 .

    除非xterm以root身份运行,否则它无法向sudo发送信号,因此sudo只能从内核获取信号 . (因为它是tty的控制进程,而在sudo下运行的进程不是 . )

    sudo 手册页说:

    除非命令在新的pty中运行,否则SIGHUP,SIGINT和SIGQUIT信号不会被中继,除非它们是由用户进程而不是内核发送的 . 否则,每次用户输入control-C时,该命令将接收SIGINT两次 .

    它在我看来像sudo 's double-signal avoidance logic for SIGHUP was designed for running as a child of an interactive shell. When there'没有涉及交互式shell(在交互式shell之后 exec sudo ,或者当首先没有涉及shell时),只有父进程(sudo)获得一个SIGHUP .

    即使在没有涉及shell的xterm中,sudo的行为也适用于SIGINT和SIGQUIT:在xterm中按^ C或^ \后, sig-counter 只接收一个SIGINT或SIGQUIT . sudo 收到一个并且不转发它 . si_code=SI_KERNEL 在这两个过程中 .


    在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
    

    sudo 下运行的命令仅在xterm关闭时才会看到SIGCONT .

    请注意,单击xterm Headers 栏上的窗口管理器关闭按钮只是让xterm手动发送一个SIGHUP . 通常这会导致xterm中的进程关闭,在这种情况下xterm将在此之后退出 . 同样,这只是xterm的行为 .


    这是 bash 在获取SIGHUP时所做的事情,产生 sudo 期望的行为:

    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 +++
    

    我不确定这部分工作是完成的 . 可能实际退出是诀窍,或者在启动命令之前它做了什么,因为 killtcsetpgrp() 都失败了 .


    我自己尝试尝试的第一次尝试是:

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

    (其中pts / 11是另一个终端 . ) sleep 在第一个SIGHUP之后退出,因此没有sudo的测试只显示xterm手动发送的SIGHUP .

    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]);
            }
        }
    }
    

    我对此的第一次尝试是perl,但安装信号处理程序并没有阻止perl在信号处理程序返回后退出SIGHUP . 我看到xterm关闭之前就出现了这条消息 .

    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" &
    

    显然perl信号处理相当复杂,因为perl必须defer them until it's not in the middle of something that doesn't do proper locking .

    C中的Unix系统调用是进行系统编程的“默认”方式,因此可以解决任何可能的混淆 . strace通常是一种廉价的方法来避免实际编写日志/打印代码来玩游戏 . :P

相关问题