Ruby 生成过程,捕获 STDOUT/STDERR,同时表现得好像它是定期生成的
Ruby spawn process, capturing STDOUT/STDERR, while behaving as if it were spawned regularly
我想要达到的目标:
- 从 Ruby 进程生成子进程
- 子进程应正常打印回终端。 "normal",我的意思是该过程不应错过颜色输出,或忽略用户输入 (STDIN)。
- 对于该子流程,捕获 STDOUT/STDERR(联合),例如到一个 String 变量中,子进程死后可以访问它。转义字符和所有。
通过传递不同的 IO 管道可以捕获 STDOUT/STDERR,但是子进程随后可以检测到它不在 tty
中。例如 git log
不会打印影响文本颜色的字符,也不会使用它的分页器。
使用 pty
启动进程本质上是 "tricks" 子进程认为它是由用户启动的。据我所知,这正是我想要的,其结果基本上符合所有要求。
我测试解决方案是否满足我的需求的一般测试是:
- 运行
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
这在大多数情况下都有效,但事实证明它并不完美。出于某种原因,某些应用程序似乎无法切换到 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
我想要达到的目标:
- 从 Ruby 进程生成子进程
- 子进程应正常打印回终端。 "normal",我的意思是该过程不应错过颜色输出,或忽略用户输入 (STDIN)。
- 对于该子流程,捕获 STDOUT/STDERR(联合),例如到一个 String 变量中,子进程死后可以访问它。转义字符和所有。
通过传递不同的 IO 管道可以捕获 STDOUT/STDERR,但是子进程随后可以检测到它不在 tty
中。例如 git log
不会打印影响文本颜色的字符,也不会使用它的分页器。
使用 pty
启动进程本质上是 "tricks" 子进程认为它是由用户启动的。据我所知,这正是我想要的,其结果基本上符合所有要求。
我测试解决方案是否满足我的需求的一般测试是:
- 运行
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
这在大多数情况下都有效,但事实证明它并不完美。出于某种原因,某些应用程序似乎无法切换到 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