我该如何获取PTY.spawn子进程的退出代码?

6

我正在尝试通过PTY模块管理与网络设备的SSH连接,使用类似于以下代码:

cmd_line = "ssh coltrane@love.supreme.com"
begin
  PTY.spawn(cmd_line) do |r_f,w_f,pid|
  ...
rescue PTY::ChildExited => cended
  ...
end

整个I/O工作得非常好,但是我不知道如何获取子进程的退出状态。
例如,如果连接断开或超时,生成的进程将以错误代码终止,但是这个代码似乎没有在$?特殊变量中返回。

你考虑过使用open4吗?它可以让你轻松获取退出状态。 - Himanshu
请纠正我,但 open() 函数仅打开描述符,而不会生成子进程。如果您有使用 ssh 并使用 open() 的代码示例,我很乐意查看它。 - devlearn
3个回答

17

简述

使用1.9.2版本,并等待PTY进程正确设置$?。

PTY.spawn(command) do |r,w,pid|
  # ...
  Process.wait(pid)
end

完整故事

在1.9.2中,您可以通过在PTY pid上调用wait来捕获PTY的退出状态。这几乎在大多数情况下都有效(据我所知)。我知道的唯一异常情况是像立即退出或发出空字符串命令的边缘情况(请参见http://redmine.ruby-lang.org/issues/5253)。

例如:

require 'pty'
require 'test/unit'

class PTYTest < Test::Unit::TestCase
  def setup
    system "true"
    assert_equal 0, $?.exitstatus
  end

  def pty(cmd, &block)
    PTY.spawn(cmd, &block)
    $?.exitstatus
  end

  def test_pty_with_wait_correctly_sets_exit_status_for_master_slave_io
    status = pty("printf 'abc'; exit 8") do |r,w,pid|
      while !r.eof?
        r.getc
      end
      Process.wait(pid)
    end
    assert_equal 8, status
  end

  def test_pty_with_wait_correctly_sets_exit_status_for_basic_commands
    status = pty("true") do |r,w,pid|
      Process.wait(pid)
    end
    assert_equal 0, status

    status = pty("false") do |r,w,pid|
      Process.wait(pid)
    end
    assert_equal 1, status
  end

  def test_pty_with_wait_sets_exit_status_1_for_immediate_exit
    status = pty("exit 8") do |r,w,pid|
      Process.wait(pid)
    end
    assert_equal 1, status
  end

  def test_pty_with_kill
    status = pty("sleep 10") do |r,w,pid|
      Process.kill(9, pid)
      Process.wait(pid)
    end

    assert_equal nil, status
  end
end

现在运行测试:

$ ruby -v
ruby 1.9.2p290 (2011-07-09 revision 32553) [x86_64-darwin10.8.0]
$ ruby example.rb
Loaded suite example
Started
....
Finished in 1.093349 seconds.

4 tests, 9 assertions, 0 failures, 0 errors, 0 skips

Test run options: --seed 31924

在1.8.7版本中,您需要进行更多操作。在较旧的Ruby版本中,即使您等到PTY进程完成,PTY仍然经常会出现PTY :: ChildExited错误。因此,如果按照编写的方式运行测试,则会出现以下情况:
$ ruby -v
ruby 1.8.7 (2010-08-16 patchlevel 302) [i686-darwin10.4.0]
$ ruby example.rb
Loaded suite example
Started
EE.E
Finished in 1.170357 seconds.

  1) Error:
test_pty_with_kill(PTYTest):
PTY::ChildExited: pty - exited: 35196
    example.rb:11:in `test_pty_with_kill'
    example.rb:11:in `spawn'
    example.rb:11:in `pty'
    example.rb:45:in `test_pty_with_kill'

  2) Error:
test_pty_with_wait_correctly_sets_exit_status_for_basic_commands(PTYTest):
PTY::ChildExited: pty - exited: 35198
    example.rb:11:in `test_pty_with_wait_correctly_sets_exit_status_for_basic_commands'
    example.rb:11:in `spawn'
    example.rb:11:in `pty'
    example.rb:26:in `test_pty_with_wait_correctly_sets_exit_status_for_basic_commands'

  3) Error:
test_pty_with_wait_sets_exit_status_1_for_immediate_exit(PTYTest):
PTY::ChildExited: pty - exited: 35202
    example.rb:11:in `test_pty_with_wait_sets_exit_status_1_for_immediate_exit'
    example.rb:11:in `spawn'
    example.rb:11:in `pty'
    example.rb:38:in `test_pty_with_wait_sets_exit_status_1_for_immediate_exit'

4 tests, 5 assertions, 0 failures, 3 errors

几乎所有测试都会报错ChildExited,只有一个测试(恰好是最真实使用PTY的测试)能如预期一样成功。这种不稳定的行为肯定是个bug,并且已经在1.9.2版本中修复了,正如前面所示。
但是,仍然有一种部分解决方案。您可以使用以下类似代码来专门处理ChildExited错误:
def pty(cmd, &block)
  begin
    PTY.spawn(cmd, &block)
    $?.exitstatus
  rescue PTY::ChildExited
    $!.status.exitstatus
  end
end

插入该代码,再次运行测试,您将得到与1.9.2一致的结果,但有一个重要的警告,即$?无法正确设置(不像1.9.2)。特别是,如果您添加以下测试:

def test_setting_of_process_status
  system "true"
  assert_equal 0, $?.exitstatus

  begin
    PTY.spawn("false") do |r,w,pid|
      Process.wait(pid)
    end
  rescue PTY::ChildExited
  end
  assert_equal 1, $?.exitstatus
end

在1.9.2中成功,在1.8.7中失败。在1.8.7的情况下,通过ChildExited错误完成PTY -- Process.wait从未被调用,因此永远不会设置$?。相反,“system“true””中的$?持续存在,你将得到0而不是1作为退出状态。

$?的行为很难跟踪,并且有更多的警告(例如,有时PTY确实会通过Process.wait完成)。


1

好的,这里有一些可能的解决方案:

  • 使用 Ruby 1.9.2PTY.check() 方法

  • 将命令行包装在脚本中

不幸的是,我不能使用最新版本的 Ruby,所以我使用了包装器解决方案,在包装器脚本的末尾将 $? 回显到文件中。当生成的子进程退出时读取退出码。

当然,如果某些东西中断了包装器脚本本身的执行,那么我们将永远无法得到结果文件...

但至少这个解决方法可以用于 Ruby 1.8.7/1.9.1 版本。


-1
简而言之:在Linux中,父进程应该等待子进程退出状态以了解其子进程的退出状态。
C代码:
int status; wait(&status) // 在父进程代码部分 WEXITSTATUS(status) // 宏返回返回子进程的退出码
很抱歉,我没有使用Ruby的经验,无法为您提供一些代码。 最好的祝愿 :)

1
我不是在寻找关于Unix进程的“抽象”指南,而是想要找到一种使用Ruby PTY模块的解决方法。你的回答毫无用处。 - devlearn
下次请您提出更具体的问题:如果您有任何关于如何获取生成的进程返回代码的想法,我会非常感激。希望您能找到答案。 - Aboelnour
抱歉,如果我的问题没有表达清楚,这确实是一个Ruby问题,而不是一般的Unix问题。也许我不应该将它标记为io和Linux... - devlearn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接