その時、状況が変になった…

その時、状況が変になった…

IRC-Bouncer/notification スクリプトが正しく動作するようにするために苦労しています。

以下は、リモートシステムに自動的にログインし、weechatを実行しているスクリーンセッションに接続し(現在存在しない場合は起動)、netcatを使用してソケットファイルから通知を読み取る別のSSH接続を開くスクリプトです。それだけwechat追加スクリプト私の通知メッセージをエクスポートします。その後、これらの通知はlib-notify(notification-sendを介して)に供給されるため、weechatからアクティビティ通知を受け取ることができます。

スクリプトは次のとおりです。

#!/bin/bash

BOUNCER="[email protected]"

function irc_notify() {
  ssh $BOUNCER "nc -k -l -U /tmp/weechat.notify.sock" | \
    while read type message; do
     notify-send -i weechat -u critical "$(echo -n $type | base64 -di -)" "$(echo -n $message | base64 -di -)"
    done
}

# Start listening for notifications
irc_notify &

# Attach to remote IRC Bouncer Screen
ssh $BOUNCER -t 'screen -d -R -S irc weechat'

# Cleanup Socket Listener
echo "cleaning up notification socket listener…"
ssh $BOUNCER 'pkill -f -x "nc -k -l -U /tmp/weechat.notify.sock"'

1つの主な欠陥を除いて、設定は実際にはかなりうまく機能します。 スクリプトが呼び出されるたびに、私の通知マネージャには2つの通知のみが送信されます。以来:何もありません。

そのため、weechatの通知スクリプトの問題を排除するために、2番目のssh呼び出し(screenセッションに接続してweechatを開始する呼び出し)を削除し、テスト中にread実行をブロックするコマンドに置き換えました。次に、irbリモートコンピュータでRubyを使用してソケットにメッセージを送信します。
しかし、手動でメッセージを送信しても、動作が停止する前に表示されるメッセージはまだ2つだけです。

strace私はいくつかの興味深い動作を示しました(フォークされたプロセスに接続したとき)、最初または2番目のメッセージの後にメッセージが改行文字で終了しないようです。しかし、何度も経過すると、彼らはもはやstrace一緒に現れませんでした。

この時点で、私はスクリプトに奇妙な動作を引き起こすものがあるかどうかを確認することにしました。 したがって、コマンドラインはssh $BOUNCER "nc -k -l -U /tmp/weechat.notify.sock"ssh connect()を直接呼び出します。ところで、私が手動で送信したすべてのメッセージが表示されました(もちろんBase64でエンコードされています)。

そのため、スクリプトで行ったように、各メッセージをデコードするロジックを追加しましたが、これは各メッセージにも機能しました。通知の送信にこれらのメッセージを入力する場合も含まれます。

だから今回関数をフォークしたときに何か奇妙なことが起こったのは間違いないと思いました。。ただし、端末のバックグラウンドでコマンドを実行すると、効果に違いはありません。それで、スクリプト内で実行されているので、何か奇妙なことが起こっているのだろうか?

その時、状況が変になった…

まず、関数からロジックを分離してから直接呼び出し、パイプコマンドの最後に「&」記号を追加しました。このように:

ssh $BOUNCER "nc -k -l -U /tmp/weechat.notify.sock" | \
  while read type message; do
    notify-send -i weechat -u critical "$(echo -n $type | base64 -di -)" "$(echo -n $message | base64 -di -)"
  done &

この操作を実行すると、突然メッセージが機能し始めました。変更を元に戻した後、同じ奇妙な2つのメッセージアクションに戻りました。

しかし、この修正によって別の奇妙な動作が発生しました。 スクリーンセッションでプログラムに登録する前に、各キーを数回押す必要がありました。 STDINに競争条件があるのと同じです。

