Ler continuamente STDOUT do processo externo em Ruby

86

Eu quero executar o blender da linha de comando por meio de um script ruby, que irá então processar a saída fornecida pelo blender linha por linha para atualizar uma barra de progresso em uma GUI. Não é realmente importante que o blender seja o processo externo cujo padrão eu preciso ler.

Não consigo pegar as mensagens de progresso que o blender normalmente imprime no shell quando o processo do blender ainda está em execução, e tentei algumas maneiras. Sempre pareço acessar a saída padrão do liquidificador depois que ele sai, não enquanto ele ainda está funcionando.

Aqui está um exemplo de tentativa fracassada. Ele obtém e imprime as primeiras 25 linhas da saída do liquidificador, mas somente depois que o processo do liquidificador termina:

blender = nil
t = Thread.new do
  blender = open "| blender -b mball.blend -o //renders/ -F JPEG -x 1 -f 1"
end
puts "Blender is doing its job now..."
25.times { puts blender.gets}

Editar:

Para deixar um pouco mais claro, o comando que invoca o blender retorna um fluxo de saída no shell, indicando o progresso (parte 1-16 concluída, etc.). Parece que qualquer chamada para "obter" a saída é bloqueada até que o blender feche. A questão é como obter acesso a esta saída enquanto o blender ainda está rodando, já que o blender imprime sua saída no shell.

ehsanul
fonte

Respostas:

174

Tive algum sucesso em resolver este meu problema. Aqui estão os detalhes, com algumas explicações, caso alguém com problema semelhante encontre esta página. Mas se você não se importa com os detalhes, aqui está a resposta curta :

Use PTY.spawn da seguinte maneira (com seu próprio comando, é claro):

require 'pty'
cmd = "blender -b mball.blend -o //renders/ -F JPEG -x 1 -f 1" 
begin
  PTY.spawn( cmd ) do |stdout, stdin, pid|
    begin
      # Do stuff with the output here. Just printing to show it works
      stdout.each { |line| print line }
    rescue Errno::EIO
      puts "Errno:EIO error, but this probably just means " +
            "that the process has finished giving output"
    end
  end
rescue PTY::ChildExited
  puts "The child process exited!"
end

E aqui está a resposta longa , com muitos detalhes:

O verdadeiro problema parece ser que se um processo não liberar explicitamente seu stdout, então qualquer coisa gravada em stdout é armazenada em buffer em vez de realmente enviada, até que o processo seja concluído, para minimizar IO (este é aparentemente um detalhe de implementação de muitos Bibliotecas C, feitas de forma que o rendimento seja maximizado por meio de E / S menos frequente). Se você puder modificar facilmente o processo para que ele libere o stdout regularmente, essa seria sua solução. No meu caso, era o liquidificador, então um pouco intimidante para um noob completo como eu modificar a fonte.

Mas quando você executa esses processos a partir do shell, eles exibem stdout no shell em tempo real, e o stdout não parece estar em buffer. Ele só é armazenado em buffer quando chamado de outro processo, eu acredito, mas se um shell está sendo tratado, o stdout é visto em tempo real, sem buffer.

Esse comportamento pode até ser observado com um processo ruby ​​como o processo filho cuja saída deve ser coletada em tempo real. Basta criar um script, random.rb, com a seguinte linha:

5.times { |i| sleep( 3*rand ); puts "#{i}" }

Em seguida, um script ruby ​​para chamá-lo e retornar sua saída:

IO.popen( "ruby random.rb") do |random|
  random.each { |line| puts line }
end

Você verá que não obtém o resultado em tempo real como poderia esperar, mas tudo de uma vez depois. STDOUT está sendo armazenado em buffer, embora se você executar random.rb por conta própria, ele não será armazenado em buffer. Isso pode ser resolvido adicionando uma STDOUT.flushinstrução dentro do bloco em random.rb. Mas se você não pode mudar a fonte, você tem que contornar isso. Você não pode liberá-lo de fora do processo.

Se o subprocesso pode imprimir no shell em tempo real, então deve haver uma maneira de capturar isso com Ruby também em tempo real. E aqui está. Você tem que usar o módulo PTY, incluído no ruby ​​core, eu acredito (1.8.6 de qualquer maneira). O triste é que não está documentado. Mas eu encontrei alguns exemplos de uso felizmente.

Primeiro, para explicar o que é PTY, significa pseudo terminal . Basicamente, ele permite que o script ruby ​​se apresente ao subprocesso como se fosse um usuário real que acabou de digitar o comando em um shell. Portanto, qualquer comportamento alterado que ocorra apenas quando um usuário iniciar o processo por meio de um shell (como o STDOUT não sendo armazenado em buffer, neste caso) ocorrerá. Esconder o fato de que outro processo foi iniciado permite que você colete o STDOUT em tempo real, pois ele não está sendo armazenado em buffer.

Para fazer isso funcionar com o script random.rb como filho, tente o seguinte código:

require 'pty'
begin
  PTY.spawn( "ruby random.rb" ) do |stdout, stdin, pid|
    begin
      stdout.each { |line| print line }
    rescue Errno::EIO
    end
  end
rescue PTY::ChildExited
  puts "The child process exited!"
