我一直在努力让 IRC-Bouncer/通知脚本正常工作。
这是一个脚本,它将自动登录到远程计算机,附加到运行 weechat 的屏幕会话(如果当前不存在则启动一个),同时打开另一个 ssh 连接,该连接使用 netcat 从套接字文件读取通知到其中一个weechat 附加脚本导出我的通知消息。然后这些通知会被输入 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"'
除了一个重大故障之外,该设置实际上运行得非常好。 每次调用脚本时,只有两个通知发送到我的通知管理器。之后:什么也没有。
因此,为了消除 weechat 中通知脚本的问题,我删除了第二个 ssh 调用(附加到屏幕会话并启动 weechat 的调用),并将其替换为read
在测试时阻止执行的命令。然后,irb
在远程计算机上使用,我使用 ruby 向套接字发送消息。
然而,即使我手动发送消息,在它停止工作之前仍然只会出现两条消息。
strace
向我展示了一些有趣的行为(当我附加到分叉进程时),在第一条或第二条消息之后,消息似乎不再被换行符终止。但又过了几次之后,他们就不再strace
一起出现了。
此时,我决定看看我的脚本中是否有某些内容导致了奇怪的行为。 所以在命令行上我只是ssh $BOUNCER "nc -k -l -U /tmp/weechat.notify.sock"
直接调用 ssh 连接()。你瞧,我手动发送的所有消息都出现了(当然,仍然是 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 &
当我这样做时,消息突然开始起作用。一旦我恢复了更改,我就回到了同样奇怪的仅两条消息行为的情况。
但这个修复引入了一些其他奇怪的行为。 一旦进入屏幕会话,我就必须多次敲击每个键才能被程序注册。就好像 STDIN 上存在竞争条件。
考虑到也许两个 SSH 会话正在争夺它(尽管我不确定为什么),我尝试通过各种方式关闭和/或占用第一个 ssh 命令上的 STDIN。例如在管道的 SSH 部分: |
之前<&-
或之后通过管道连接。</dev/null
虽然这似乎确实解决了竞争条件,但这重新引入了仅两条消息的行为。
认为这可能与多层子处理有关,然后我尝试通过像这样包装 SSH 调用来重现这一点bash -c
:bash -c 'ssh $BOUNCER "nc -k -l -U /tmp/weechat.notify.sock" &'
。这也表现出了仅两条消息的行为。
我还直接在远程计算机上进行了测试(通过 SSH 连接到本地主机,并包装在两个bash -c
调用中),并目睹了相同的损坏行为。 它似乎也与导致孤立进程的双分叉无关。 因为该进程是否最终成为孤儿似乎并不重要。
我还验证了这也发生在zsh
。
这似乎与进程在子处理层下运行时处理 STDIN 和 STDOUT 的方式有关。
重现。指令和strace
输出:
为了简化调试,我从图中删除了 SSH,并编写了两个简化的测试脚本,它们成功地在本地完全重现了该行为。
使用 Juergen Nickelsen 的socket
命令,我创建了一个本地 UNIX 域套接字 ( socket -l -s ./test.sock
),并再次能够irb
使用以下 Ruby 代码块向其发送测试消息:
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 -f
:http://pastebin.com/SMjti3qW
第二个脚本将包装函数置于后台(触发 2-and-done 行为):
#!/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 -f
:http://pastebin.com/WsrXX0EJ
当查看上述脚本的输出时,对我来说突出的一件事是特定于命令strace
的输出nc
。这似乎显示了这两个脚本执行之间的主要区别之一。
第一个脚本的“工作”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,
第二个脚本的nc
strace
输出中看到的“2-and-done”行为:
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
,所以我不太确定这些不同的输出意味着什么——除了一个显然在工作而另一个没有的事实。
当我挖掘更大的strace
输出时,前两条之后的消息似乎不再由换行符终止?但同样,我不确定这意味着什么,或者我是否正确地阅读了它。
我绝对不明白不同的子处理技术,甚至关闭 STDIN,可能会如何影响这种行为。
知道我在这里遇到了什么吗?
--
太长了;博士
我试图弄清楚为什么在不止一层子处理下运行我的通知侦听器会导致只处理两条消息;不这样做会导致 STDIN 出现竞争条件。
答案1
OpenBSD 的较新衍生版本netcat
(包括 FreeBSD[1] 和 Debian[2])支持一个-d
标志,该标志可防止从 stdin 读取并修复您所描述的问题。
问题是 netcat 正在轮询 stdin 及其“网络”fd,并且 stdin 在/dev/null
上面的第二种情况下重新打开,其中 shell 函数在创建管道之前在后台运行。这意味着第一次从 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 0poll(2)
两次:一次 for ,在没有命令行参数的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,而 fd 0 现在已关闭。 netcat 代码不处理在的成员POLLNVAL
中设置的情况,因此它进入无限循环,永远不会再次调用:.revents
struct pollfd
accept(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}])
...
在第一个命令中,管道处于后台但不在 shell 函数中运行,stdin 保持打开状态,因此不会出现这种情况。
代码参考(见readwrite
函数):
答案2
如果您像这样运行该函数,问题会消失吗?
irc_notify </dev/null &
如果是这样,问题可能是两个进程同时尝试从标准输入读取。像 zackse 建议的那样,使用 -n 运行所有 ssh 命令也可能有所帮助,至少可以调试哪些进程正在争夺 stdin。