상위에 tty 입력이 필요한 경우 bash 하위 쉘 중지

상위에 tty 입력이 필요한 경우 bash 하위 쉘 중지

문제가 있습니다. 상위 프로세스에는 TTY의 입력이 필요하지만 하위 프로세스는 입니다 bash.SIGTTIN 무시, 계속 달리고 방해하십시오. bash잘 놀 수 있는 방법 없을까요 ?

세부 사항

이는 Alpine Linux 3.9 및 4.4.19에 있습니다 bash.

단순화를 위해 wrapper몇 가지 작업을 수행한 다음 하위 명령으로 분기하는 command 가 있다고 가정해 보겠습니다. 따라서 본질적으로 wrapper bash서브쉘로 실행됩니다.bash

내 경우에는 wrapper다음과 같은 함수로 포장되어 있습니다.

function wrap() {
    wrapper bash -l
}

그래서 1에서 bash를 실행 $SHLVL하고 wrap2에 값을 입력합니다 $SHLVL. 래퍼에서 제공하는 향상된 기능을 사용하여 하위 셸에서 작업하고 있습니다. 저는 bash서브셸을 일반 대화형 셸로 사용하고 있으므로 여전히 작업 제어가 필요합니다. set +m작업 제어를 사용 하거나 비활성화하는 set +o monitor것은 허용되지 않습니다.

wrapperTTY에서 데이터를 읽으려고 할 때 문제가 발생합니다. SIGTTIN을 읽고 수신하려고 시도합니다. 이 시점에서 나는 $SHLVL백그라운드에서 1 로 되돌아갔습니다 . wrapper불행하게도 $SHLVL2는 bash신호를 받지 못하고 여전히 쉘 프롬프트를 출력하고 있으며 TTY에서 읽으려고 시도하고 있지만 지금은 EOF를 받고 있습니다. 조심하지 않으면(운이 좋을 수도 있습니다) 종료되므로 첫 번째 문자인 Yes를 가져 fg옵니다. wrapper전면에 배치하고 TTY에서 읽습니다.

이것은 유동적인 상황이었고 저는 강력한 것을 원했습니다. 나는 무엇을 해야할지 모르겠습니다. 그래픽이 아닌 터미널을 사용하고 있어서 다른 창을 열 수 없습니다. openvt그래픽 환경에서 실행해야 하기 때문에 제대로 작동하지 못하는 것 같아요 . bash이런 대본을 쓰려고 노력 중이야

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

bash그러나 쉘이 종료될 때 까지 기다리는 데는 성공하지 못했습니다 . 그것은 바로 돌아왔다.

내가 원하는 것은 wrapper터미널에서 읽으려고 할 때 하위 프로세스가 일시 중지된 다음 래퍼가 다시 백그라운드에 들어갈 때 다시 시작되는 것입니다. 래퍼가 시작되는 방법을 변경하거나 시작하기 전에 다른 래퍼 스크립트를 시작하도록 하고 싶지만 직접 bash제어할 수는 없습니다 wrapper.

답변1

귀하의 설명을 올바르게 이해했다면 이 wrapper프로그램은 대화형 자식을 생성하도록 설계되지 않았다고 감히 말할 수 있습니다. 그렇지 않으면 tty에 액세스하기 전에 자식을 중지(SIGSTOP)한 다음 tty를 사용하여 tty에 액세스하기 전에 즉시 자식을 다시 시작(SIGCONT)합니다. 함께 완성했습니다. 분명히 tty에 대한 임의의 액세스를 허용하려는 경우에는 그렇지 않습니다.

wrapperSLVL=1 사이에 도우미 프로그램을 두어 둘 사이의 버퍼 계층 역할을 하여 첫 번째 쉘이 중지된 것을 감지하지 못하게 하는 것은 쉽습니다 wrapper. 그러면 이 도우미 프로그램은 wrapper중지된 시점을 감지하고 이 경우 중지됩니다. wrapper자식은 tty를 반환 wrapper하고 다시 시작합니다. 그러나 wrapper 적극적인 협력(예: 알림)이 없으면 tty가 언제 완료되는지 감지하기가 쉽지 않습니다 wrapper. 사실, 설명된 동작을 보면 wrapper실제로는 백그라운드에 있거나 다른 작업을 수행하는 것이 아니라 일부 차단 시스템 호출에서 잠자기 상태에 있는 것 같습니다.

그러나 그것이 백그라운드에 배치된다면, 당신이 할 수 있는 최선은 도우미가 현재 포그라운드 프로세스에 대해 tty를 지속적으로 폴링하도록 하는 것입니다. 그리고 그것이 wrapper하위 프로세스로 다시 변경되면 도우미는 이를 재개할 것입니다( 제가 말했듯이 자체적으로 이 작업을 수행하지 않는 경우 의심됩니다 wrapper .)

즉, 일반적으로 하위 항목을 복원하려면 외부에서 감지할 수 있는 특정 이벤트(또는 이벤트 시퀀스)가 필요합니다 wrapper. 이를 통해 wrapper tty가 실제로 완료되었음을 올바르게 추론할 수 있습니다. 그러한 경우에는 Kids on(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이 필요합니다.

잘 설계되지 않았습니다. 하나의 주요 기능과 여러 하위 기능으로 구성되어 있지만, 기본적인 필수 기능을 제공하면서 최대한 읽기 쉽고 이해하기 쉽도록 하는 것이 목표입니다.

또한 직접적인 부모가 아닌 쉘, 동일한 프로그램에 대한 재귀 호출, 현재 포그라운드 프로세스를 쿼리하고 나중에 변경할 때 발생할 수 있는 경쟁 조건 및 기타 특수한 경우와 잘 작동하도록 확장될 수 있습니다. .

위 프로그램은 현재 tty 소유자를 자동으로 중지하고 wrapper tty에 대한 액세스가 허용되지 않기 때문에 중지되면 다시 시작합니다. 이전 tty 소유자를 수동으로 복원하기 위해 다음 두 가지 옵션을 제공합니다.

  1. 소프트 재개: wrapper 이는 tty 작업이 완료되었다고 확신할 때 SIGUSR1을 도우미에게 보내고 이전 tty 소유자를 복원할 때 더 나은 접근 방식입니다.
  2. 하드 이력서: 중지하려고 할 때 사용되는 방법 wrapperSIGUSR2를 도우미 프로그램에 보내고 wrapper 이전 tty 소유자를 복원하기 전에 SIGSTOP을 실행 합니다.

SIGCONT를 도우미 프로그램에 보낼 수도 있습니다. 그러면 현재 tty 소유자를 강제로 중지하고 계속 실행됩니다 wrapper.

wrapper이 설정을 사용하면 일반적으로 STOP/CONT 신호를 하위 항목이나 하위 항목에 직접 보내는 것을 피해야 합니다 .

모든 경우에, 특히 대화형 쉘 내에서 대화형 쉘을 호출할 때 "외부" 프로그램과 제어된 작업 사이의 미묘한 상호작용을 다루고 있다는 점을 항상 기억하십시오. 이들은 일반적으로 SIGSTOP 및 SIGCONT 신호를 임의로 발행하는 것을 좋아하지 않습니다. 따라서 일반적으로 작업이 갑자기 종료되거나 터미널 창을 복잡하게 만들지 않도록 올바른 작업 순서를 주의 깊게 적용해야 합니다.

관련 정보