Un écouteur de notification se comporte étrangement lorsqu'il est en arrière-plan

J'ai eu du mal à faire fonctionner mon IRC-Bouncer / Notification Script.

Il s'agit d'un script qui se connecte automatiquement à une machine distante, se connecte à une session d'écran (ou en démarre une si aucune n'existe actuellement) en exécutant weechat, tout en ouvrant simultanément une autre connection ssh qui utilise netcat pour lire les notifications depuis un socket à laquelle un script d'ajout de weechat exporte mes messages de notification. Ces notifications sont ensuite introduites dans lib-notify (via notify-send) afin que je puisse être averti de l'activité dans weechat.

Voici le script:

#!/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 listning 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 listner…" ssh $BOUNCER 'pkill -f -x "nc -k -l -U /tmp/weechat.notify.sock"' 

La configuration fonctionne vraiment très bien, sauf pour un problème majeur. Seules deux notifications ont été transmises à mon gestionnaire de notifications, par invocation du script. Après ça: rien.

Pour éliminer les problèmes avec le script de notification dans weechat, j'ai supprimé la deuxième invocation ssh (celle qui se rattache à la session d'écran et lançait weechat) et l'ai remplacée par une command de read pour bloquer l'exécution. Ensuite, en utilisant irb sur la machine distante, j'ai envoyé des messages à la socket avec ruby.
Cependant, même lorsque j'émettais les messages manuellement, deux messages seulement apparaissaient avant de cesser de fonctionner .

strace m'a montré un comportement intéressant (quand je me suis attaché au process fork) où il semblait que les messages ont cessé d'être terminés par des caractères de nouvelle ligne après le premier ou le deuxième message. Mais après un peu plus, ils ont cessé d'apparaître dans strace tous set.

À ce stade, j'ai décidé de voir s'il y avait quelque chose dans mon script qui causait un comportement étrange. Donc, sur la command line, j'ai simplement appelé la connection ssh ( ssh $BOUNCER "nc -k -l -U /tmp/weechat.notify.sock" ) directement. Et voilà, tous les messages que j'émettais manuellement apparaissaient (codés en base64, bien sûr).

Alors, j'ai ajouté sur la logique de décoder chaque message tel qu'il apparaissait, comme dans mon script, et cela fonctionnait parfaitement pour chaque message. Y compris quand j'ai nourri ces messages dans notify-send.

Donc, à ce stade, j'ai décidé que quelque chose d'étrange doit se passer lorsque j'ai fourché la fonction . Mais je n'ai observé aucune différence d'efficacité lorsque j'ai mis les commands à l'arrière du terminal. Je me demandais donc si quelque chose d'étrange se produisait parce qu'il était exécuté à partir d'un script.

C'est quand les choses ont été bizarres …

J'ai commencé par casser la logique de la fonction et en l'invoquant directement, avec un esperluette à la fin des commands. Ainsi:

 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 & 

Dès que j'ai fait cela, les messages ont soudainement commencé à fonctionner. Et dès que j'ai inversé le changement, j'étais de return à la case départ avec le même comportement étrange à deux messages seulement.

Mais cette solution, introduit un autre comportement étrange. Une fois à l'intérieur de la session d'écran, je devais bash chaque touche plusieurs fois avant d'être enregistrée par le programme. Comme s'il y avait une course-condition se battre sur STDIN.

Il se peut que les deux sessions SSH se disputent (même si je ne savais pas pourquoi) j'ai essayé de fermer et / ou d'occuper STDIN sur la première command ssh par différents moyens. Tels que par la tuyauterie dans : | avant, ou appedning <&- ou </dev/null après la partie SSH du canal. Et bien que cela semble avoir résolu la condition de race, ceci a réintroduit le comportement à deux messages seulement.

En pensant que cela pourrait avoir quelque chose à voir avec être sous plusieurs couches de sous-traitement , j'ai ensuite essayé de reproduire ceci en enveloppant l'appel SSH avec bash -c comme ceci: bash -c 'ssh $BOUNCER "nc -k -l -U /tmp/weechat.notify.sock" &' . Et cela montre aussi le comportement à deux messages seulement.

Je suis également allé de l'avant et l'ai testé directement sur la machine distante (SSHing sur localhost et enveloppant dans deux invocations bash -c ) et j'ai vu le même comportement brisé. Il ne semble pas non plus être lié à un double fork provoquant des process orphelins. Comme cela ne semble pas important si le process se termine orphelin ou non.

J'ai aussi vérifié que cela se passe également sous zsh .

Il semble que cela soit en quelque sorte lié à la façon dont STDIN et STDOUT sont traités lorsqu'un process est exécuté sous des couches de sous-traitement.

Repro. Instructions et strace Sortie:

