如何阻止在 Ruby 中读取命名管道?

Posted

技术标签:

【中文标题】如何阻止在 Ruby 中读取命名管道?【英文标题】:How do I block on reading a named pipe in Ruby? 【发布时间】:2012-03-20 22:54:01 【问题描述】:

我正在尝试设置一个 Ruby 脚本,该脚本在循环中从命名管道中读取,阻塞直到管道中的输入可用。

我有一个进程定期将调试事件放入命名管道:

# Open the logging pipe
log = File.open("log_pipe", "w+") #'log_pipe' created in shell using mkfifo
...
# An interesting event happens
log.puts "Interesting event #4291 occurred"
log.flush
...

然后我想要一个单独的进程,该进程将从该管道中读取并在事件发生时将事件打印到控制台。我试过使用这样的代码:

input = File.open("log_pipe", "r+") 
while true
  puts input.gets  #I expect this to block and wait for input
end
# Kill loop with ctrl+c when done

我希望input.gets 阻塞,耐心等待新输入到达fifo;但它会立即读取 nil 并再次循环,滚动到控制台窗口的顶部。

我尝试过的两件事:

    我已经用“r”和“r+”打开了输入fifo——无论哪种方式我都有同样的问题;

    我试图确定我的写入过程是否正在发送 EOF(我听说这会导致读取 fifo 关闭)--AFAIK 不是。

一些背景:

如果有帮助,这里是我正在尝试做的事情的“全局”视图:

我正在开发一款在 RGSS(基于 Ruby 的游戏引擎)中运行的游戏。由于它没有很好的集成调试,我想在游戏运行时设置一个实时日志——当游戏中发生事件时,我希望消息显示在侧面的控制台窗口中。我可以使用类似于上面编写器代码的代码将 Ruby 游戏代码中的事件发送到命名管道;我现在正在尝试设置一个单独的进程,该进程将等待事件出现在管道中,并在它们到达时将它们显示在控制台上。我什至不确定是否需要 Ruby 来执行此操作,但这是我能想到的第一个解决方案。

请注意,我使用的是来自 cygwin 的 mkfifo,我碰巧已经安装了它;我想知道这是否是我麻烦的根源。

如果它对任何人有帮助,这正是我在 irb 中看到的“阅读器”流程:

irb(main):001:0> input = File.open("mypipe", "r")
=> #<File:mypipe>
irb(main):002:0> x = input.gets
=> nil
irb(main):003:0> x = input.gets
=> nil

我不希望 002 和 003 处的 input.gets 立即返回 - 我希望他们会阻止。

【问题讨论】:

@matt 进一步研究:我尝试在 Mac 上复制描述的设置,它运行良好;我现在认为问题不在于 Ruby,而在于 Cygwin 的命名管道实现,这显然是非常不稳定的(参见 cygwin.com/ml/cygwin/2011-01/msg00276.html 或 cygwin.com/ml/cygwin/2011-04/msg00276.html 示例)。我将尝试设计一个避免 Cygwin 的解决方案;如果我找到了,我会在这里发布。 我刚刚看到你的评论 - 我认为你是对的,问题在于 Cygwin 的命名管道。我在调查时发现了您链接到的其中一个邮件线程。不幸的是,我一般不知道 Cygwin 或 Windows 提供任何解决方案。祝你好运。 【参考方案1】:

我找到了一个完全避免使用 Cygwin 不可靠的命名管道实现的解决方案。 Windows 有自己的命名管道工具,甚至还有一个名为 win32-pipe 的 Ruby Gem 使用它。

不幸的是,似乎无法在 RGSS 脚本中使用 Ruby Gems。但是通过剖析 win32-pipe gem,我能够将相同的想法融入到 RGSS 游戏中。此代码是将游戏事件实时记录到后台通道所需的最低限度,但它对于深度调试非常有用。

我在“Main”之前添加了一个新的脚本页面并添加了这个:

