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
Use 1.9.2 and wait on the PTY process to correctly set $?
PTY.spawn(command) do |r,w,pid|
# ...
Process.wait(pid)
end
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).