Allan
Allan

Reputation: 4710

Continuously read from STDOUT and STDERR in real time

Say we have a small application (example.rb) which behave like the following ruby code:

#!/usr/bin/env ruby

done = false

out =Thread.new do
    cnt = 0
    while !done
        STDOUT.puts "out #{cnt}"
        cnt += 1
        sleep 1
    end
end

err = Thread.new do
    cnt = 0
    while !done
        STDERR.puts "err #{cnt}"
        cnt += 1
        sleep 1
    end
end


while true
    i = STDIN.gets
    if i == "q\n"
        puts "Quiting"
        done = true
        break
    end
end

out.join
err.join

exit 42

It print something to stdout and to stderr, it must be quited by writing "q\n" to stdin, and when it exit a value is returned in the return code.

Now, I would like to write a small ruby script which can run this program in an external process, where stdout and stdin are captured, and when the external process should be terminated this is done by writing "q\n" to its stdin. This program is called monitor.rb.

This is what I have tried here:

#!/usr/bin/env ruby

require 'open3'

class Monitor
    @cmd
    attr_accessor :return_code

    def initialize cmd
        @cmd = cmd
    end

    def run
        @runner_thread = Thread.new do
            Open3::popen3(@cmd) do |stdin, stdout, stderr, thread|
                puts "#{Time.now} #{@cmd} is running as pid: #{thread.pid}"

                stdin.sync = true;
                stdout.sync = true;
                stderr.sync = true;
                @stdin = stdin

                t_out = Thread.new do
                    stdout.readlines do |l|
                        puts "#{Time.now} STDOUT> #{l}"
                    end
                end

                t_err = Thread.new do
                    stderr.readlines do |l|
                        puts "#{Time.now} STDERR> #{l}"
                    end
                end

                thread.join
                t_err.join
                t_out.join
                @return_code = thread.value
            end
        end
    end

    def quit
        puts "Quiting"
        @stdin.puts "q"
        @stdin.close
        @runner_thread.join
    end
end

mon = Monitor.new "./example.rb"
mon.run
sleep 5
mon.quit
puts "Return code: #{mon.return_code}"

Question 1: What is wrong with my code since the output of the external process is not being printed?

Question 2: Can this be done in a more elegant way, and what would that look like?

The code must be able to run on Linux and portability is not a priority, I uses ruby 2.0.

When run example.rb in a terminal I get:

$ ./example.rb 
out 0
err 0
out 1
err 1
out 2
err 2
q
Quiting

When I run the monitor application I get:

$ ./monitor.rb 
2013-11-19 14:39:20 +0100 ./example.rb is running as pid: 7228
Quiting
Return code: pid 7228 exit 42

I expected the monitor.rb to print the output from example.rb

Upvotes: 1

Views: 1555

Answers (1)

Adrian
Adrian

Reputation: 86

Try changing your t_out and t_err threads to use the following code. readlines will read the entire file at once and stdout and stderr will block until your script exits. I think this is why you were not getting any output.

while l = stdout.gets
  puts "#{Time.now} STDOUT> #{l}"
end

This should print out to the screen as soon as any output is available.

Upvotes: 2

Related Questions