Como encaminhar entre processos com pipes nomeados?

5

Os /tmp/in , /tmp/out e /tmp/err são pipes nomeados, já criados e abertos por algum processo (para leitura, escrita e escrita, respectivamente).

Eu gostaria de criar um novo processo que canalize seu stdin em /tmp/in , e escreva o conteúdo de /tmp/out em seu stdout, e o conteúdo de /tmp/err em seu stderr quando eles estiverem disponíveis. Tudo deve funcionar de maneira em buffer de linha . O processo deve sair quando o outro processo, que criou /tmp/in , parar de ler e fechar /tmp/in . A solução deve funcionar no Ubuntu, de preferência sem instalar nenhum pacote extra. Eu gostaria de resolvê-lo em um script bash.

mikeserv apontou que sem um SSCCE , é difícil entender o que eu quero. Então, abaixo está um SSCCE, mas tenha em mente que é um exemplo mínimo, então é muito bobo.

A configuração original

Um processo pai inicia um processo filho e se comunica com ele através do stdin e stdout da criança, linha por linha. Se eu correr, fico:

$ python parent.py 
Parent writes to child:  a
Response from the child: A

Parent writes to child:  b
Response from the child: B

Parent writes to child:  c
Response from the child: C

Parent writes to child:  d
Response from the child: D

Parent writes to child:  e
Response from the child: E

Waiting for the child to terminate...
Done!
$ 

parent.py

from __future__ import print_function
from subprocess import Popen, PIPE
import os

child = Popen('./child.py', stdin=PIPE, stdout=PIPE)
child_stdin  = os.fdopen(os.dup(child.stdin.fileno()), 'w')
child_stdout = os.fdopen(os.dup(child.stdout.fileno()))

for letter in 'abcde':
    print('Parent writes to child: ', letter)
    child_stdin.write(letter+'\n')
    child_stdin.flush()
    response = child_stdout.readline()
    print('Response from the child:', response)
    assert response.rstrip() == letter.upper(), 'Wrong response'

child_stdin.write('quit\n')
child_stdin.flush()
print('Waiting for the child to terminate...')
child.wait()
print('Done!')

child.py, deve ser executável!

#!/usr/bin/env python
from __future__ import print_function
from sys import stdin, stdout

while True:
    line = stdin.readline()
    if line == 'quit\n':
        quit()
    stdout.write(line.upper())
    stdout.flush()

A configuração desejada e uma solução hackish

Nem o arquivo de origem do pai nem o arquivo de origem da criança podem ser editados; Não é permitido.

Eu renomeio o child.py para child_original.py (e o faço executável). Então, eu coloco um script bash (um proxy ou um intermediário se você quiser) chamado child.py, inicie o child_original.py antes de rodar python parent.py e ter o parent.py chamando o falso child.py que agora é meu script bash, encaminhamento entre o parent.py e o child_original.py.

O falso child.py

#!/bin/bash
parent=$$
cat std_out &
(head -n 1 shutdown; kill -9 $parent) &
cat >>std_in

O start_child.sh para iniciar o child_original.py antes de executar o pai:

#!/bin/bash
rm -f  std_in std_out shutdown
mkfifo std_in std_out shutdown
./child_original.py <std_in >std_out
echo >shutdown
sleep 1s
rm -f  std_in std_out shutdown

A maneira de executá-los:

$ ./start_child.sh & 
[1] 7503
$ python parent.py 
Parent writes to child:  a
Response from the child: A

Parent writes to child:  b
Response from the child: B

Parent writes to child:  c
Response from the child: C

Parent writes to child:  d
Response from the child: D

Parent writes to child:  e
Response from the child: E

Waiting for the child to terminate...
Done!
$ echo 

[1]+  Done                    ./start_child.sh
$ 

Esta solução hackeada funciona. Tanto quanto eu sei, ele não atende a exigência de buffer de linha e há um fifo de desligamento extra para informar o start_child.sh que child_original.py fechou os pipes e start_child.sh pode sair com segurança.

A pergunta pede por um script falho child.py falso aprimorado, atendendo aos requisitos (linha em buffer, sai quando child_original.py fecha qualquer um dos canais, não requer um canal extra de desligamento).

Coisas que eu gostaria de saber:

  • Se uma API de alto nível for usada para abrir um fifo como um arquivo, ele deve ser aberto para leitura e gravação , caso contrário, a chamada para open já será bloqueada. Isso é incrivelmente contra-intuitivo. Consulte também Por que uma abertura somente leitura de um bloco de pipe nomeado?
  • Na realidade, meu processo pai é um aplicativo Java. Se você trabalha com um processo externo do Java, leia o stdout e o stderr do processo externo a partir de threads do daemon (chame setDamon(true) nesses threads antes iniciá-los). Caso contrário, a JVM será interrompida para sempre, mesmo que todos estejam prontos. Embora não relacionado à questão, outras armadilhas incluem: Navegue por si mesmo em torno de armadilhas relacionadas ao método Runtime.exec () .
  • Aparentemente, sem buffer significa buffer, mas não esperamos até que o buffer fique cheio, mas o liberemos assim que pudermos.
