Leitura contínua do STDOUT do processo externo em Ruby

Eu quero executar o Blender a partir da linha de comando através de um script ruby, que irá então processar a saída dada pelo liquidificador linha por linha para atualizar uma barra de progresso em uma GUI. Não é realmente importante que o liquidificador seja o processo externo cujo stdout eu preciso ler.

Parece que não consigo captar as mensagens de progresso que o liquidificador normalmente imprime no shell quando o processo do liquidificador ainda está em execução, e já tentei algumas maneiras. Eu sempre pareço acessar o stdout do liquidificador depois que o liquidificador parou, não enquanto ele ainda está rodando.

Aqui está um exemplo de uma tentativa fracassada. Ele obtém e imprime as primeiras 25 linhas da saída do liquidificador, mas somente após o processo do liquidificador ter saído:

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 torná-lo um pouco mais claro, o comando chamando liquidificador retorna um stream de saída no shell, indicando o progresso (parte 1-16 concluída etc). Parece que qualquer chamada para “recebe” a saída é bloqueada até que o liquidificador seja encerrado. A questão é como obter access a essa saída enquanto o blender ainda está em execução, já que o blender imprime sua saída para o shell.

Eu tive algum sucesso em resolver este problema meu. Aqui estão os detalhes, com algumas explicações, no caso de alguém com um problema semelhante encontrar esta página. Mas se você não se importa com 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 problema real parece ser que, se um processo não esvazia explicitamente seu stdout, então qualquer coisa escrita em stdout é armazenada em buffer em vez de ser enviada, até que o processo seja concluído, para minimizar o IO ( aparentemente é um detalhe de implementação de muitos Bibliotecas C, feitas para que a taxa de transferência seja maximizada por meio de IO menos frequente). Se você puder modificar facilmente o processo para que ele elimine o stdout regularmente, essa será a sua solução. No meu caso, era 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 para o shell em tempo real, e o stdout não parece estar em buffer. É apenas buffer quando chamado de outro processo, creio eu, 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 seria de esperar, mas de uma vez só depois. STDOUT está sendo armazenado em buffer, mesmo se você executar random.rb por conta própria, ele não está em buffer. Isso pode ser resolvido adicionando uma instrução STDOUT.flush dentro do bloco em random.rb. Mas se você não pode mudar a fonte, você tem que contornar isso. Você não pode expulsá-lo de fora do processo.

Se o subprocess puder imprimir no shell em tempo real, deve haver uma maneira de capturar isso com o Ruby em tempo real também. E aqui está. Você tem que usar o módulo de PTY, incluído no núcleo de ruby, 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 é o PTY, ele significa pseudo-terminal . Basicamente, ele permite que o script ruby ​​se apresente ao subprocess como se fosse um usuário real que acabou de digitar o comando em um shell. Portanto, qualquer comportamento alterado que ocorre apenas quando um usuário iniciou o processo por meio de um shell (como o STDOUT não sendo armazenado em buffer, nesse caso) ocorrerá. Ocultar o fato de que outro processo iniciou esse processo permite coletar o STDOUT em tempo real, já que 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 

use 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 

STDOUT.flush ou STDOUT.sync = true

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

O problema é que o IO#gets (e vários outros methods de IO) usam a quebra de linha como um delimitador. Eles lerão o stream até que eles atinjam o caractere “\ n” (que o liquidificador não está enviando).

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

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

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

Eu não entendo o trabalho do ehsanul com o Blender, então eu fiz outro exemplo com tar e xz . tar adicionará o (s) arquivo (s) de input ao stream stdout, em seguida, tomará xz esse stdout e o compactará novamente para outro stdout. Nosso trabalho é pegar o último stdout e escrevê-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