그때 상황이 이상해졌어...

그때 상황이 이상해졌어...

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"'

한 가지 주요 결함을 제외하고 설정은 실제로 꽤 잘 작동합니다. 스크립트가 호출될 때마다 내 알림 관리자에는 두 개의 알림만 전송됩니다. 이후: 아무것도 없습니다.

그래서 weechat의 알림 스크립트 문제를 제거하기 위해 두 번째 ssh 호출(screen 세션에 연결되고 weechat을 시작하는 호출)을 제거하고 read테스트하는 동안 실행을 차단하는 명령으로 대체했습니다. 그런 다음 irb원격 컴퓨터에서 Ruby를 사용하여 소켓에 메시지를 보냅니다.
하지만,수동으로 메시지를 보내도 작동이 멈추기 전에 나타나는 메시지는 여전히 두 개뿐입니다..

strace몇 가지 흥미로운 동작을 보여주었는데(포크된 프로세스에 연결했을 때) 첫 번째 또는 두 번째 메시지 이후에 메시지가 더 이상 개행 문자로 종료되지 않는 것 같았습니다. 그러나 몇 번 더 지나면 그들은 더 이상 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 &

이 작업을 수행했을 때 갑자기 메시지가 작동하기 시작했습니다. 변경 사항을 되돌린 후에는 똑같은 이상한 두 메시지 동작으로 돌아갔습니다.

하지만 이 수정으로 인해 다른 이상한 동작이 발생했습니다. 스크린 세션에서 프로그램에 등록되기 전에 각 키를 여러 번 눌러야 했습니다. STDIN에 경쟁 조건이 있는 것과 같습니다.

아마도 두 개의 SSH 세션이 이를 위해 싸우고 있다고 생각하면서(이유는 확실하지 않지만) 첫 번째 ssh 명령에서 STDIN을 닫거나 호깅하는 다양한 방법을 시도했습니다. 예를 들어 파이프의 SSH 부분 : |앞 이나 뒤에 <&-연결을 파이프합니다 . </dev/null이렇게 하면 경쟁 조건이 해결되는 것처럼 보이지만 두 개의 메시지만 동작하게 됩니다.

생각하다이는 다층 하위 처리와 관련이 있을 수 있습니다., 그런 다음 SSH 호출을 다음과 같이 래핑하여 이를 재현하려고 했습니다 bash -c. bash -c 'ssh $BOUNCER "nc -k -l -U /tmp/weechat.notify.sock" &'이는 또한 두 개의 메시지만으로 동작을 나타냅니다.

또한 원격 컴퓨터(SSH를 통해 localhost에 연결되고 두 번의 bash -c호출로 래핑)에서 직접 테스트했는데 동일한 손상 동작을 목격했습니다. 또한 고아 프로세스로 이어지는 이중 포크와 관련이 없는 것 같습니다. 프로세스가 결국 고아가 되는지 여부는 중요하지 않은 것 같기 때문입니다.

나는 또한 이것이 또한 발생한다는 것을 확인했습니다.zsh.

이는 프로세스가 하위 처리 계층에서 실행될 때 STDIN 및 STDOUT이 처리되는 방식과 관련된 것으로 보입니다.

다시 나타납니다. 지침 및 strace출력:

디버깅을 단순화하기 위해 다이어그램에서 SSH를 제거하고 동작을 로컬에서 정확하게 재현하는 두 개의 단순화된 테스트 스크립트를 작성했습니다.

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 -f.http://pastebin.com/SMjti3qW

두 번째 스크립트래퍼 함수를 ​​백그라운드에 배치합니다(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 -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회 완료" 동작은 다음과 같습니다.

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] 포함)은 -dstdin에서 읽기를 방지하는 플래그를 지원하고 설명하는 문제를 수정합니다.

문제는 netcat이 stdin과 해당 "네트워크" fd를 폴링하고 stdin이 /dev/null파이프를 생성하기 전에 백그라운드에서 쉘 함수가 실행되는 위의 두 번째 경우에서 다시 열린다는 것입니다. 이는 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이 두 번 포함됩니다 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 &

그렇다면 표준 입력에서 동시에 읽으려고 하는 두 프로세스가 문제일 수 있습니다. 또한 zackse가 제안한 대로 -n을 사용하여 모든 ssh 명령을 실행하는 것이 도움이 될 수 있습니다. 적어도 어떤 프로세스가 stdin을 위해 싸우고 있는지 디버깅하는 것입니다.

관련 정보