web-dev-qa-db-ja.com

キーストロークでBashスクリプトから呼び出されたタイムアウトを強制終了できないのはなぜですか?

[編集:これは、生成されたすべてのプロセスを強制終了する方法を尋ねる他のいくつかの質問に似ています–答えはすべてpkillを使用することです。したがって、私の質問の核心は次のようになるかもしれません:スクリプトによって生成されたすべてのプロセスにCtrl-C/Zを伝播する方法はありますか?]

Coreutilsからrecコマンドを使用してSoX timeoutを呼び出す場合(議論 here )、キーストロークでそれを強制終了する方法はないようです。 Bashスクリプト内から呼び出されました。

例:

timeout 10 rec test.wav

...で殺すことができます Ctrl+C または Ctrl+Z bashからですが、スクリプト内から呼び出されたときはそうではありません。

timeout 10 ping nowhere

...で殺すことができます Ctrl+C または Ctrl+Z bashから、そして Ctrl+Z スクリプト内から実行した場合。

プロセスIDを見つけてそのように終了することはできますが、標準のブレークキーストロークを使用できないのはなぜですか。そして、私ができるようにスクリプトを構成する方法はありますか?

11
meetar

などの信号キー Ctrl+C フォアグラウンドのすべてのプロセスにシグナルを送信します プロセスグループ

典型的なケースでは、プロセスグループはパイプラインです。たとえば、head <somefile | sortheadを実行しているプロセスとsortを実行しているプロセスは、シェルと同じプロセスグループにあるため、すべてがシグナルを受信します。バックグラウンドでジョブを実行すると(somecommand &)、そのジョブは独自のプロセスグループにあるため、 Ctrl+C 影響はありません。

timeoutプログラムは、自身を独自のプロセスグループに配置します。ソースコードから:

/* Ensure we're in our own group so all subprocesses can be killed.
   Note we don't just put the child in a separate group as
   then we would need to worry about foreground and background groups
   and propagating signals between them.  */
setpgid (0, 0);

タイムアウトが発生すると、timeoutは、それがメンバーであるプロセスグループを強制終了するという簡単な方法を実行します。自身を別のプロセスグループに配置しているため、その親プロセスはグループに含まれません。ここでプロセスグループを使用すると、子アプリケーションが複数のプロセスに分岐した場合、そのすべてのプロセスがシグナルを受信するようになります。

コマンドラインでtimeoutを直接実行して、 Ctrl+C、結果のSIGINTはtimeoutと子プロセスの両方で受信されますが、timeoutの親プロセスであるインタラクティブシェルでは受信されません。 timeoutがスクリプトから呼び出されると、スクリプトを実行しているシェルのみがシグナルを受信します。timeoutは別のプロセスグループにあるため、シグナルを取得しません。

trapビルトインを使用して、シェルスクリプトでシグナルハンドラーを設定できます。残念ながら、それはそれほど簡単ではありません。このことを考慮:

#!/bin/sh
trap 'echo Interrupted at $(date)' INT
date
timeout 5 sleep 10
date

押すと Ctrl+C 2秒後、これはまだ5秒間完全に待機してから、「中断」メッセージを出力します。これは、フォアグラウンドジョブがアクティブな間、シェルがトラップコードの実行を控えるためです。

これを修正するには、ジョブをバックグラウンドで実行します。シグナルハンドラーでkillを呼び出して、シグナルをtimeoutプロセスグループにリレーします。

#!/bin/sh
trap 'kill -INT -$pid' INT
timeout 5 sleep 10 &
pid=$!
wait $pid

Gillesによって提供された優れた答えに基づいて構築します。タイムアウトコマンドにはフォアグラウンドオプションがあり、使用すると、CTRL + Cでタイムアウトコマンドを終了します。

#!/bin/sh
trap 'echo caught interrupt and exiting;exit' INT
date
timeout --foreground 5 sleep 10
date
13

基本的に Ctrl+CSIGINTシグナルを送信しますが、 Ctrl+ ZはSIGTSTPシグナルを送信します。

SIGTSTPはプロセスを停止するだけで、SIGCONTはプロセスを続行します。

これは、コマンドラインでフォークされたフォアグラウンドプロセスで機能します。

プロセスがバックグラウンドプロセスの場合は、別の方法でそのシグナルをプロセスに送信する必要があります。 killがそれを行います。理論的には、そのkillの「-」演算子も子プロセスにシグナルを送る必要がありますが、これが期待どおりに機能することはめったにありません。

さらに読むために Unix-Signals

1
Nils

既存のスクリプトに「検索と置換」のアプローチを提供することで @ Gilles 'answer を改善します。

  1. コードの先頭に次のスニペットを追加します。
declare -a timeout_pids
exec 21>&1; exec 22>&2 # backup file descriptors, see https://superuser.com/a/1446738/187576
my_timeout(){
    local args tp ret
    args="$@"
    timeout $args &
    tp=$!
    echo "pid of timeout: $tp"
    timeout_pids+=($tp)
    wait $tp
    ret=$?
    count=${#timeout_pids[@]}
    for ((i = 0; i < count; i++)); do
        if [ "${timeout_pids[i]}" = "$tp" ] ; then
            unset 'timeout_pids[i]'
        fi
    done
    return $ret
}
  1. 次のコードをINTハンドラーに追加(またはマージ)します。
pre_cleanup(){
    exec 1>&21; exec 2>&22 # restore file descriptors, see https://superuser.com/a/1446738/187576
    echo "Executing pre-cleanup..."
    for i in "${timeout_pids[*]}"; do
        if [[ ! -z $i ]]; then
            #echo "Killing PID: $i"
            kill -INT -$i 2> /dev/null
        fi
    done
    exit
}

trap pre_cleanup INT
  1. スクリプトのtimeoutコマンドをmy_timeout関数に置き換えます。

次にスクリプトの例を示します。

#!/bin/bash

# see "killing timeout": https://unix.stackexchange.com/a/57692/65781
declare -a timeout_pids
exec 21>&1; exec 22>&2 # backup file descriptors, see https://superuser.com/a/1446738/187576
my_timeout(){
    local args tp ret
    args="$@"
    timeout $args &
    tp=$!
    echo "pid of timeout: $tp"
    timeout_pids+=($tp)
    wait $tp
    ret=$?
    count=${#timeout_pids[@]}
    for ((i = 0; i < count; i++)); do
        if [ "${timeout_pids[i]}" = "$tp" ] ; then
            unset 'timeout_pids[i]'
        fi
    done
    return $ret
}

cleanup(){
    echo "-----------------------------------------"
    echo "Restoring previous routing table settings"
}

pre_cleanup(){
    exec 1>&21; exec 2>&22 # restore file descriptors, see https://superuser.com/a/1446738/187576
    echo "Executing pre-cleanup..."
    for i in "${timeout_pids[*]}"; do
        if [[ ! -z $i ]]; then
            echo "Killing PID: $i"
            kill -INT -$i 2> /dev/null
        fi
    done
    exit
}

trap pre_cleanup INT
trap cleanup EXIT

echo "Executing 5 temporary timeouts..."
unreachable_ip="192.168.44.5"
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
echo "ctrl+c now to execute cleanup"
my_timeout 9s ping -c 1 "$unreachable_ip" &> /dev/null 
0
ceremcem