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 [email protected]"
begin
PTY.spawn(cmd_line) do |r_f,w_f,pid|
...
rescue PTY::ChildExited => cended
...
end
The whole I/O works pretty well, however I don't know how to get the exit status of the child process.
For instance, if the connection is broken or simply times out, the spawned process will terminate with an error code, but this code does not seem to be returned in the $?
special variable.
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).
Ok, here are some possible solutions for this problem :
use ruby 1.9.2 PTY.check() method
wrap the command line in a script
Unfortunately I can't use the latest version of ruby as so I used the wrapper solution, that echoes $? to a file at the end of the wrapper script. The exit code is read when the spawned child exits.
Of course if something interrupts the execution of the wrapper script itself, then we'll never get the result file ...
But at least this workaround can be used for 1.8.7/1.9.1 versions of Ruby
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With