views:

56

answers:

3

Hi everyone,

I am trying to use IO.popen in order to put (with .puts method) and to get (with .gets method) messages from a process to its sub-process.

I am not very experimented and I have a question about. Having the following code, I have an error because it is not possible to write in a closed stream.

class Interface
  def initialize(path)
    @sub_process = IO.popen(path, 'w+')
  end

  def start!
    if ok?
      @sub_process.puts 'Hello', 'my name is ...'
      # and more...
    end
  end

  protected

  def ok?
    is_ready?(@sub_process) && is_cool?(@sub_process)
  end

  def is_ready?(sub_process)
    reply = process_command(sub_process, 'are u ready?')
    reply.chomp.match(/yes_i_am_ready$/)
  end

  def is_cool?(sub_process)
    reply = process_command(sub_process, 'are u cool?')
    reply.chomp.match(/yes_i_am_cool$/)
  end

  def process_command(sub_process, command)
    rdr = Thread.new { sub_process.read } # alternative: io.readlines
    sub_process.puts "#{command}"
    sub_process.close_write
    rdr.value # joins and fetches the result
  end
end

a = Interface.new("./program")
a.start!

(...) in `write': not opened for writing (IOError)

As we can see, this error occur during is_cool? test (as explained at: http://ruby-doc.org/core/classes/IO.html#M002289).

But if I try to comment in process_command method the line:

# sub_process.close_write

the script seems to sleep... infinitely :s

I believe that it is not possible to open again a closed stream. And I can't create an other IO.popen instance of my program "./program" because it needs to be initialized with some command (like 'are u ready?' and 'are u cool?') at the beginning, before I use it (by sending and receiving messages like a simple discussion).

How changes can I do over the current code in order to solve this problem?

Edit: in other words, I would like to establish a such communication (according to a given protocol):

Parent message:                Child answer:
--------------                 ------------

'are u ready?'                 'yes_i_am_ready'
'are u cool?'                  'yes_i_am_cool'
'Hello'                        'foo'
'my name is ...'               'bar'

Many thanks for any help.

A: 

A closed IO can not be used anymore. You should not close an IO if you intend on still using it.

If you remove the IO#close_write there still remains the problem with your code in the following line.

rdr = Thread.new { sub_process.read }

IO#read read's until EOF. So until the stream get's closed it never terminates. You mentioned IO#readline in your code, this would be the better alternative. Using IO#readline your program would only hang if the popend process never send's a newline.

Another problem with popen is the following. IO#popen create's a new process. Process's may be killed by you, other users, memory shortages, …. Don't expect your process to always run all the time. If the process is killed IO#readline will throw an EOFError, IO#read will return imidiatley. You can determine the termination reason with the following code.

Process::wait(io.pid)
status= $?
status.class # => Process::Status
status.signaled? # killed by signal?
status.stopsig # the signal which killed it
status.exited # terminated normal
status.exitstatus # the return value
status.ki
johannes
Thank you for your reply very educational, very clear. I have tested IO#readline instead of IO#read, but again I get a "write" error. I don't exactly see how to fix it.
Puru puru rin..
A: 

Perhaps it will help to have a working example. Here's one, tested and known to work in MRI 1.8.7 on Linux.

bar.rb

#!/usr/bin/ruby1.8

begin
  loop do
    puts "You said: #{gets}"
    $stdout.flush
  end
rescue Errno::EPIPE
end

foo.rb

#!/usr/bin/ruby1.8

class Parent

  def initialize
    @pipe = IO.popen(CHILD_COMMAND, 'w+')
  end

  def talk(message)
    @pipe.puts(message)
    response = @pipe.gets
    if response.nil?
      $stderr.puts "Failed: #{CHILD_COMMAND}"
      exit(1)
    end
    response.chomp
  end

  private

  CHILD_COMMAND = './bar.rb'

end

parent = Parent.new
puts parent.talk('blah blah blah')
puts parent.talk('foo bar baz')

foo.rb output

You said: blah blah blah
You said: foo bar baz
Wayne Conrad
Wow, impressive! Thanks! Just a thin question about this solution: if the child command is "CHILD_COMMAND = 'yes'", then it work too (and print "y\n" twice), but if we try to execute this Ruby script instead: "#!/usr/bin/rubyloop { puts 'y' }", then the output is: "$ ./child.rb:2:in `write': Broken pipe (Errno::EPIPE)". Do you know the reason?
Puru puru rin..
The error is a result of the parent exiting while the child process is still running. That closes the pipe, causing the child process to raise `Errno::EPIPE` from the `gets` it was blocked on. The solution is either to make the child process handle Errno::EPIPE gracefully, or to end the child process before the parent exits. If you want the parent to kill the child, send the child a "please die now" message via the pipe, or send it a TERM signal. Either way, you'll want the parent to wait for the child to be dead before exiting.
Wayne Conrad
A: 

Does it help to use this form of Thread.new?

rdr = Thread.new(sub_process) {|x| x.readlines }
bta
Cool syntax :) But according my test, this form seems to produce the same effect.
Puru puru rin..