8

我有一个小 ruby​​ 脚本,它以mysql方式进行导入:mysql -u <user> -p<pass> -h <host> <db> < file.sql,但Open3.popen3用于这样做。这就是我到目前为止所拥有的:

mysqlimp = "mysql -u #{mysqllocal['user']} "
mysqlimp << "-h #{mysqllocal['host']} "
mysqlimp << "-p#{mysqllocal['pass']} "
mysqlimp << "#{mysqllocal['db']}"

Open3.popen3(mysqlimp) do |stdin, stdout, stderr, wthr|
  stdin.write "DROP DATABASE IF EXISTS #{mysqllocal['db']};\n"
  stdin.write "CREATE DATABASE #{mysqllocal['db']};\n"
  stdin.write "USE #{mysqllocal['db']};\n"

  stdin.write mysqldump #a string containing the database data
  stdin.close

  stdout.each_line { |line| puts line }
  stdout.close

  stderr.each_line { |line| puts line }
  stderr.close
end

那实际上是在做这项工作,但有一件事情让我感到困扰,与我想看到的输出有关。

如果我将第一行更改为:

mysqlimp = "mysql -v -u #{mysqllocal['user']} " #note the -v

然后整个脚本永远挂起。

我想,这是因为读写流相互阻塞,而且我还猜想stdout需要定期刷新,以便stdin继续消耗。换句话说,只要缓冲区stdout已满,进程就会等到它被刷新,但由于这是首先在最底层完成的,所以这永远不会发生。

我希望有人可以验证我的理论?我怎么能写出打印出所有内容的代码,stdout并将所有内容都写入其中stdin

提前谢谢!

4

2 回答 2

17
  • 由于您只写入标准输出,因此您可以简单地使用Open3#popen2ewhich consolidates stdoutand stderrinto a single stream。
  • 要将换行符终止的字符串写入流,您可以puts$stdout在简单的 hello world 程序中一样使用。
  • 您必须使用waith_thread.joinorwait_thread.value等​​到子进程终止。
  • 无论如何,如果您想立即查看结果,则必须启动一个单独的线程来从流中读取。

例子:

require 'open3'

cmd = 'sh'

Open3.popen2e(cmd) do |stdin, stdout_stderr, wait_thread|
  Thread.new do
    stdout_stderr.each {|l| puts l }
  end

  stdin.puts 'ls'
  stdin.close

  wait_thread.value
end

您的代码,已修复:

require 'open3'

mysqldump = # ...

mysqlimp = "mysql -u #{mysqllocal['user']} "
mysqlimp << "-h #{mysqllocal['host']} "
mysqlimp << "-p#{mysqllocal['pass']} "
mysqlimp << "#{mysqllocal['db']}"

Open3.popen2e(mysqlimp) do |stdin, stdout_stderr, wait_thread|
  Thread.new do
    stdout_stderr.each {|l| puts l }
  end

  stdin.puts "DROP DATABASE IF EXISTS #{mysqllocal['db']};"
  stdin.puts "CREATE DATABASE #{mysqllocal['db']};"
  stdin.puts "USE #{mysqllocal['db']};"
  stdin.close

  wait_thread.value
end
于 2014-08-15T12:44:06.377 回答
4

每当您从命令行或 via 启动进程时fork,该进程都会从父进程继承 stdin、stdout 和 stderr。这意味着,如果您的命令行在终端中运行,则新进程的 stdin、stdout 和 stderr 将连接到终端。

Open3.popen3另一方面,不将 stdin、stdout 和 stderr 连接到终端,因为您不希望直接的用户交互。所以我们需要别的东西。

对于标准输入,我们需要具有两种能力的东西:

  1. 父进程需要一些东西来将子进程从标准输入读取时应该获取的数据排入队列。
  2. 子进程需要提供read像标准输入那样的功能的东西。

对于标准输出和标准错误,我们需要类似的东西:

  1. 子进程需要写一些东西。 puts并且 print应该将父进程应该读取的数据排入队列。
  2. 父进程需要一些提供read函数的东西来获取子进程的 stdout 和 stderr 数据。

这意味着,对于标准输入、标准输出和标准错误,我们需要三个队列(FIFO)用于父进程和子进程之间的通信。这些队列的行为有点像文件,因为它们必须提供read( writefor putsand print)closeselect(is data available?)。因此,Linux 和 Windows 都提供匿名管道。这是传统的(本地)进程间通信机制之一。而且,嗯,Open3.popen3真的很想在两个不同的进程之间进行通信。这就是为什么Open3.popen3将标准输入、标准输出和标准错误连接到匿名管道的原因。

每个管道,无论是匿名的还是命名的,都有一个大小有限的缓冲区。此大小取决于操作系统。问题是:如果缓冲区已满并且一个进程试图写入管道,操作系统会暂停该进程,直到另一个进程从管道读取

这可能是你的问题:

  1. 您不断向子进程提供数据,但您没有读取子进程写入标准输出的内容。
  2. 因此,我们的子进程的输出不断累积在缓冲区中,直到缓冲区已满。
  3. 这是操作系统暂停您的子进程(putsprint块)的时间。
  4. 现在,您仍然可以将数据提供给连接到子进程的标准输入的匿名管道,直到积累了太多标准输入数据。标准输入管道的缓冲区已满。然后操作系统将暂停父进程(stdin.write将阻塞)。

我建议您使用Open3.capture2e或类似的包装器Open3.popen3。您可以使用关键字参数将数据传递给子流程:stdin_data

如果您坚持“交互式”地与您的子进程通信,则需要了解IO.select或使用多线程。这两个都是相当大的挑战。更好用Open3.capture*

于 2014-11-30T08:58:35.753 回答