当父级需要 tty 输入时停止 bash 子 shell

当父级需要 tty 输入时停止 bash 子 shell

我有一个问题,父进程需要来自 TTY 的输入,但子进程是bash忽略 SIGTTIN,继续运行并干扰。有没有什么办法可以让自己bash玩得好呢?

细节

这是在 Alpine Linux 3.9 和bash4.4.19 上。

为了简单起见,假设我们有 command wrapper,它会执行一些操作,然后分叉一个子命令。所以本质上是作为子shellwrapper bash运行的。bash

在我的情况下,wrapper它本身包装在一个函数中,如下所示:

function wrap() {
    wrapper bash -l
}

因此,我在 1 处运行 bash $SHLVL,然后输入wrap2 处的值$SHLVL。我正在子 shell 中使用包装器提供的增强功能进行工作。我使用bash子 shell 作为常规交互式 shell,因此我仍然需要作业控制。使用set +m或禁用作业控制set +o monitor是不可接受的。

wrapper当想要从 TTY 读取数据时就会出现问题。它尝试读取并收到 SIGTTIN。此时我在后台被踢回$SHLVL1 。wrapper不幸的是,$SHLVL2bash没有收到信号,并且仍在输出 shell 提示,尝试从 TTY 读取,但现在正在获取 EOF,如果我不小心(也许幸运),它就会退出,所以我的第一个字符是fg并将其带到wrapper前台并从 TTY 中读取。

这是一个不稳定的情况,我想要一些稳健的东西。我只是不知道该怎么办。我使用的是非图形终端,所以我不能打开另一个窗口。我猜我无法开始openvt工作,因为它需要在图形环境中运行。我尝试写一个bash像这样的脚本

#!/bin/bash -m
trap "echo parent TTIN" TTIN
bash &
wait $!

但并没有成功等待bashshell结束。它立刻就回来了。

我希望发生的是,当wrapper想要从终端读取时,子进程被挂起,然后当包装器再次将自身置于后台时,子进程将恢复。我愿意改变包装器的启动方式,或者让它启动另一个包装器脚本,然后启动bash,但我无法控制wrapper它自己。

答案1

如果我正确理解你的描述,我敢说该wrapper程序不是设计来生成交互式子级的,否则它会在访问 tty 之前停止(SIGSTOP)其子级,然后在访问 tty 之前立即恢复它们(SIGCONT)与 tty 一起完成。显然情况并非如此,如果它希望被允许任意访问 tty。

可以很容易地将一个辅助程序放在 SLVL=1 之间,wrapper从而充当两者之间的缓冲层,这样您的第一个 shell 就不会检测到wrapper被停止;然后这个帮助程序将检测何时wrapper停止,并且在这种情况下它将停止wrapper的子级,将 tty 返回wrapper并恢复它。但是wrapper 如果没有主动配合(即通知),那么检测 tty 何时完成就不太容易了wrapper。事实上,考虑到所描述的行为,我怀疑wrapper实际上并没有将自己置于后台,也没有做任何其他事情,只是在某些阻塞的系统调用上休眠。

但是,如果它确实将自己置于后台,那么您能做的最好的事情就是让帮助程序不断轮询有关当前前台进程的 tty,当它变回 beingwrapper的子进程时,帮助程序将恢复它(正如我怀疑的那样, ifwrapper 本身不这样做)。

也就是说,一般来说,为了恢复孩子,恐怕你需要一些特定的事件(或事件序列),可以从外部检测到wrapper,用它你可以正确地推断出wrapper 确实已经完成了tty,并且在这样的事件上(s) 简历wrapper的孩子。

对于手动恢复 的子级的解决方案是合理的情况wrapper,下面是一个应该处理特定情况的示例 Python 程序:

#!/usr/bin/python3

import os
import sys
import signal


