Ruby 生成过程,捕获 STDOUT/STDERR,同时表现得就像定期生成一样

Dan*_*552 2 ruby io subprocess pty

我想要实现的目标:

  • 从 Ruby 进程生成子进程
  • 子进程应正常打印回终端。我所说的“正常”是指该过程不应错过颜色输出,或忽略用户输入(STDIN)。
  • 对于该子进程,将 STDOUT/STDERR (联合)捕获到例如子进程终止后可以访问的字符串变量中。转义字符等等。

通过传递不同的 IO 管道可以捕获 STDOUT/STDERR,但是子进程可以检测到它不在tty. 例如git log,不会打印影响文本颜色的字符,也不会使用其寻呼机。

使用 apty启动进程本质上是“欺骗”子进程,使其认为它是由用户启动的。据我所知,这正是我想要的,而且其结果基本上满足了所有要求。

我测试解决方案是否满足我的需求的一般测试是:

  • 运行ls -al正常吗?
  • 运行vim正常吗?
  • 运行irb正常吗?

以下 Ruby 代码可以检查以上所有内容:

to_execute = "vim"

output = ""
require 'pty'
require 'io/console'

master, slave = PTY.open
slave.raw!

pid = ::Process.spawn(to_execute, :in => STDIN, [:out, :err] => slave)
slave.close
master.winsize = $stdout.winsize
Signal.trap(:WINCH) { master.winsize = $stdout.winsize }
Signal.trap(:SIGINT) { ::Process.kill("INT", pid) }

master.each_char do |char|
  STDOUT.print char
  output.concat(char)
end

::Process.wait(pid)
master.close
Run Code Online (Sandbox Code Playgroud)

这在大多数情况下都有效,但事实证明它并不完美。由于某种原因,某些应用程序似乎无法切换到某种raw状态。尽管vim工作得很好,但事实证明 Neovim 却没有。起初我以为这是 neovim 中的一个错误,但后来我能够使用 Rust 语言的 Termion crate 重现该问题。

通过在执行前手动设置为 raw ( IO.console.raw!),neovim 等应用程序的行为符合预期,但类似的应用程序则irb不然。

奇怪的是,在这个 pty 中,在 Python 中生成了另一个pty,允许应用程序按预期工作(使用python -c 'import pty; pty.spawn("/usr/local/bin/nvim")')。这显然不是一个真正的解决方案,但仍然很有趣。

对于我的实际问题,我想我正在寻求任何帮助来解决这个奇怪的raw问题,或者说,如果我完全误解了 tty/pty,我应该在哪里/如何看待问题有任何不同的方向。

Dan*_*552 5

[编辑:请参阅底部的修订更新]

弄清楚了 :)

为了真正理解这个问题,我阅读了大量有关 PTY 工作原理的文章。在我把它画出来之前,我认为我并没有真正理解它。基本上 PTY 可用于终端模拟器,这是考虑其数据流的最简单方法:

keyboard -> OS -> terminal -> master pty -> termios -> slave pty -> shell
                                               |
                                               v
 monitor <- OS <- terminal <- master pty <- termios
Run Code Online (Sandbox Code Playgroud)

(注意:这可能不是100%正确,我绝对不是这个主题的专家,只是发布它,以防它帮助其他人理解它)

因此,图中我没有真正意识到的重要一点是,当您键入时,您在屏幕上看到输入的唯一原因是因为它被传回向左)到母版。

首先,这个 ruby​​ 脚本应该首先将 tty 设置为 raw ( IO.console.raw!),它可以在执行完成后恢复它 ( IO.console.cooked!)。这将确保此父 Ruby 脚本不会打印键盘输入。

第二件事是从属本身不应该是原始的,因此该slave.raw!调用被删除。为了解释这一点,我最初添加了这个,因为它从输出中删除了额外的回车符:运行echo hello结果为"hello\r\n". 我错过的是这个回车是终端模拟器的关键指令(哎呀)。

第三件事,该进程应该只与从站交谈。通过STDIN感觉很方便,但它打乱了图中所示的流程。

这就带来了如何传递用户输入的新问题,所以我尝试了这个。所以我们基本上传递STDINmaster

  input_thread = Thread.new do
    STDIN.each_char do |char|
      master.putc(char) rescue nil
    end
  end
Run Code Online (Sandbox Code Playgroud)

这种方法有效,但它有自己的问题,因为某些交互过程有时没有收到密钥。时间会证明一切,但使用它IO.copy_stream似乎可以解决这个问题(当然读起来更好)。

input_thread = Thread.new { IO.copy_stream(STDIN, master) }
Run Code Online (Sandbox Code Playgroud)

8月21日更新:

因此,上面的示例大部分都有效,但由于某些原因,像 CTRL+c 这样的键仍然无法正常运行。我什至查阅了其他人的方法,看看我可能做错了什么,实际上,这似乎是相同的方法 - 就像IO.copy_stream(STDIN, master)成功发送3给主人一样。以下似乎没有任何帮助:

master.putc 3
master.putc "\x03"
master.putc "\003"
Run Code Online (Sandbox Code Playgroud)

在我深入研究尝试用较低级别的语言实现这一目标之前,我又尝试了一种东西 - 块语法。显然,块语法神奇地解决了这个问题。

为了防止这个答案变得过于冗长,以下方法似乎有效:

keyboard -> OS -> terminal -> master pty -> termios -> slave pty -> shell
                                               |
                                               v
 monitor <- OS <- terminal <- master pty <- termios
Run Code Online (Sandbox Code Playgroud)