おそらく、2つのSSHセッションがこのために戦っていると考えている(理由はわかりませんが)、最初のsshコマンドでSTDINを閉じるかホギングするさまざまな方法を試しました。たとえば、パイプのSSH部分の: |前後に<&-接続をパイプします。</dev/nullこれにより、競合状態が解決されるように見えますが、2つのメッセージのみが機能します。

考えるこれは多層サブプロセスに関連している可能性があります。その後、SSH呼び出しを次のようにラップしてそれを再現しようとしましたbash -cbash -c 'ssh $BOUNCER "nc -k -l -U /tmp/weechat.notify.sock" &'これは、2つのメッセージだけで動作を示します。

また、リモートコンピュータ(SSHを介してlocalhostに接続し、2回のbash -c呼び出しでラップ)で直接テストしましたが、同じ破損動作を目撃しました。 また、孤児プロセスにつながる二重フォークとは関係がないようです。 プロセスが最終的に孤児になるかどうかは重要ではないようです。

私もこれも起こっていることを確認しました。zsh

これは、プロセスが下位処理層で実行されるときにSTDINとSTDOUTがどのように処理されるかに関連しているようです。

再び表示されます。指示とstrace出力:

デバッグを簡素化するために、ダイアグラムからSSHを削除し、動作をローカルで正確に再現する2つの単純化されたテストスクリプトを作成しました。

Juergen Nickelsenのsocketコマンドを使用してローカルUNIXドメインソケット()を作成し、次のRubyコードブロックを使用してsocket -l -s ./test.sock再テストメッセージを送信できました。irb

require 'socket'
require 'base64'

SOCKET = './test.sock'

def send(subtitle, message)
  UNIXSocket.open(SOCKET) do |socket|
    socket.puts "#{Base64.strict_encode64(subtitle)} #{Base64.strict_encode64(message)}"
  end
end

send('test', 'hi')
send('test', 'hi')
send('test', 'hi')
send('test', 'hi')
send('test', 'hi')
send('test', 'hi')

最初のスクリプトパイプライン式に対してのみバックグラウンド処理(前述のように無制限のメッセージを処理します):

#!/bin/bash
 
# to aid in cleanup when using Ctrl-C to exit strace
trap "pkill -f -x 'nc -k -l -U $HOME/test.sock'; exit" SIGINT
 
# Start listening for notifications
nc -k -l -U $HOME/test.sock | \
  while read type message; do
    # write messages to a local file instead of sending to notification daemon for simplicity.
    echo "$(echo -n $type | base64 -di -)" "$(echo -n $message | base64 -di -)" >> /tmp/msg
  done &
 
read

実行時に次の出力を生成しますstrace -fhttp://pastebin.com/SMjti3qW

2番目のスクリプトラッパー関数をバックグラウンドに配置します(2回の完了アクショントリガー)。

#!/bin/bash

# to aid in cleanup when using Ctrl-C to exit strace
trap "pkill -f -x 'nc -k -l -U $HOME/test.sock'; exit" SIGINT

# Start listening for notifications
function irc_notify() {
  nc -k -l -U $HOME/test.sock | \
    while read type message; do
      # write messages to a local file instead of sending to notification daemon for simplicity.
      echo "$(echo -n $type | base64 -di -)" "$(echo -n $message | base64 -di -)" >> /tmp/msg
    done
}

irc_notify &

read

次のコマンドを実行すると、次の出力が生成されますstrace -fhttp://pastebin.com/WsrXX0EJ

上記のスクリプトの出力を見るときに目立つのは、コマンド固有のstrace出力です。nc。これは、2つのスクリプト実行の主な違いの1つを示しているようです。

最初のスクリプトの「作業中」nc strace出力:

