親にtty入力が必要な場合はbashサブシェルを停止する

親にtty入力が必要な場合はbashサブシェルを停止する

問題があります。親プロセスにはTTYの入力が必要ですが、子プロセスはですbashSIGTTINを無視、走り続け、邪魔しなさい。bashよく遊ぶ方法はないでしょうか?

詳細

これはAlpine Linux 3.9および4.4.19にありますbash

単純化のためにwrapperいくつかの作業を実行し、サブコマンドに分岐するコマンドがあるとします。したがって、本質的にサブシェルwrapper bashとして実行されます。bash

私の場合は、wrapper次の関数でパッケージ化されています。

function wrap() {
    wrapper bash -l
}

したがって、1でbashを実行し$SHLVLwrap2に値を入力します$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の間にヘルパープログラムを置き、2つの間のバッファ層として機能し、最初のシェルが停止したことを検出できないようにするのは簡単です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が必要です。

うまく設計されていません。 1つの主要機能と複数のサブ機能で構成されていますが、基本的な必須機能を提供しながら、できるだけ読みやすく理解しやすくすることが目標です。

また、直接の親ではなくシェル、同じプログラムへの再帰呼び出し、現在のフォアグラウンドプロセスを照会して後で変更したときに発生する可能性がある競合状態、およびその他の特殊なケースとうまく機能するように拡張できます。 。

上記のプログラムは現在のtty所有者を自動的に停止し、wrapper ttyへのアクセスが許可されていないため、停止すると再起動します。古いtty所有者を手動で復元するには、次の2つのオプションがあります。

  1. ソフト再開:wrapper これは、tty操作が完了したと確信したときにSIGUSR1をヘルパーに送信し、古いtty所有者を復元するときのより良いアプローチです。
  2. ハード履歴:停止しようとしたときに使用される方法wrapperSIGUSR2をヘルパープログラムに送信し、wrapper 古いtty所有者を復元する前にSIGSTOPを実行します。

SIGCONT をヘルパープログラムに送信することもできます。これにより、現在のtty所有者を強制的に停止して実行を続けますwrapper

wrapperこの設定では、通常、STOP / CONT信号を子または子に直接送信することを避ける必要があります。

すべての場合において、特にインタラクティブシェル内でインタラクティブシェルを呼び出すときは、「外部」プログラムと制御された操作との間の微妙な相互作用を扱っていることに注意してください。これらは通常、SIGSTOPおよびSIGCONT信号をランダムに発行することを好まない。したがって、通常、タスクが突然終了したり端末ウィンドウを複雑にしたりしないように、正しいタスクシーケンスを慎重に適用する必要があります。

関連情報