por Ali 12.07.2015 / 14:23

3 respostas

3

Se você se livrar do material de encerramento e desligamento (o que é inseguro e você pode, em um caso extremo, mas não insondável, quando child.py morre antes que a subcamada (head -n 1 shutdown; kill -9 $parent) & acabe kill -9 ing algum processo inocente ), então child.py não será encerrado porque seu parent.py não está se comportando como um bom cidadão do UNIX.

O subprocesso cat std_out & terá terminado quando você enviar a mensagem quit , porque o gravador de std_out é child_original.py , que termina ao receber quit , quando fecha seu stdout , que é o std_out pipe e que close fará o subprocesso cat terminar.

O cat > std_in não está terminando porque está lendo um pipe originado no processo parent.py e o processo parent.py não se preocupou em fechar esse canal. Se assim fosse, cat > stdin_in e consequentemente todo o child.py terminaria sozinho e você não precisaria do pipe de desligamento ou da killing part (matar um processo que não é seu filho no UNIX é sempre um potencial de segurança buraco se uma condição de corrida causada devido à reciclagem rápida do PID ocorrer.)

Processos na extremidade direita de um pipeline geralmente só terminam quando eles terminam de ler seu stdin, mas como você não está fechando isso ( child.stdin ), você está dizendo implicitamente ao processo filho "wait, i have mais entrada para você "e então você vai matá-lo, porque ele espera por mais entrada de você como deveria.

Em suma, faça com que parent.py se comporte de maneira razoável:

from __future__ import print_function
from subprocess import Popen, PIPE
import os

child = Popen('./child.py', stdin=PIPE, stdout=PIPE)

for letter in 'abcde':
    print('Parent writes to child: ', letter)
    child.stdin.write(letter+'\n')
    child.stdin.flush()
    response = child.stdout.readline()
    print('Response from the child:', response)
    assert response.rstrip() == letter.upper(), 'Wrong response'

child.stdin.write('quit\n')
child.stdin.flush()
child.stdin.close()
print('Waiting for the child to terminate...')
child.wait()
print('Done!')

E o seu child.py pode ser tão simples quanto

#!/bin/sh
cat std_out &
cat > std_in
wait #basically to assert that cat std_out has finished at this point

(Note que me livrei daquelas chamadas do fd dup porque de outra forma você precisaria fechar o child.stdin e o child_stdin duplicado).

Como parent.py opera de maneira orientada por linhas, gnu cat é sem buffer (como mikeserv apontou) e child_original.py opera de maneira orientada a linhas, você efetivamente tem a coisa toda com buffer de linha. / p>

Observação no Cat: O Unbufferred pode não ser o termo mais sortudo, pois o gnu cat usa um buffer. O que ele não faz é tentar obter todo o buffer antes de escrever as coisas (ao contrário do stdio). Basicamente, ele faz solicitações de leitura ao sistema operacional para um tamanho específico (seu tamanho de buffer) e grava o que recebe sem esperá-lo para obter uma linha inteira ou o buffer inteiro. ( read (2) pode ser preguiçoso e dar a você apenas o que ele pode lhe dar no momento em vez do todo buffer que você pediu.)

(Você pode inspecionar o código-fonte em link ; safe_read (usado em vez de read puro) está no submódulo gnulib e é um wrapper muito simples em torno de read (2 ) que abstrai EINTR (veja a man page)).

    
por 14.07.2015 / 16:40
2

Com um sed , a entrada sempre será lida em um buffer de linha e a saída pode ser explicitamente liberada por linha com o comando w . Por exemplo:

(       cd /tmp; c=
        mkfifo i o
        dd  bs=1    <o&
        sed -n w\ o <i&
        while   sleep 1
        do      [ -z "$c" ] && rm [io]
                [ "$c" = 5 ]   && exit
                date "+%S:%t$((c+=1))"
        done|   tee i
)
44: 1
44: 1
45: 2
45: 2
46: 3
46: 3
47: 4
47: 4
48: 5
48: 5
30+0 records in
30+0 records out
30 bytes (30 B) copied, 6.15077 s, 0.0 kB/s

... onde tee (que é especifique para não bloquear o buffer ) grava sua saída no terminal e no sed ' i pipe simultaneamente. sedi linha-a-linha e w rita cada linha que lê para seu o ut pipe assim que o fizer. dd lê o o ut canalizar um byte de cada vez e compartilha um stdout com tee e, portanto, ambos gravam a saída no terminal ao mesmo tempo. Isso não aconteceria se sed não explicitamente o buffer de linha. Aqui está a mesma execução, mas sem o comando w rite:

(       cd /tmp; c=
        mkfifo i o
        dd  bs=1    <o&
        sed ''  >o  <i&
        while   sleep 1
        do      [ -z "$c" ] && rm [io]
                [ "$c" = 5 ]   && exit
                date "+%S:%t$((c+=1))"
        done|   tee i
)
48: 1
49: 2
50: 3
51: 4
52: 5
48: 1
49: 2
50: 3
51: 4
52: 5
30+0 records in
30+0 records out
30 bytes (30 B) copied, 6.15348 s, 0.0 kB/s

Nesse caso, sed bloqueia buffers e não escreve nada em dd até que sua entrada seja fechada, e nesse momento ela libera sua saída e termina. Na verdade, ele sai quando o gravador faz nos dois casos, como é testemunhado no diagnóstico de dd sendo gravado no final do pipeline.

Ainda assim ...

(       cd /tmp; c=
        mkfifo i o
        dd  bs=1    <o&
        cat >o      <i&
        while   sleep 1
        do      [ -z "$c" ] && rm [io]
                [ "$c" = 5 ]   && exit
                date "+%S:%t$((c+=1))"
        done|   tee i
)
40: 1
40: 1
41: 2
41: 2
42: 3
42: 3
43: 4
43: 4
44: 5
44: 5
30+0 records in
30+0 records out
30 bytes (30 B) copied, 6.14734 s, 0.0 kB/s

Agora meu cat é uma versão GNU - e o cat (se chamado sem opções) do GNU nunca bloqueia buffers. Se você também está usando um GNU cat , então parece bastante claro para mim que o problema não é com seu relay, mas com seu programa Java. No entanto, se você não estiver usando um GNU cat , então há uma chance de que o buffer seja enviado. Para sua sorte, porém, existe apenas uma opção de especificação POSIX para cat e isso é para a saída -u nbuffered. Você pode tentar.

Eu estou olhando para o seu negócio, e depois de brincar com ele por algum tempo eu tenho certeza que seu problema é uma situação de deadlock. Você tem o cat pendurado na entrada lá no final, e se o processo da JVM também estiver esperando por alguém para falar com ele, provavelmente nada irá acontecer. Então eu escrevi isso:

#!/bin/sh
die()   for io  in  i o e
        do      rm "$io"
                kill -9 "$(($io))"
        done    2>/dev/null
io()    while   eval "exec $((fd+=1))>&-"
        do      [ "$fd" = 9 ] &&
                { cat; kill -1 0; }
        done
cd /tmp; fd=1
mkfifo   i o e
{   io <o >&4 & o=$!
    io <e >&5 & e=$!
    io >i <&3 & i=$!
}   3<&0  4>&1  5>&2
trap "die; exit 0" 1
echo; wait

É meio desleixado lidar com códigos de retorno, infelizmente. Com mais trabalho, pode-se fazê-lo de forma confiável. De qualquer forma, como você pode ver os fundos todos do cat s, escreve uma linha vazia para stdout, então wait s até que um dos cat s seja encerrado, o que deve desencadear um cadeia que mata todos eles em todos os casos, eu acho.

    
por 12.07.2015 / 17:40
0

No bash, você pode tentar:

forward() { while read line; do echo "$line"; done; } 
forward </tmp/out & 
forward </tmp/err >&2 &
forward >/tmp/in
wait

e, em seguida, execute o script com stdbuf -i0 -oL .

A função forward é basicamente o seu método pipe do seu código python com src e dest com o padrão stdin e stdout e sem o flushing explícito, esperando que stdbuf possa fazer isso.

Se você já está preocupado com o desempenho e deseja usá-lo no código C, coloque-o no código C. Eu não estou familiarizado com um stdbuf friendly cat alternativa mas aqui está um liner C ++ (quase) para cat ting stdin to stdout :

#include <iostream>
using namespace std;
int main() { for(string line; getline(cin,line); ){ cout<<line<<'\n'; }; }

Ou se você precisa ter o código C:

#include <stdlib.h>
#include <stdio.h>
int main()
{
  const size_t N = 80;
  char *lineptr = (char*)malloc(N); //this will get realloced if a longer line is encountered
  size_t length = N;
  while(getline(&lineptr, &length, stdin) != -1){
    fputs(lineptr, stdout);
  }
  free(lineptr);
  return 0;
}

Nem o exemplo C nem o C ++ limpam explicitamente após cada linha, porque acho que deixar isso para stdbuf é uma decisão de design melhor, mas você sempre pode adicionar isso chamando fflush(stdout) no exemplo C após cada linha , substituindo '\n' por endl no exemplo C ++, ou mais eficientemente, pré-configurando o buffer para o buffer de linha em ambos os casos, para que você não precise fazer as chamadas de função C / C ++ "caras".

    
por 12.07.2015 / 16:35