module PipeLogger
  # -- Change THIS to change the name of the pipe!
  PIPE_NAME = "RGSSPipe"

  # Constant Defines
  PIPE_DEFAULT_MODE        = 0            # Pipe operation mode
  PIPE_ACCESS_DUPLEX       = 0x00000003   # Pipe open mode
  PIPE_UNLIMITED_INSTANCES = 255          # Number of concurrent instances
  PIPE_BUFFER_SIZE         = 1024         # Size of I/O buffer (1K)
  PIPE_TIMEOUT             = 5000         # Wait time for buffer (5 secs)
  INVALID_HANDLE_VALUE     = 0xFFFFFFFF   # Retval for bad pipe handle

  #-----------------------------------------------------------------------
  # make_APIs
  #-----------------------------------------------------------------------
  def self.make_APIs
    $CreateNamedPipe     = Win32API.new('kernel32', 'CreateNamedPipe', 'PLLLLLLL', 'L')
    $FlushFileBuffers    = Win32API.new('kernel32', 'FlushFileBuffers', 'L', 'B')
    $DisconnectNamedPipe = Win32API.new('kernel32', 'DisconnectNamedPipe', 'L', 'B')
    $WriteFile           = Win32API.new('kernel32', 'WriteFile', 'LPLPP', 'B')
    $CloseHandle         = Win32API.new('kernel32', 'CloseHandle', 'L', 'B')
  end

  #-----------------------------------------------------------------------
  # setup_pipe
  #-----------------------------------------------------------------------
  def self.setup_pipe
    make_APIs
    @@name = "\\\\.\\pipe\\" + PIPE_NAME

    @@pipe_mode = PIPE_DEFAULT_MODE
    @@open_mode = PIPE_ACCESS_DUPLEX
    @@pipe         = nil
    @@buffer       = 0.chr * PIPE_BUFFER_SIZE
    @@size         = 0
    @@bytes        = [0].pack('L')

    @@pipe = $CreateNamedPipe.call(
      @@name,
      @@open_mode,
      @@pipe_mode,
      PIPE_UNLIMITED_INSTANCES,
      PIPE_BUFFER_SIZE,
      PIPE_BUFFER_SIZE,
      PIPE_TIMEOUT,
      0
    )

    if @@pipe == INVALID_HANDLE_VALUE
      # If we could not open the pipe, notify the user
      # and proceed quietly
      print "WARNING -- Unable to create named pipe: " + PIPE_NAME
      @@pipe = nil
    else
      # Prompt the user to open the pipe
      print "Please launch the RGSSMonitor.rb script"
    end
  end

  #-----------------------------------------------------------------------
  # write_to_pipe ('msg' must be a string)
  #-----------------------------------------------------------------------
  def self.write_to_pipe(msg)
    if @@pipe
      # Format data
      @@buffer = msg
      @@size   = msg.size

      $WriteFile.call(@@pipe, @@buffer, @@buffer.size, @@bytes, 0)
    end
  end

  #------------------------------------------------------------------------
  # close_pipe
  #------------------------------------------------------------------------
  def self.close_pipe
    if @@pipe
      # Send kill message to RGSSMonitor
      @@buffer = "!!GAMEOVER!!"
      @@size   = @@buffer.size
      $WriteFile.call(@@pipe, @@buffer, @@buffer.size, @@bytes, 0)

      # Close down the pipe
      $FlushFileBuffers.call(@@pipe)
      $DisconnectNamedPipe.call(@@pipe)
      $CloseHandle.call(@@pipe)
      @@pipe = nil
    end
  end
end

要使用它,您只需要确保在编写事件之前调用PipeLogger::setup_pipe;并在游戏退出前致电PipeLogger::close_pipe。 (我将设置调用放在“Main”的开头,并添加一个ensure 子句来调用close_pipe。)之后,您可以在任何脚本中的任何位置添加对PipeLogger::write_to_pipe("msg") 的调用,并使用任何字符串"msg" 并写入管道。

我已经用 RPG Maker XP 测试了这段代码;它也应该适用于 RPG Maker VX 及更高版本。

您还需要从管道中读取一些内容。有很多方法可以做到这一点,但一种简单的方法是使用标准 Ruby 安装、win32-pipe Ruby Gem 和以下脚本:

require 'rubygems'
require 'win32/pipe'
include Win32

# -- Change THIS to change the name of the pipe!
PIPE_NAME = "RGSSPipe"

Thread.new  loop  sleep 0.01   # Allow Ctrl+C

pipe = Pipe::Client.new(PIPE_NAME)
continue = true

while continue
  msg = pipe.read.to_s
  puts msg

  continue = false if msg.chomp == "!!GAMEOVER!!"
end

我使用Ruby 1.8.7 for Windows 和上面提到的win32-pipe gem(请参阅here 以获得有关安装gem 的良好参考)。将上述内容另存为“RGSSMonitor.rb”,并从命令行调用它为ruby RGSSMonitor.rb

注意事项:

    上面列出的 RGSS 代码是脆弱的;特别是,它不处理打开命名管道的失败。这在您自己的开发机器上通常不是问题,但我不建议发布此代码。 我还没有测试过,但是我怀疑如果你在日志中写入很多东西而不运行一个进程来读取管道(例如RGSSMonitor.rb),你会遇到问题。 Windows 命名管道具有固定大小(我在此处将其设置为 1K),默认情况下,一旦管道被填满,写入将被阻塞(因为没有进程通过读取它来“缓解压力”)。不幸的是,RPGXP 引擎将终止一个已停止运行 10 秒的 Ruby 脚本。 (我听说 RPGVX 已经取消了这个看门狗功能——在这种情况下,游戏将挂起而不是突然终止。)

【讨论】:

谢谢!当我看到 win32-pipe 时,我想我可以一起破解一些东西。【参考方案2】:

可能发生的情况是正在退出写入进程,并且由于没有其他写入进程,EOF 被发送到导致gets 返回nil 的管道,因此您的代码不断循环。

要解决这个问题,您通常可以在阅读器端打开读写管道。这适用于我(在 Mac 上),但不适用于您(您已尝试过 "r""r+")。我猜这要归功于 Cygwin (POSIX says opening a FIFO read-write is undefined)。

另一种方法是打开管道两次,一次是只读的,一次是只写的。您不会将只写 IO 用于任何事情,只是为了始终有一个活动的写入器连接到管道,因此它不会被关闭。

input = File.open("log_pipe", "r")      # note 'r', not 'r+'
keep_open = File.open("log_pipe", "w")  # ensure there's always a writer
while true
  puts input.gets
end

【讨论】:

遗憾的是,情况似乎并非如此。你的解释是有道理的,但我非常有信心我不会关闭管道的写入端。我也试过你的“保命”技巧,但没有帮助。

以上是关于如何阻止在 Ruby 中读取命名管道?的主要内容,如果未能解决你的问题,请参考以下文章

如何在命名管道中的 EOF 之后恢复读取

在 Ruby exec 语句中使用命名管道

如何避免命名管道中的多个作者?

如何写入命名管道而不等待读取管道

为啥 os.path.exists() 会阻止 Windows 命名管道连接?

Linux。 Python。从命名管道读取