def main():
    if len(sys.argv) < 2:
        sys.exit(0)

    def _noop_handler(sig, frame):
        """signal handler that does nothing"""
        pass

    termination_signals = {signal.SIGHUP, signal.SIGINT, signal.SIGTERM}
    management_signals = {signal.SIGCHLD, signal.SIGCONT, signal.SIGTTIN,
                          signal.SIGUSR1, signal.SIGUSR2}
    signal.pthread_sigmask(
            signal.SIG_BLOCK,
            management_signals | termination_signals
    )

    child = os.fork()

    if child == 0:  # child process after fork
        signal.sigwait({signal.SIGUSR1})  # wait go-ahead signal from parent
        signal.pthread_sigmask(
                signal.SIG_UNBLOCK,
                management_signals | termination_signals
        )
        os.execvp(sys.argv[1], sys.argv[1:])  # run command
    elif child > 0:  # parent process after fork
        # I want to manipulate tty ownership freely, so ignore SIGTTOU
        signal.signal(signal.SIGTTOU, signal.SIG_IGN)
        # A handler for SIGCHLD is required on some systems where semantics
        # for ignored signals is to never deliver them even to sigwait(2)
        signal.signal(signal.SIGCHLD, _noop_handler)

        in_fd = sys.stdin.fileno()
        my_pid = os.getpid()
        ppid = os.getppid()
        os.setpgid(child, child)  # put child in its own process group
        if os.tcgetpgrp(in_fd) == my_pid:
            # if I have been given the tty, hand it over to child
            # This is not the case when shell spawned me in "background" &
            os.tcsetpgrp(in_fd, child)
        os.kill(child, signal.SIGUSR1)  # all set for child, make it go ahead
        last_robbed_group = 0
        # signals to care for child
        io_wanted_signals = {signal.SIGTTIN, signal.SIGTTOU}

        def _send_sig(_pgid, _sig, accept_myself=False) -> bool:
            """
            send a signal to a process group if that is not my own or
            if accept_myself kwarg is True, and ignore OSError exceptions
            """
            if not accept_myself and _pgid == my_pid:
                return True
            try:
                os.killpg(_pgid, _sig)
            except OSError:
                return False
            return True

        def _resume_child_if_appropriate():
            """
            resume child unless that would steal tty from my own parent
            """
            nonlocal last_robbed_group
            fg_group = os.tcgetpgrp(in_fd)
            if fg_group == os.getpgid(ppid):
                # Minimal protection against stealing tty from parent shell.
                # If this would be the case, rather stop myself too
                _send_sig(my_pid, signal.SIGTTIN, accept_myself=True)
                return
            # Forcibly stop current tty owner
            _send_sig(fg_group, signal.SIGSTOP)
            if fg_group not in {os.getpgid(child), my_pid}:
                # remember who you stole tty from
                last_robbed_group = fg_group
            # Resume child
            os.tcsetpgrp(in_fd, os.getpgid(child))
            _send_sig(os.getpgid(child), signal.SIGCONT)

        waited_signals = termination_signals | management_signals
        while True:
            # Blocking loop over wait for signals
            sig = signal.sigwait(waited_signals)
            if sig in termination_signals:
                # Propagate termination signal and then exit
                _send_sig(os.getpgid(child), sig)
                os.wait()
                sys.exit(128 + sig)
            elif sig == signal.SIGCONT:
                # CONT received, presumably from parent shell, propagate it
                _resume_child_if_appropriate()
            elif sig == signal.SIGTTIN:
                # TTIN received, presumably from myself
                prev_fg = os.tcgetpgrp(in_fd)
                # Stop current tty owner if not my own parent
                if prev_fg != os.getpgid(ppid):
                    _send_sig(prev_fg, signal.SIGSTOP)
                try:
                    # Give tty back to my own parent and stop myself
                    os.tcsetpgrp(in_fd, os.getpgid(ppid))
                    _send_sig(my_pid, signal.SIGSTOP, accept_myself=True)
                except OSError:
                    try:
                        # ugh, parent unreachable, restore things
                        os.tcsetpgrp(in_fd, prev_fg)
                        _send_sig(prev_fg, signal.SIGCONT)
                    except OSError:
                        # Non-restorable situation ? let's idle then
                        os.tcsetpgrp(in_fd, my_pid)
            elif sig == signal.SIGCHLD:
                # Event related to child, let's investigate it
                pid, status = os.waitpid(child, os.WNOHANG | os.WUNTRACED)
                if pid > 0:
                    if os.WIFSIGNALED(status):
                        # Child terminated by signal, let's propagate this
                        sys.exit(128 + os.WTERMSIG(status))
                    elif os.WIFEXITED(status):
                        # Child exited normally, let's propagate this
                        sys.exit(os.WEXITSTATUS(status))
                    elif os.WIFSTOPPED(status) and \
                            os.WSTOPSIG(status) in io_wanted_signals:
                        # Child got stopped trying to access the tty, resume it
                        _resume_child_if_appropriate()
            elif sig in {signal.SIGUSR1, signal.SIGUSR2} \
                    and last_robbed_group:
                # Management signals to resume robbed process
                if sig == signal.SIGUSR2:
                    # Forcibly stop child, whatever it is doing or not doing
                    _send_sig(os.getpgid(child), signal.SIGSTOP)
                try:
                    # resume robbed process
                    os.tcsetpgrp(in_fd, last_robbed_group)
                    os.killpg(last_robbed_group, signal.SIGCONT)
                except OSError:
                    # Robbed process no longer exists ? oh well..
                    last_robbed_group = 0
                    try:
                        # resume child then
                        os.tcsetpgrp(in_fd, os.getpgid(child))
                        os.killpg(os.getpgid(child), signal.SIGCONT)
                    except OSError:
                        pass


if __name__ == '__main__':
    main()

它至少需要 Python v3.3。

它的设计并不好:它由一个主功能和几个子功能组成,但目的是尽可能更具可读性和可理解性,同时仍然提供基本数量的必要功能。

此外,它还可以扩展为例如与不是其直接父级的 shell 很好地配合,或者与同一程序的可能的递归调用,或者在查询当前前台进程并稍后更改它时可能的竞争条件,以及可能的其他角落案例。

上面的程序会自动停止当前的 tty 所有者,并wrapper 在由于不允许访问 tty 而停止时恢复。为了手动恢复以前的 tty 所有者,我提供了两种选择:

  1. 软简历:当您确定wrapper 已完成 tty 时,这是更好的方法;向帮助程序发送 SIGUSR1,它将恢复之前的 tty 所有者
  2. 硬简历:当你想停下来时使用的方法wrapper;发送 SIGUSR2 到帮助程序,它会wrapper 在恢复之前的 tty 所有者之前发出 SIGSTOP

您还可以向帮助程序发送 SIGCONT:它将强制停止当前 tty 所有者并继续运行wrapper

通过此设置,通常您应该最好避免直接向其wrapper子级或子级中的任何一个或任何子级发送 STOP/CONT 信号。

在所有情况下,请始终记住,您正在处理“外部”程序与其受控作业之间通常微妙的交互,尤其是当您在交互式 shell 中调用交互式 shell 时。这些通常不喜欢任意地发出 SIGSTOP 和 SIGCONT 信号。因此,您通常需要仔细应用正确的操作顺序,以免它们突然退出或弄乱终端窗口。

相关内容