Afin de simplifier le debugging, j'ai supprimé SSH de l'image et j'ai écrit deux scripts de test simplifiés qui ont reproduit le comportement entièrement localement.

En utilisant la command socket Juergen Nickelsen, j'ai créé une socket de domaine UNIX locale ( socket -l -s ./test.sock ), et une fois de plus socket -l -s ./test.sock pu lui envoyer des messages de test avec irb utilisant le morceau de code Ruby suivant:

 require 'socket' require 'base64' SOCKET = './test.sock' def send(subtitle, message) UNIXSocket.open(SOCKET) do |socket| socket.puts "#{Base64.ssortingct_encode64(subtitle)} #{Base64.ssortingct_encode64(message)}" end end send('test', 'hi') send('test', 'hi') send('test', 'hi') send('test', 'hi') send('test', 'hi') send('test', 'hi') 

Le premier script n'a mis en arrière que l'expression canalisée (qui, comme indiqué précédemment, a traité un nombre illimité de messages):

 #!/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 listning 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 

Et produit la sortie suivante lorsqu'il est exécuté avec strace -f : http://pastebin.com/SMjti3qW

Le deuxième script a utilisé la fonction wrapping (qui triggers le comportement 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 listning 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 

Et qui, à son tour, a produit cette sortie suivante lorsqu'il est exécuté avec strace -f : http://pastebin.com/WsrXX0EJ

Une chose qui me tient à coeur en regardant la sortie strace des scripts ci-dessus est la sortie spécifique à la command nc . Ce qui semble montrer l'une des principales différences entre l'exécution de ces deux scripts.

Le résultat du script "working" nc strace premier script :

 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, 

Comportement "2-and-done" du second script vu dans la sortie nc strace :

 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]....... 

Je ne suis pas là où je voudrais être en ce qui concerne ma lisibilité de la sortie strace , donc je ne suis pas exactement sûr de ce que ces différentes sorties impliquent – mis à part le fait que l'un travaille évidemment, tandis que l'autre n'est pas.

Comme je l'ai creusé à travers la sortie strace plus grande, il semble également que les messages après les deux premiers ne sont plus terminés par une nouvelle ligne? Mais encore une fois, je ne suis pas sûr de ce que cela implique, ou si je le lis correctement.

Et je ne comprends vraiment pas comment les différentes techniques de sous-traitement, ou même la fermeture de STDIN, pourraient affecter ce comportement.

Une idée de ce que je rencontre ici?

tl; dr

J'essaie de comprendre pourquoi l'exécution de mon écouteur de notification sous plus d'une couche de sous-traitement entraîne le traitement de seulement deux messages; et ne pas le faire entraîne une condition de course sur STDIN.

Les nouveaux dérivés de l'OpenBSD netcat , y compris FreeBSD [1] et Debian [2], supportent un drapeau -d qui empêche la lecture de stdin et corrige le problème que vous avez décrit.

Le problème est que netcat interroge stdin ainsi que son "réseau" fd, et que stdin est rouvert de /dev/null dans le second cas ci-dessus, où la fonction shell est exécutée en arrière-plan avant la création du pipeline. Cela signifie un EOF immédiat sur la première lecture de stdin (fd 0), mais netcat continuera à poll(2) sur le stdin maintenant fermé, créant une boucle sans fin.

Voici la redirection de stdin avant la création du pipeline:

 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 

Maintenant, lorsque netcat (pid 23187) appelle son premier poll(2) , il lit EOF à partir de stdin et ferme 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 

Le prochain appel à accept(2) donne un client sur fd 0, qui est maintenant le fd libre le plus bas:

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

Notez que netcat inclut désormais deux fois fd 0 dans args to poll(2) : une fois pour STDIN_FILENO , qui est toujours inclus en l'absence du paramètre de command line -d et une fois pour le client nouvellement connecté:

 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}]) 

Le client envoie des déconnections EOF et 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 

Mais maintenant, il est en difficulté car il continuera à interroger sur fd 0, qui est maintenant fermé. Le code netcat ne gère pas le cas où POLLNVAL est défini dans le membre .revents de struct pollfd , donc il entre dans une boucle sans fin, pour ne plus jamais appeler 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}]) ... 

Dans la première command, où le pipeline est en arrière-plan mais n'est pas exécuté dans une fonction shell, stdin est laissé ouvert, donc ce cas ne se pose pas.

Références de code (voir la fonction readwrite ):

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

Est-ce que le problème disparaît si vous exécutez la fonction comme ceci?

 irc_notify </dev/null & 

Si c'est le cas, le problème est probablement deux process essayant simultanément de lire à partir de stdin. Exécuter toutes les commands ssh avec -n, comme zackse l'a suggéré, pourrait également aider, au less à déboguer les process qui se battent sur stdin.