Ruby 生成过程,捕获 STDOUT/STDERR,同时表现得好像它是定期生成的

Ruby spawn process, capturing STDOUT/STDERR, while behaving as if it were spawned regularly

我想要达到的目标:

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

使用 pty 启动进程本质上是 "tricks" 子进程认为它是由用户启动的。据我所知,这正是我想要的,其结果基本上符合所有要求。

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

以下 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

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

通过在执行前手动设置为 raw (IO.console.raw!),像 neovim 这样的应用程序会按预期运行,但像 irb 这样的应用程序不会。

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

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

[已编辑:修改更新见底部]

想通了:)

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

keyboard -> OS -> terminal -> master pty -> termios -> slave pty -> shell
                                               |
                                               v
 monitor <- OS <- terminal <- master pty <- termios

(注意:这可能不是 100% 正确,我绝对不是这方面的专家,只是张贴它以防它帮助其他人理解它)

所以图表中我没有真正意识到的重要一点是,当您键入时,您在屏幕上看到输入的唯一原因是因为它已通过返回(向左)交给主人。

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

其次,slave 本身不应该是原始的,所以 slave.raw! 调用被移除了。为了解释这一点,我最初添加这个是因为它从输出中删除了额外的 return 回车:运行 echo hello 结果为 "hello\r\n"。我错过的是这个return马车是终端仿真器的关键指令(哎呀)。

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

这带来了一个关于如何传递用户输入的新问题,所以我尝试了这个。所以我们基本上将 STDIN 传递给 master:

  input_thread = Thread.new do
    STDIN.each_char do |char|
      master.putc(char) rescue nil
    end
  end

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

input_thread = Thread.new { IO.copy_stream(STDIN, master) }

8 月 21 日更新:

所以上面的例子大部分都有效,但由于某些原因,像 CTRL+c 这样的键仍然不能正常工作。我什至查看了 other people's approach 看看我可能做错了什么,实际上它似乎是相同的方法 - 因为 IO.copy_stream(STDIN, master) 成功地将 3 发送给了主人。 None 以下似乎完全有帮助:

master.putc 3
master.putc "\x03"
master.putc "[=13=]3"

在我深入研究尝试用较低级别的语言实现这一点之前,我尝试了另外一件事——块语法。显然块语法神奇地解决了这个问题。

为了防止这个答案变得有点过于冗长,以下似乎可行:

require 'pty'
require 'io/console'

def run
  output = ""

  IO.console.raw!

  input_thread = nil

  PTY.spawn('bash') do |read, write, pid|
    Signal.trap(:WINCH) { write.winsize = STDOUT.winsize }
    input_thread = Thread.new { IO.copy_stream(STDIN, write) }

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

    Process.wait(pid)
  end

  input_thread.kill if input_thread

  IO.console.cooked!
end

Bundler.send(:with_env, Bundler.clean_env) do
  run
end