end
ehsanul
fonte
7
Isso é ótimo, mas acredito que os parâmetros de bloco stdin e stdout devem ser trocados. Veja: ruby-doc.org/stdlib-1.9.3/libdoc/pty/rdoc/…
Mike Conigliaro
1
Como fechar o pty? Mate o pid?
Boris B.
Resposta incrível. Você me ajudou a melhorar meu script de implantação de rake para heroku. Ele exibe o log do 'git push' em tempo real e aborta a tarefa se 'fatal:' encontrado gist.github.com/sseletskyy/9248357
Serge Seletskyy
1
Eu tentei usar este método originalmente, mas 'pty' não está disponível no Windows. Acontece que STDOUT.sync = trueé tudo o que é necessário (resposta de Mveerman abaixo). Aqui está outro tópico com algum código de exemplo .
Pakman
12

usar IO.popen. Este é um bom exemplo.

Seu código se tornaria algo como:

blender = nil
t = Thread.new do
  IO.popen("blender -b mball.blend -o //renders/ -F JPEG -x 1 -f 1") do |blender|
    blender.each do |line|
      puts line
    end
  end
end
Sinan Taifour
fonte
Eu tentei isso. O problema é o mesmo. Eu tenho acesso à saída depois. Eu acredito que IO.popen começa executando o primeiro argumento como um comando e espera que ele termine. No meu caso, a saída é dada pelo blender enquanto o blender ainda está processando. E então o bloqueio é invocado depois, o que não me ajuda.
ehsanul de
Aqui está o que tentei. Ele retorna a saída depois que o blender é feito: IO.popen ("blender -b mball.blend // renderiza / -F JPEG -x 1 -f 1", "w +") do | blender | blender.each {| linha | coloca linha; saída + = linha;} fim
ehsanul
3
Não tenho certeza do que está acontecendo no seu caso. Testei o código acima com yesum aplicativo de linha de comando que nunca termina e funcionou. O código era a seguinte: IO.popen('yes') { |p| p.each { |f| puts f } }. Suspeito que seja algo relacionado ao liquidificador e não ao rubi. Provavelmente o liquidificador nem sempre libera seu STDOUT.
Sinan Taifour
Ok, acabei de experimentar com um processo externo de ruby ​​para testar, e você está certo. Parece ser um problema do liquidificador. Obrigado pela resposta de qualquer maneira.
ehsanul de
Acontece que existe uma maneira de obter a saída através do ruby, mesmo que o liquidificador não libere seu stdout. Detalhes em uma resposta separada, caso você esteja interessado.
ehsanul
6

STDOUT.flush ou STDOUT.sync = true

mveerman
fonte
sim, esta foi uma resposta idiota. Sua resposta foi melhor.
mveerman de
Não coxo! Funcionou para mim.
Clay Bridges
Mais precisamente:STDOUT.sync = true; system('<whatever-command>')
caram
4

O Blender provavelmente não imprime quebras de linha até terminar o programa. Em vez disso, está imprimindo o caractere de retorno de carro (\ r). A solução mais fácil é provavelmente procurar a opção mágica que imprime quebras de linha com o indicador de progresso.

O problema é que IO#gets(e vários outros métodos de ES) usam a quebra de linha como um delimitador. Eles lerão o stream até atingirem o caractere "\ n" (que o blender não está enviando).

Tente definir o separador de entrada $/ = "\r"ou usar blender.gets("\r").

BTW, para problemas como esses, você deve sempre verificar puts someobj.inspectou p someobj(ambos fazem a mesma coisa) para ver quaisquer caracteres ocultos dentro da string.

hhaamu
fonte
1
Acabei de inspecionar a saída fornecida e parece que o blender usa uma quebra de linha (\ n), então esse não era o problema. Obrigado pela dica de qualquer maneira, vou manter isso em mente na próxima vez que estiver depurando algo assim.
ehsanul
0

Não sei se no momento em que o ehsanul respondeu à pergunta, ela ainda estava Open3::pipeline_rw()disponível, mas realmente torna as coisas mais simples.

Não entendo o trabalho do ehsanul com o Blender, então fiz outro exemplo com tare xz. tarirá adicionar arquivo (s) de entrada ao fluxo stdout, então xzpegar isso stdoute compactá-lo, novamente, para outro stdout. Nosso trabalho é pegar o último stdout e gravá-lo em nosso arquivo final:

require 'open3'

if __FILE__ == $0
    cmd_tar = ['tar', '-cf', '-', '-T', '-']
    cmd_xz = ['xz', '-z', '-9e']
    list_of_files = [...]

    Open3.pipeline_rw(cmd_tar, cmd_xz) do |first_stdin, last_stdout, wait_threads|
        list_of_files.each { |f| first_stdin.puts f }
        first_stdin.close

        # Now start writing to target file
        open(target_file, 'wb') do |target_file_io|
            while (data = last_stdout.read(1024)) do
                target_file_io.write data
            end
        end # open
    end # pipeline_rw
end
condichoso
fonte
0

Pergunta antiga, mas tinha problemas semelhantes.

Sem realmente mudar meu código Ruby, uma coisa que ajudou foi envolver meu pipe com stdbuf , assim:

cmd = "stdbuf -oL -eL -i0  openssl s_client -connect #{xAPI_ADDRESS}:#{xAPI_PORT}"

@xSess = IO.popen(cmd.split " ", mode = "w+")  

No meu exemplo, o comando real com o qual desejo interagir como se fosse um shell é o openssl .

-oL -eL diga a ele para armazenar STDOUT e STDERR somente até uma nova linha. Substitua Lpor 0para desempacotar completamente.

Porém, isso nem sempre funciona: às vezes o processo de destino impõe seu próprio tipo de buffer de fluxo, como outra resposta apontada.

Marcos
fonte