Ruby Open3标准库中的stdout和stdin如何交互

8

sum.rb非常简单。您输入两个数字,它会返回它们的和。

# sum.rb
puts "Enter number A"
a = gets.chomp
puts "Enter number B"
b = gets.chomp
puts "sum is #{a.to_i + b.to_i}"

robot.rb 使用 Open3.popen3sum.rb 进行交互。以下是代码:

# robot.rb
require 'open3'

Open3.popen3('ruby sum.rb') do |stdin, stdout, stderr, wait_thr| 
  while line = stdout.gets
    if line == "Enter number A\n"
      stdin.write("10\n")
    elsif line == "Enter number B\n"
      stdin.write("30\n")
    else
      puts line
    end
  end
end

robot.rb无法运行。似乎它卡在了sum.rbgets.chomp处。

后来我发现,我必须按照以下方式编写才能使其正常工作。您需要提前以正确的顺序输入所需的内容。

# robot_2.rb
require 'open3'

Open3.popen3('ruby sum.rb') do |stdin, stdout, stderr, wait_thr| 
  stdin.write("10\n")
  stdin.write("30\n")
  puts stdout.read
end

我感到困惑的是:
  1. robot_2.rb 不像 与 shell 交互,更像是 提供 shell 所需的内容,因为我只知道这些。如果一个程序需要很多输入,而我们无法预测顺序怎么办?

  2. 我发现如果在 sum.rb 的每个 puts 后面添加 STDOUT.flushrobot.rb 就可以运行了。但实际上我们不能相信 sum.rb 的作者会添加 STDOUT.flush,对吗?

谢谢您的时间!
2个回答

1

终于弄明白了如何做。使用 write_nonblockreadpartial。你需要小心的是,stdout.readpartial 确切地做了它说的事情,这意味着你需要聚合数据并通过查找换行符来执行 gets

require 'open3'
env = {"FOO"=>"BAR", "BAZ"=>nil}
options = {}
Open3.popen3(env, "cat", **options) {|stdin, stdout, stderr, wait_thr|
    stdin.write_nonblock("hello")

    puts stdout.readpartial(4096)
    # the magic 4096 is just a size of memory from this example:
    # https://apidock.com/ruby/IO/readpartial


    stdin.close
    stdout.close
    stderr.close
    wait_thr.join
}

对于想要更通用互动的人(例如ssh互动),您可能需要创建单独的线程来聚合标准输出并触发标准输入。

require 'open3'
env = {"FOO"=>"BAR", "BAZ"=>nil}
options = {}
unprocessed_output = ""
Open3.popen3(env, "cat", **options) {|stdin, stdout, stderr, wait_thr|

    on_newline = ->(new_line) do
        puts "process said: #{new_line}"
        # close after a particular line
        stdin.close
        stdout.close
        stderr.close
    end

    Thread.new do
        while not stdout.closed? # FYI this check is probably close to useless/bad
            unprocessed_output += stdout.readpartial(4096)
            if unprocessed_output =~ /(.+)\n/
                # extract the line
                new_line = $1
                # remove the line from unprocessed_output
                unprocessed_output.sub!(/(.+)\n/,"")
                # run the on_newline
                on_newline[new_line]
            end

            # in theres no newline, this process will hang forever
            # (e.g. probably want to add a timeout)
        end
    end

    stdin.write_nonblock("hello\n")

    wait_thr.join
}

顺便说一句,这并不是非常线程安全的。这只是我找到的一个未经优化但功能齐备的解决方案,希望将来会得到改进。


0
我稍微修改了@jeff-hykin的答案。所以,主要部分是以非阻塞模式发送sum.rb中的数据,即使用STDOUT.write_nonblock
# sum.rb
STDOUT.write_nonblock "Enter number A\n"
a = gets.chomp
STDOUT.write_nonblock "Enter number B\n"
b = gets.chomp
STDOUT.write_nonblock "sum is #{a.to_i + b.to_i}"

-- 注意在 STDOUT.write_nonblock 调用中的 \n。它将使用 gets 读取的字符串/行分隔开来,在 robot.rb 中进行操作。然后,只需在条件中添加 strip 即可使 robot.rb 保持不变。

# robot.rb
require 'open3'

Open3.popen3('ruby sum.rb') do |stdin, stdout, stderr, wait_thr|
  while line = stdout.gets
    puts "line: #{line}" # for debugging
    if line.strip == "Enter number A"
      stdin.write("10\n")
    elsif line.strip == "Enter number B"
      stdin.write("30\n")
    else
      puts line
    end
  end
end

我的 Ruby 版本是 3.0.2。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接