dcere
dcere

Reputation: 23

How to execute interactive shell program on a remote host from ruby

I am trying to execute an interactive shell program on a remote host from another ruby program. For the sake of simplicity let's suppose that the program I want to execute is something like this:

puts "Give me a number:"
number = gets.chomp()
puts "You gave me #{number}"

The approach that most successful has been so far is using the one I got from here. It is this one:

require 'open3'
Open3.popen3("ssh -tt root@remote 'ruby numbers.rb'") do |stdin, stdout, stderr|
    # stdin  = input stream
    # stdout = output stream
    # stderr = stderr stream
    threads = []
    threads << Thread.new(stderr) do |terr|
        while (line = terr.gets)
          puts "stderr: #{line}"
        end
    end
    threads << Thread.new(stdout) do |terr|
        while (line = terr.gets)
          puts "stdout: #{line}"
        end
    end

    sleep(2)
    puts "Give me an answer: "
    answer = gets.chomp()
    stdin.puts answer


    threads.each{|t| t.join()} #in order to cleanup when you're done.
end

The problem is that this is not "interactive" enough to me, and the program that I would like to execute (not the simple numbers.rb) has a lot more of input / output. You can think of it as an apt-get install that will ask you for some input to solve some problems.
I have read about net::ssh and pty, but couldn't see if they were going to be the (easy/elegant) solution I am looking for.

The ideal solution will be to make it in such a way that the user does not realize that the IO is being done on a remote host: the stdin goes to the remote host stdin, the stdout from the remote host comes to me and I show it.

If you have any ideas I could try I will be happy to hear them. Thank you!

Upvotes: 1

Views: 3613

Answers (1)

dgo.a
dgo.a

Reputation: 2764

Try this:

require "readline"
require 'open3'

Open3.popen3("ssh -tt root@remote 'ruby numbers.rb'") do |i, o, e, th|


  Thread.new {
    while !i.closed? do
      input =Readline.readline("", true).strip 
      i.puts input
    end
  }

  t_err = Thread.new {
    while !e.eof?  do
      putc e.readchar
    end
  }

  t_out = Thread.new {
    while !o.eof?  do
      putc o.readchar
    end
  }

  Process::waitpid(th.pid) rescue nil 
  # "rescue nil" is there in case process already ended.

  t_err.join
  t_out.join

end

I got it working, but don't ask me why it works. It was mainly trial/error.

Alternatives:

  • Using Net::SSH, you need to use :on_process and a Thread: ruby net/ssh channel dies? Don't forget to add session.loop(0.1). More info at the link. The Thread/:on_process idea inspired me to write a gem for my own use: https://github.com/da99/Chee/blob/master/lib/Chee.rb
  • If the last call in your Ruby program is SSH, then you can exec ssh -tt root@remote 'ruby numbers.rb'. But, if you still want interactivity between User<->Ruby<->SSH, then the previous alternative is the best.

Upvotes: 6

Related Questions