How do I get the PTY.spawn child exit code?

后端 未结 3 1021
清酒与你
清酒与你 2021-02-10 02:27

I\'m trying to manage a SSH connection to a network device via the PTY module, with a code similar to this:

cmd_line = \"ssh coltrane@love.supreme.com\"
begin
          


        
3条回答
  •  遥遥无期
    2021-02-10 03:00

    TLDR

    Use 1.9.2 and wait on the PTY process to correctly set $?

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

    Full Story

    On 1.9.2 you can capture the exit status for PTY by calling wait on the PTY pid. This works out almost all the time (AFAIK). The only exceptions I know of are with edge cases like exiting immediately or issuing an empty string for a command (see http://redmine.ruby-lang.org/issues/5253).

    For example:

    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
    

    Now run the test:

    $ 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
    

    On 1.8.7 you need to do a bit more. In older rubies PTY would often exit with PTY::ChildExited errors, even when you wait for the PTY process to finish. As a result, if you run the tests as written you get this:

    $ 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
    

    Notice ALMOST all the tests bomb with a ChildExited error, but one (incidentally the one representing the most realistic use of PTY) succeeds as expected. Surely this erratic behavior is a bug and, as already shown, it has been fixed in 1.9.2.

    There is a partial workaround, however. You can specifically handle the ChildExited errors using something like this:

    def pty(cmd, &block)
      begin
        PTY.spawn(cmd, &block)
        $?.exitstatus
      rescue PTY::ChildExited
        $!.status.exitstatus
      end
    end
    

    Insert that, run the tests again, and you get results consistent with 1.9.2, with the BIG caveat that $? will not be set correctly (unlike 1.9.2). Specifically if you were to add this test:

    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
    

    You get success on 1.9.2 and you get failure on 1.8.7. In the 1.8.7 case the PTY completes via the ChildExited error -- the Process.wait never gets called and thus never sets $?. Instead the $? from the 'system "true"' persists and you get 0 instead of 1 as the exit status.

    The behavior of $? is hard to follow and has more caveats that I won't get into (ie sometimes the PTY will complete via the Process.wait).

提交回复
热议问题