当终端关闭时终止 sudo python 脚本

2024-01-03

如何判断运行 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 脚本完成的。


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

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

I 向上游报告了该问题 http://bugzilla.sudo.ws/show_bug.cgi?id=719, and 它现在在 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'

If your -cargument 是一个命令,bash 通过执行它来优化。跟踪另一个命令(微不足道的true在这种情况下),让 bash 继续运行并作为一个孩子运行 sudo 。我测试过,使用这种方法,当您关闭 xterm 时,sig-counter 会从内核收到一个 SIGHUP。 (对于任何其他终端模拟器来说应该是相同的。)

我已经测试过了,它可以与 bash 和 dash 一起使用。包含一个方便的无需退出的信号接收程序的源代码,您可以跟踪它以查看它接收到的所有信号。


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


POSIX 说 http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html that close()在伪终端的主端会导致这样的情况:“应将 SIGHUP 信号发送到控制进程(如果有),其中伪终端的从机端是控制终端。”

POSIX 的写法为close()意味着只能有一个以 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 下运行的进程不是。)

The sudo手册页说:

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

在我看来,sudo 的 SIGHUP 双信号避免逻辑是为作为交互式 shell 的子进程运行而设计的。当不涉及交互式 shell 时(之后exec sudo从交互式 shell 中,或者当一开始就没有涉及 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 +++

我不确定哪一部分可以完成工作。可能实际的退出是技巧,或者是它在启动命令之前所做的事情,因为kill and tcsetpgrp()都失败了。


我自己的第一次尝试是:

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 必须推迟它们,直到它不在无法正确锁定的事物中间 http://perldoc.perl.org/perlipc.html#Deferred-Signals-%28Safe-Signals%29.

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

当终端关闭时终止 sudo python 脚本 的相关文章

  • numpy python 中的“AttributeError:'matrix'对象没有属性'strftime'”错误

    我有一个维度为 72000 1 的矩阵 该矩阵涉及时间戳 我想使用 strftime 如下所示 strftime d m y 为了得到像这样的输出 11 03 02 我有这样一个矩阵 M np matrix timestamps 我使用了
  • ctypes 错误:libdc1394 错误:无法初始化 libdc1394

    我正在尝试将程序编译为共享库 我可以使用 ctypes 在 Python 代码中使用该库 使用以下命令该库可以正常编译 g shared Wl soname mylib O3 o mylib so fPIC files pkg config
  • NumPy linalg.eig

    我有这个烦人的问题 但我还没有弄清楚 我有一个矩阵 我想找到特征向量 所以我写 val vec np linalg eig mymatrix 然后我得到了 vec 我的问题是 当我小组中的其他人对相同的矩阵 mymatrix 做同样的事情时
  • 在 python 3 中使用子进程

    我使用 subprocess 模块在 python 3 中运行 shell 命令 这是我的代码 import subprocess filename somename py in practical i m using a real fil
  • 从sklearn PCA获取特征值和向量

    如何获取 PCA 应用程序的特征值和特征向量 from sklearn decomposition import PCA clf PCA 0 98 whiten True converse 98 variance X train clf f
  • 字符串中的注释和注释中的字符串

    我正在尝试使用 Python 和 Regex 计算 C 代码中包含的注释中的字符数 但没有成功 我可以先删除字符串以删除字符串中的注释 但这也会删除注释中的字符串 结果会很糟糕 是否有机会通过使用正则表达式来询问不匹配注释中的字符串 反之亦
  • 根据 Pandas 中的列表对多列进行排序

    感谢有关如何根据 pandas 中的倍数列表对给定多列进行排序的任何提示 如下所示 import pandas as pd sort a a d e sort b s1 s3 s6 sort c t1 t2 t3 df pd DataFra
  • 更改 x 轴比例

    我使用 Matlab 创建了这个图 使用 matplotlib x 轴绘制大数字 例如 100000 200000 300000 我想要 1 2 3 和 10 5 之类的值来指示它实际上是 100000 200000 300000 有没有一
  • 使用 Python 计算 Spark 中成对 (K,V) RDD 中每个 KEY 的平均值

    我想与 Python 共享这个特定的 Apache Spark 解决方案 因为它的文档非常贫乏 我想通过 KEY 计算 K V 对 存储在 Pairwise RDD 中 的平均值 示例数据如下所示 gt gt gt rdd1 take 10
  • 设置 verify_certs=False 但 elasticsearch.Elasticsearch 因证书验证失败而引发 SSL 错误

    self host KibanaProxy 自我端口 443 self user 测试 self password 测试 我需要禁止证书验证 使用选项时它与curl一起使用 k在命令行上 但是 在使用 Elasticsearch pytho
  • Linux TUN/TAP:无法从 TAP 设备读回数据

    问题是关于如何正确配置想要使用 Tun Tap 模块的 Linux 主机 My Goal 利用现有的路由软件 以下为APP1和APP2 但拦截并修改其发送和接收的所有消息 由Mediator完成 我的场景 Ubuntu 10 04 Mach
  • 使用 Conda 更新特定模块会删除大量软件包

    我最近开始使用 Anaconda Python 发行版 因为它提供了许多开箱即用的数据分析库 使用 conda 创建环境和安装软件包也轻而易举 但是当我想更新 Python 本身或任何其他模块时 我遇到了一些严重的问题 我事先被告知我的很多
  • Werkzeug 中的线程和本地代理。用法

    首先 我想确保我正确理解了功能的分配 分配本地代理功能以通过线程内的模块 包 共享变量 对象 我对吗 其次 用法对我来说仍然不清楚 也许是因为我误解了作业 我用烧瓶 如果我有两个 或更多 模块 A B 我想将对象C从模块A导入到模块B 但我
  • 为什么我应该使用 WSGI?

    使用 mod python 一段时间了 我读了越来越多关于 WSGI 有多好的文章 但没有真正理解为什么 那么我为什么要切换到它呢 有什么好处 这很难吗 学习曲线值得吗 为了用 Python 开发复杂的 Web 应用程序 您可能会使用更全面
  • `pyqt5'错误`元数据生成失败`

    我正在尝试安装pyqt5使用带有 M1 芯片和 Python 3 9 12 的 mac 操作系统 我怀疑M1芯片可能是原因 我收到一个错误metadata generation failed 最小工作示例 directly in the t
  • 在 Spyder 的变量资源管理器中查看局部变量

    我是 python 新手 正在使用 Spyder 的 IDE 我欣赏它的一项功能是它的变量资源管理器 然而 根据一些研究 我发现它只显示全局变量 我找到的解决方法是使用检查模块 import inspect local vars def m
  • 导入错误:无法导入名称“时间戳”

    我使用以下代码在 python 3 6 3 中成功安装了 ggplot conda install c conda forge ggplot 但是当我使用下面的代码将其导入笔记本时 出现错误 from ggplot import Impor
  • gdb查找行号的内存地址

    假设我已将 gdb 附加到一个进程 并且在其内存布局中有一个文件和行号 我想要其内存地址 如何获取文件x中第n行的内存地址 这是在 Linux x86 上 gdb info line test c 56 Line 56 of test c
  • 操作错误:(sqlite3.OperationalError) SQL 变量太多,同时将 SQL 与数据帧一起使用

    我有一个熊猫数据框 如下所示 activity User Id 0 VIEWED MOVIE 158d292ec18a49 1 VIEWED MOVIE 158d292ec18a49 2 VIEWED MOVIE 158d292ec18a4
  • 通过 Web 界面执行 python 单元测试

    是否可以通过 Web 界面执行单元测试 如果可以 如何执行 EDIT 现在我想要结果 对于测试 我希望它们是自动化的 可能每次我对代码进行更改时 抱歉我忘了说得更清楚 EDIT 这个答案此时已经过时了 Use Jenkins https j

随机推荐