【问题标题】:Discussion with a sub-process, using Ruby with IO and threading讨论一个子进程,使用 Ruby 和 IO 和线程
【发布时间】:2011-01-22 16:49:33
【问题描述】:

我正在尝试使用 IO.popen 将(使用 .puts 方法)和获取(使用 .gets 方法)消息从一个进程发送到其子进程。

我不是很有经验,我有一个问题。有以下代码,我有一个错误,因为无法在封闭的流中写入。

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 opens for writing (IOError)

正如我们所见,这个错误发生在 is_cool?测试(解释:http://ruby-doc.org/core/classes/IO.html#M002289)。

但如果我尝试在 process_command 方法中注释该行:

# sub_process.close_write

脚本似乎在休眠...无限:s

我相信不可能再次打开已关闭的流。而且我无法创建我的程序“./program”的另一个 IO.popen 实例,因为它需要在开始时使用一些命令(比如“你准备好了吗?”和“你酷吗?”)进行初始化,之前我使用它(通过像简单讨论一样发送和接收消息)。

为了解决这个问题,我可以对当前代码进行哪些更改?

编辑:换句话说,我想建立这样的通信(根据给定的协议):

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

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

非常感谢您的帮助。

【问题讨论】:

  • 如果将sub_process.close_write 替换为sub_process.flush 会发生什么?
  • 父进程似乎无限等待。如果我使用 Timeout::timeout(5) { ... },则退出 Timeout::Error。

标签: ruby process multithreading io popen


【解决方案1】:

也许有一个可行的例子会有所帮助。这是一个,经过测试,已知可在 Linux 上的 MRI 1.8.7 中工作。

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 输出

You said: blah blah blah
You said: foo bar baz

【讨论】:

  • 哇,令人印象深刻!谢谢!关于这个解决方案的一个小问题:如果子命令是“CHILD_COMMAND = 'yes'”,那么它也可以工作(并打印“y\n”两次),但如果我们尝试执行这个 Ruby 脚本:“#! /usr/bin/ruby loop { puts 'y' }",则输出为:"$ ./child.rb:2:in `write': Broken pipe (Errno::EPIPE)"。你知道原因吗?
  • 该错误是父进程退出而子进程仍在运行的结果。这会关闭管道,导致子进程从被阻塞的gets 提升Errno::EPIPE。解决方案是要么让子进程优雅地处理 Errno::EPIPE,要么在父进程退出之前结束子进程。如果您希望父母杀死孩子,请通过管道向孩子发送“请立即死亡”消息,或向其发送 TERM 信号。无论哪种方式,您都希望父母在退出之前等待孩子死亡。
【解决方案2】:

已关闭的IO 无法再使用。如果您打算继续使用 IO,则不应关闭它。

如果您删除IO#close_write,您的代码在下一行中仍然存在问题。

rdr = Thread.new { sub_process.read }

IO#read 读到 EOF。因此,在流关闭之前,它永远不会终止。您在代码中提到了IO#readline,这将是更好的选择。使用IO#readline,只有在 popend 进程从不发送换行符时,您的程序才会挂起。

popen 的另一个问题如下。 IO#popen 创建一个新进程。进程可能会被您、其他用户、内存不足等杀死。不要期望您的流程始终运行。如果进程被杀死IO#readline 将抛出一个EOFErrorIO#read 将返回 imidiatley。您可以通过以下代码确定终止原因。

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

【讨论】:

  • 感谢您的回复非常有教育意义,非常清楚。我测试了 IO#readline 而不是 IO#read,但我再次收到“写入”错误。我不知道如何解决它。
【解决方案3】:

使用Thread.new这种形式有帮助吗?

rdr = Thread.new(sub_process) {|x| x.readlines }

【讨论】:

  • 很酷的语法:) 但是根据我的测试,这种形式似乎产生了相同的效果。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-16
  • 1970-01-01
  • 2020-07-17
  • 2021-03-16
  • 2013-01-16
  • 1970-01-01
相关资源
最近更新 更多