3

@Bakuriu がコメントで指摘しているように、これは基本的にBASH と同じ問題です。入力中に Ctrl+C を押す と、現在の端末が中断されます。 、端末のクリーンアップをうまく処理しているようです。この点で bash が壊れているように見える理由についての回答に興味があります。

そのスクリプトによって開始されたサブプロセスの出力をログに記録するための Python スクリプトがあります。サブプロセスがたまたま bash スクリプトであり、ある時点でread -sビルトイン (入力された-s文字のエコーを防ぐ がキー) を呼び出してユーザー入力を読み取り、ユーザーがスクリプトを中断した場合 (つまり、Ctrl-C によって)、その後、bash は引き続き入力を受け付けますが、出力を tty に復元できません。

これを簡単な例に絞り込みました。

$ cat test.py
#!/usr/bin/python
import subprocess as sp
p = sp.Popen(['bash', '-c', 'read -s foo; echo $foo'])
p.wait()

実行./test.pyすると、何らかの入力を待ちます。なんらかの入力をして Enter キーを押すと、スクリプトは期待どおりに入力を返し、エコーします。問題はありません。ただし、すぐに「Ctrl-C」を押すと、Python は のトレースバックを表示してKeyboardInterruptから、bash プロンプトに戻ります。ただし、入力したものは端末に表示されません。ただし、入力reset<enter>すると端末が正常にリセットされます。

ここで何が起こっているのか正確にはわかりません。

更新: Python を使用せずにこれを再現することもできました。strace で bash を実行して、何が起こっているのかを収集できるかどうかを確認しようとしていました。次の bash スクリプトを使用します。

$ cat read.sh
#!/bin/bash
read -s foo
echo $foo

実行strace ./read.shしてすぐに Ctrl-C を押すと、次のようになります。

...
ioctl(0, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, {B38400 opost isig icanon -echo ...}) = 0
brk(0x1a93000)                          = 0x1a93000
read(0, Process 25487 detached
 <detached ...>

PID 25487 があった場所read.sh。これにより、端末は同じ壊れた状態のままになります。ただし、strace -I1 ./read.sh単に./read.shプロセスを中断し、通常の壊れていない端末に戻ります。

4

2 に答える 2

1

質問に対するコメントに書いたように、read -s実行時に bash は現在の tty 属性を保存し、スタック フレームの終了add_unwind_protect時に以前の tty 属性を復元するハンドラーをインストールします。read

通常、は起動時にbashのハンドラをインストールします。このハンドラは、 によって追加されたものなど、SIGINTすべてのハンドラの実行を含め、スタックの完全な巻き戻しを呼び出します。ただし、このハンドラーは通常、bash が対話モードで実行されている場合にのみインストールされます。ソース コードによると、インタラクティブ モードは次の条件でのみ有効になります。unwind_protectreadSIGINT

 /* First, let the outside world know about our interactive status.
     A shell is interactive if the `-i' flag was given, or if all of
     the following conditions are met:
    no -c command
    no arguments remaining or the -s flag given
    standard input is a terminal
    standard error is a terminal
     Refer to Posix.2, the description of the `sh' utility. */

これは、bash 内から bash を実行するだけでは問題を再現できなかった理由も説明できると思います。しかし、私がそれを実行したときstrace、またはPythonから開始されたサブプロセスは、私が使用してい-cたか、プログラムstderrが端末ではないなどのいずれかでした.

@Baikuriu が彼らの回答で見つけたように、私がこれを書いている最中に投稿されたのと同じように、「インタラクティブモード」の使用-iを強制し、それ自体が適切にクリーンアップされます。bash

私としては、これはバグだと思います。stdinTTY でない場合、-sオプション toreadは無視されることが man ページに記載されています。しかし、私の例stdin ではまだ TTY ですが、インタラクティブな動作を呼び出しているにもかかわらず、bash は技術的にはインタラクティブ モードではありません。この場合でも、 a から適切にクリーンアップする必要がありますSIGINT

価値のあるものとして、Python 固有の (ただし簡単に一般化できる) 回避策を次に示します。まず、SIGINT(SIGTERM適切な測定のために) がサブプロセスに渡されることを確認します。subprocess.Popen次に、端末設定用の小さなコンテキスト マネージャーで呼び出し全体をラップします。

import contextlib
import os
import signal
import subprocess as sp
import sys
import termios

@contextlib.contextmanager
def restore_tty(fd=sys.stdin.fileno()):
    if os.isatty(fd):
        save_tty_attr = termios.tcgetattr(fd)
        yield
        termios.tcsetattr(fd, termios.TCSAFLUSH, save_tty_attr)
    else:
        yield

@contextlib.contextmanager
def send_signals(proc, *sigs):
    def handle_signal(signum, frame):
        try:
            proc.send_signal(signum)
        except OSError:
            # process has already exited, most likely
            pass

    prev_handlers = []

    for sig in sigs:
        prev_handlers.append(signal.signal(sig, handle_signal))

    yield

    for sig, handler in zip(sigs, prev_handlers):
        signal.signal(sig, handler)


with restore_tty():
    p = sp.Popen(['bash', '-c', 'read -s test; echo $test'])
    with send_signals(p, signal.SIGINT, signal.SIGTERM):
        p.wait()

なぜこれが必要なのかを説明する答えにまだ興味があります-なぜbash自体をより良くクリーンアップできないのですか?

于 2016-07-07T14:03:16.300 に答える