accept(3, {sa_family=AF_FILE, NULL}, [2]) = 4
poll([{fd=4, events=POLLIN}, {fd=0, events=POLLIN}], 2, -1) = 1 ([{fd=4, revents=POLLIN|POLLHUP}])
read(4, "dGVzdA== aGk=\n", 2048)        = 14
write(1, "dGVzdA== aGk=\n", 14)         = 14
poll([{fd=4, events=POLLIN}, {fd=0, events=POLLIN}], 2, -1) = 1 ([{fd=4, revents=POLLIN|POLLHUP}])
read(4, "", 2048)                       = 0
shutdown(4, 0 /* receive */)            = 0
close(4)                                = 0
accept(3, {sa_family=AF_FILE, NULL}, [2]) = 4
poll([{fd=4, events=POLLIN}, {fd=0, events=POLLIN}], 2, -1) = 1 ([{fd=4, revents=POLLIN|POLLHUP}])
read(4, "dGVzdA== aGk=\n", 2048)        = 14
write(1, "dGVzdA== aGk=\n", 14)         = 14
poll([{fd=4, events=POLLIN}, {fd=0, events=POLLIN}], 2, -1) = 1 ([{fd=4, revents=POLLIN|POLLHUP}])
read(4, "", 2048)                       = 0
shutdown(4, 0 /* receive */)            = 0
close(4)                                = 0
accept(3, {sa_family=AF_FILE, NULL}, [2]) = 4
poll([{fd=4, events=POLLIN}, {fd=0, events=POLLIN}], 2, -1) = 1 ([{fd=4, revents=POLLIN|POLLHUP}])
read(4, "dGVzdA== aGk=\n", 2048)        = 14
write(1, "dGVzdA== aGk=\n", 14)         = 14
poll([{fd=4, events=POLLIN}, {fd=0, events=POLLIN}], 2, -1) = 1 ([{fd=4, revents=POLLIN|POLLHUP}])
read(4, "", 2048)                       = 0
shutdown(4, 0 /* receive */)            = 0
close(4)                                = 0
accept(3, {sa_family=AF_FILE, NULL}, [2]) = 4
poll([{fd=4, events=POLLIN}, {fd=0, events=POLLIN}], 2, -1) = 1 ([{fd=4, revents=POLLIN|POLLHUP}])
read(4, "dGVzdA== aGk=\n", 2048)        = 14
write(1, "dGVzdA== aGk=\n", 14)         = 14
poll([{fd=4, events=POLLIN}, {fd=0, events=POLLIN}], 2, -1) = 1 ([{fd=4, revents=POLLIN|POLLHUP}])
read(4, "", 2048)                       = 0
shutdown(4, 0 /* receive */)            = 0
close(4)                                = 0
accept(3, {sa_family=AF_FILE, NULL}, [2]) = 4
poll([{fd=4, events=POLLIN}, {fd=0, events=POLLIN}], 2, -1) = 1 ([{fd=4, revents=POLLIN|POLLHUP}])
read(4, "dGVzdA== aGk=\n", 2048)        = 14
write(1, "dGVzdA== aGk=\n", 14)         = 14
poll([{fd=4, events=POLLIN}, {fd=0, events=POLLIN}], 2, -1) = 1 ([{fd=4, revents=POLLIN|POLLHUP}])
read(4, "", 2048)                       = 0
shutdown(4, 0 /* receive */)            = 0
close(4)                                = 0
accept(3, {sa_family=AF_FILE, NULL}, [2]) = 4
poll([{fd=4, events=POLLIN}, {fd=0, events=POLLIN}], 2, -1) = 1 ([{fd=4, revents=POLLIN|POLLHUP}])
read(4, "dGVzdA== aGk=\n", 2048)        = 14
write(1, "dGVzdA== aGk=\n", 14)         = 14
poll([{fd=4, events=POLLIN}, {fd=0, events=POLLIN}], 2, -1) = 1 ([{fd=4, revents=POLLIN|POLLHUP}])
read(4, "", 2048)                       = 0
shutdown(4, 0 /* receive */)            = 0
close(4)                                = 0
accept(3,

2番目のスクリプトnc strace出力に表示される「2回完了」動作は次のとおりです。

accept(3, {sa_family=AF_FILE, NULL}, [2]) = 4
poll([{fd=4, events=POLLIN}, {fd=0, events=POLLIN}], 2, -1) = 2 ([{fd=4, revents=POLLIN|POLLHUP}, {fd=0, revents=POLLHUP}])
read(4, "dGVzdA== aGk=\n", 2048)        = 14
write(1, "dGVzdA== aGk=\n", 14)         = 14
shutdown(4, 1 /* send */)               = 0
close(0)                                = 0
poll([{fd=4, events=POLLIN}, {fd=-1}], 2, -1) = 1 ([{fd=4, revents=POLLIN|POLLHUP}])
read(4, "", 2048)                       = 0
shutdown(4, 0 /* receive */)            = 0
close(4)                                = 0
accept(3, {sa_family=AF_FILE, NULL}, [2]) = 0
poll([{fd=0, events=POLLIN}, {fd=0, events=POLLIN}], 2, -1) = 2 ([{fd=0, revents=POLLIN|POLLHUP}, {fd=0, revents=POLLIN|POLLHUP}])
read(0, "dGVzdA== aGk=\n", 2048)        = 14
write(1, "dGVzdA== aGk=\n", 14)         = 14
read(0, "", 2048)                       = 0
shutdown(0, 1 /* send */)               = 0
close(0)                                = 0
poll([{fd=0, events=POLLIN}, {fd=-1}], 2, -1) = 1 ([{fd=0, revents=POLLNVAL}])
poll([{fd=0, events=POLLIN}, {fd=-1}], 2, -1) = 1 ([{fd=0, revents=POLLNVAL}])
poll([{fd=0, events=POLLIN}, {fd=-1}], 2, -1) = 1 ([{fd=0, revents=POLLNVAL}])
poll([{fd=0, events=POLLIN}, {fd=-1}], 2, -1) = 1 ([{fd=0, revents=POLLNVAL}])
poll([{fd=0, events=POLLIN}, {fd=-1}], 2, -1) = 1 ([{fd=0, revents=POLLNVAL}])
poll([{fd=0, events=POLLIN}, {fd=-1}], 2, -1) = 1 ([{fd=0, revents=POLLNVAL}])
poll([{fd=0, events=POLLIN}, {fd=-1}], 2, -1) = 1 ([{fd=0, revents=POLLNVAL}])
poll([{fd=0, events=POLLIN}, {fd=-1}], 2, -1) = 1 ([{fd=0, revents=POLLNVAL}])
poll([{fd=0, events=POLLIN}, {fd=-1}], 2, -1) = 1 ([{fd=0, revents=POLLNVAL}])
poll([{fd=0, events=POLLIN}, {fd=-1}], 2, -1) = 1 ([{fd=0, revents=POLLNVAL}])
.......[truncated].......

私は出力の読みやすさの観点から私が望む場所にいないstraceので、これらの異なる出力が何を意味するのかわかりません。 1つは明らかに機能し、もう1つは機能しないという事実を除いて言葉です。

大きな出力を掘り下げてみると、strace最初の2つ以降のメッセージは改行文字で終了しなくなります。しかし、もう一度申し上げますが、それが何を意味するのか、それとも私が正しく読んでいるのかわかりません。

さまざまなサブ処理技術やSTDINをオフにすると、この動作にどのような影響を与える可能性があるのか​​はまったくわかりません。

私がここで何をしているのか知っていますか?

-

長すぎます。

2つ以上のサブ処理レベルで通知リスナーを実行すると、2つのメッセージのみが処理される理由を特定しようとします。そうしないと、STDINで競合状態が発生します。

答え1

OpenBSDの最新の派生物netcat(FreeBSD [1]とDebian [2]を含む)は、-dstdinが読み取るのを防ぐフラグをサポートして説明する問題を修正します。

問題は、netcatがstdinとその「ネットワーク」fdをポーリングし、stdinが/dev/nullパイプを作成する前にバックグラウンドでシェル関数が実行される上記の2番目のケースで再び開くことです。これはstdin(fd 0)から最初に読み込まれるとすぐにEOFが発生しますが、netcatはpoll(2)閉じたstdinから読み込みを続けて無限ループを生成することを意味します。

パイプが作成される前のstdinリダイレクトは次のとおりです。

249 [pid 23186] open("/dev/null", O_RDONLY <unfinished ...>
251 [pid 23186] <... open resumed> )        = 3
253 [pid 23186] dup2(3, 0)                  = 0
254 [pid 23186] close(3)                    = 0

netcat(pid 23187)が最初のものを呼び出すと、poll(2)stdinからEOFを読み取り、fd 0を閉じます。

444 [pid 23187] poll([{fd=4, events=POLLIN}, {fd=0, events=POLLIN}], 2, 4294967295) = 2 ([{fd=4, revents=POLLIN|POLLHUP}, {fd=0, revents=POLLIN}])
448 [pid 23187] read(0,  <unfinished ...>
450 [pid 23187] <... read resumed> "", 2048) = 0
456 [pid 23187] close(0 <unfinished ...>
458 [pid 23187] <... close resumed> )       = 0

次の呼び出しaccept(2)では、fd 0にクライアントが作成されます。これは最も低い番号の利用可能なfdです。

476 [pid 23187] accept(3,  <unfinished ...>
929 [pid 23187] <... accept resumed> {sa_family=AF_LOCAL, NULL}, [2]) = 0

netcatには引数にfd 0が2回含まれますpoll(2)。一度はコマンドライン引数なしで常に含まれSTDIN_FILENO、一度は新しく接続されたクライアントに対して含まれます。-d

930 [pid 23187] poll([{fd=0, events=POLLIN}, {fd=0, events=POLLIN}], 2, 4294967295) = 2 ([{fd=0, revents=POLLIN|POLLHUP}, {fd=0, revents=POLLIN|POLLHUP}])

クライアントはEOFを送信し、netcat接続を切断します。

936 [pid 23187] read(0,  <unfinished ...>
938 [pid 23187] <... read resumed> "", 2048) = 0
940 [pid 23187] shutdown(0, SHUT_WR <unfinished ...>
942 [pid 23187] <... shutdown resumed> )    = 0
944 [pid 23187] close(0 <unfinished ...>
947 [pid 23187] <... close resumed> )       = 0

しかし、今は閉じたfd 0をポーリングし続けるので、問題が発生します。 netcatコードはメンバーPOLLNVALの設定を処理しないため、無限ループに陥り、再度呼び出されません。.reventsstruct pollfdaccept(2)

949 [pid 23187] poll([{fd=0, events=POLLIN}, {fd=-1}], 2, 4294967295 <unfinished ...>
951 [pid 23187] <... poll resumed> )        = 1 ([{fd=0, revents=POLLNVAL}])
953 [pid 23187] poll([{fd=0, events=POLLIN}, {fd=-1}], 2, 4294967295 <unfinished ...>
955 [pid 23187] <... poll resumed> )        = 1 ([{fd=0, revents=POLLNVAL}])
...

最初のコマンドでは、パイプはバックグラウンドにありますが、シェル関数で実行されず、stdinが開いているため、これは発生しません。

コード参照(readwrite関数参照):

  1. http://svnweb.freebsd.org/base/head/contrib/netcat/
  2. https://sources.debian.net/src/netcat-openbsd/1.105-7/

答え2

このように関数を実行すると問題は解決しますか?

irc_notify </dev/null &

その場合は、標準入力から同時に読み取ろうとする2つのプロセスが問題かもしれません。また、zackseが提案したように、-nを使用してすべてのsshコマンドを実行すると便利です。少なくともどのプロセスがstdinのために戦っているかをデバッグすることです。

関連情報