【问题标题】:Terminate sudo python script when the terminal closes终端关闭时终止 sudo python 脚本
【发布时间】:2015-09-10 20:36:51
【问题描述】:

如何判断运行我的 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 的情况下运行时总是写入文件。我在树莓派上运行它。我在 LXTerminal 和 gnome-terminal 中看到了同样的情况。此示例脚本将在 50 秒后结束,但我的冗长代码在无限循环中运行

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

【问题讨论】:

  • 如果 shell 以用户权限运行而脚本以 root 权限运行,则 shell 可能只是缺少向脚本发送信号的权限。即使 shell 尝试发送 SIGHUP,在这种情况下脚本也不会收到它。如果 shell 也以 root 权限运行,它可以发送 SIGHUP——它工作时可能就是这种情况。
  • 当您以 root 或 sudo 启动终端并运行脚本时,它的行为是否如此?
  • 当我以 root 身份启动终端时调用处理程序!我还注意到,当脚本作为 sudo 运行时,我需要 sudo 使用“kill”命令发送信号。该脚本从桌面图标启动。你知道如何让桌面启动器以 root 身份启动终端吗?
  • 这听起来很像XY problem。你真正想要完成什么?以sudo 运行脚本几乎要求它在此之后不能依赖用户交互。一个常见的解决方法是让特权程序使用命名管道(或其他一些 RPC 机制)设置通信通道,以便授权用户可以继续从他们的常规帐户与其通信。不过,这对于您的程序所做的事情可能有点过分了;但如果没有更多细节,我们真的无法提供帮助
  • @SvenMarnach:shell 尝试发送 SIGHUP,但它没有以 root 身份运行,因此它不可能发出 sudo 信号。它工作的唯一方法是内核将 SIGHUP 发送给 sudo 的孩子(和 sudo),因为 sudo 旨在不中继内核发送的 SIGHUP。是的,它相当参与。如果您查看我的答案的编辑历史记录,您会发现我花了大约 3 个阶段来弄清楚发生了什么,并得出了不同的结论。

标签: python linux raspberry-pi


【解决方案1】:

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

xterm -e sudo cmd 直接在伪终端上运行 sudo。这会产生与 sudo 预期不同的 SIGHUP 语义。只有 sudo 接收来自内核的 SIGHUP,并且不会转发它,因为它期望只有在其子进程也有自己的子进程时(因为 sudo 的父进程(例如 bash)会这样做)才能从内核获取 SIGHUP。

reported the issue upstream现在在 sudo 1.8.15 及更高版本中标记为已修复

解决方法:

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。包含一个方便的信号接收程序的源代码,无需退出程序,您可以 strace 以查看它接收到的所有信号。


此答案的其余部分可能略有不同步。在弄清楚 sudo 作为控制进程与 sudo 作为 shell 的子进程之前,我经历了一些理论和测试方法。


POSIX says 伪终端主端的close() 导致:“应向控制进程发送 SIGHUP 信号,如果有的话,伪终端的从端是控制进程。终端。”

close() 的 POSIX 措辞意味着只能有一个处理进程以 pty 作为其控制终端。

当 bash 是 pty 的从属端的控制进程时,它会执行一些导致所有其他进程接收 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

对此进行测试很棘手,因为xterm 在退出和关闭 pty 之前也会自行发送 SIGHUP。其他终端仿真器(gnome-terminal、konsole)可能会也可能不会这样做。我必须自己编写一个信号测试程序,以免在第一次 SIGHUP 后就死掉。

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

sudo 手册页说:

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

在我看来,sudo 的 SIGHUP 双信号避免逻辑是为作为交互式 shell 的子级运行而设计的。当不涉及交互式 shell 时(在交互式 shell 中的exec sudo 之后,或者当首先没有涉及到 shell 时),只有父进程 (sudo) 获得 SIGHUP。

sudo 的行为对 SIGINT 和 SIGQUIT 是好的,即使在不涉及 shell 的 xterm 中:在 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 标题栏上的窗口管理器的关闭按钮只会使 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

【讨论】:

  • 我没有xterm,但是如果你以sudo方式启动终端本身,应该会发送信号。 “sudo xterm -e sudo strace -o /dev/pts/11 sleep 60”我不知道如何将其合并到 .desktop 启动器中。这是拼图的最后一块......
  • @petEEy:控制 tty 关闭时发送信号的是内核,而不是控制伪终端另一端的进程。从sudo sleep 的角度来看,如果它在关闭的xterm 中运行,或者如果它在连接到挂起的调制解调器的串行端口上运行,都是一样的。但是,sudo xterm 确实会导致发送信号。我猜这与 tty 设备的所有权有关。 POSIX 几乎可以肯定地说明了为什么会发生这种情况。
  • @petEEy:嗯,实际上我可能在这里弄错了。 strace输出表明信号源为SI_USER,pid为xterm的pid。
  • @petEEy:好的,我明白了。 sudo 没有传递 SIGHUP,因为它认为它的孩子会从内核中获得它自己的一个,但这并没有发生。 POSIX 说不应该,所以 sudo 坏了。
  • @petEEy:找到了解决方法。 sudo 并没有完全损坏,但它假定它是其他东西的孩子。我不确定它应该如何检查它是进程组组长还是它的名字。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-07-17
  • 1970-01-01
  • 1970-01-01
  • 2014-11-07
  • 2020-08-16
  • 1970-01-01
  • 2019-04-27
相关资源
最近更新 更多