How do I display progress bars from a shell command over ssh - ruby

I've got a script thats supposed to mimic ffmpeg on my local machine, by sending the command of to a remote machine, running it there and then returning the results.
(see previous stackoverflow question.)
#!/usr/bin/env ruby
require 'rubygems'
require 'net/ssh'
require 'net/sftp'
require 'highline/import'
file = ARGV[ ARGV.index( '-i' ) + 1] if ARGV.include?( '-i' )
puts 'No input file specified' unless file;
host = "10.0.0.10"
user = "user"
prod = "new-#{file}" # product filename (call it <file>-new)
rpath = "/home/#{user}/.rffmpeg" # remote computer operating directory
rfile = "#{rpath}/#{file}" # remote filename
rprod = "#{rpath}/#{prod}" # remote product
cmd = "ffmpeg -i #{rfile} #{rprod}"# remote command, constructed
pass = ask("Password: ") { |q| q.echo = false } # password from stdin
Net::SSH.start(host, user ) do |ssh|
ssh.sftp.connect do |sftp|
# upload local 'file' to remote 'rfile'
sftp.upload!(file, rfile)
# run remote command 'cmd' to produce 'rprod'
ssh.exec!(cmd)
# download remote 'rprod' to local 'prod'
sftp.download!(rprod, prod)
end
end
now my problem is at
ssh.exec!(cmd)
I want to display the cmd's output to the local user in real-time. But making it
puts ssh.exec!(cmd)
I only get the resulting output after the command has finished running. How would I have to change the code to make this work?

On the display side of your question, you can generate an updating progress bar in Ruby using the "\r" string char. This backs you up to the beginning of the current line allowing you to re-write it. For example:
1.upto(100) { |i| sleep 0.05; print "\rPercent Complete #{i}%"}
Or if you just want a progress bar across the screen you can simply do something similar to this:
1.upto(50) { sleep 0.05; print "|"}
Also, relating to stdout, in addition to flushing output per previous example (STDOUT.flush), you can ask Ruby to automatically sync writes to an IO buffer (in this case STDOUT) with associated device writes (basically turns off internal buffering):
STDOUT.sync = true
Also, I find that sometimes flush doesn't work for me, and I use "IO.fsync" instead. For me that's mostly been related to file system work, but it's worth knowing.

From ri Net::SSH::start:
-------------------------------------------------------- Net::SSH::start
Net::SSH::start(host, user, options={}, &block) {|connection| ...}
------------------------------------------------------------------------
The standard means of starting a new SSH connection. When used with
a block, the connection will be closed when the block terminates,
otherwise the connection will just be returned. The yielded (or
returned) value will be an instance of
Net::SSH::Connection::Session (q.v.). (See also
Net::SSH::Connection::Channel and Net::SSH::Service::Forward.)
Net::SSH.start("host", "user") do |ssh|
ssh.exec! "cp /some/file /another/location"
hostname = ssh.exec!("hostname")
ssh.open_channel do |ch|
ch.exec "sudo -p 'sudo password: ' ls" do |ch, success|
abort "could not execute sudo ls" unless success
ch.on_data do |ch, data|
print data
if data =~ /sudo password: /
ch.send_data("password\n")
end
end
end
end
ssh.loop
end
So it looks like you can get more interactive by using #open_channel
Here's some example code:
user#server% cat echo.rb
#! /usr/local/bin/ruby
def putsf s
puts s
STDOUT.flush
end
putsf "hello"
5.times do
putsf gets.chomp
end
putsf "goodbye"
And on your local machine:
user#local% cat client.rb
#! /usr/local/bin/ruby
require 'rubygems'
require 'net/ssh'
words = %w{ earn more sessions by sleaving }
index = 0;
Net::SSH.start('server', 'user') do |ssh|
ssh.open_channel do |ch|
ch.exec './echo.rb' do |ch, success|
abort "could not execute ./echo.rb" unless success
ch.on_data do |ch, data|
p [:data, data]
index %= words.size
ch.send_data( words[index] + "\n" )
index += 1
end
end
end
end
user#local% ./client.rb
[:data, "hello\n"]
[:data, "earn\n"]
[:data, "more\n"]
[:data, "sessions\n"]
[:data, "by\n"]
[:data, "sleaving\n"]
[:data, "goodbye\n"]
So you can interact with a running process this way.
It's important that the running process flush its output before requesting input - otherwise, the program might hang as the channel may not have received the unflushed output.

Related

Interactive Ruby script

I'm trying to write a ruby script that logs into a remote server, switches to another user, executes a script and answers questions to that script. Right now, I can log in but it hangs on the execution of the bash script. I'm not sure if I got the prompt part right but it's not getting to that point yet. It "hangs" on the running of the script or it just isn't printing the output of the script to the screen.
Here's what I got for now:
require 'rubygems'
require 'net/ssh'
require 'net/ssh/telnet'
s = Net::SSH::Telnet.new("Host" => "server1", "Username" => "dev", "Password" => "12345", "Prompt" => /[$%#>] \z/n)
puts s.cmd("sudo -s")
puts s.cmd("su - user1")
puts s.cmd("/opt/develop/develop-bin/start.sh")
puts s.waitfor(/Prompt/)
Here is the snippet that works for me. It is taken from the bigger library, so treat it as an example. Fill up the variables command, username, password, servername and sudoer and give a try
class AuthenticationError < StandardError; end
AUTH_METHODS = ['hostbased', 'password', 'keyboard-interactive']
MYPROMPT = SecureRandom.hex # or whatever you want
ret = ""
stderr_data = ""
Net::SSH.start(servername, username, :password => password, :auth_methods => AUTH_METHODS) do |ssh|
ssh.open_channel do |channel|
channel.on_data do |channel, data|
ret += data
raise AuthenticationError, "SUDO ACCESS DENIED" if data.inspect.include?('Sorry, try again.') || data.inspect.include?('not in sudoers')
channel.send_data(password+"\n") if data.inspect.include? MYPROMPT
sleep 0.1
end
channel.on_extended_data do |ch, type, data|
stderr_data+=data
end
channel.request_pty
channel.exec("sudo -p #{MYPROMPT} su - #{sudoer} -c '#{command}'")
channel.wait
end
end
puts ret.gsub(/^"/,'').gsub(/"$/,'')

ruby exit tail and continue to next part of script from keypress

I found this snippet of code I am having trouble trying to find a good way to exit the tail and move on to the next part of the script. Essentially I ant the first part to make a change for the person running the scripts, show log output using the below code, then move on to the next part of the script on user keypress. I cant get out of the tail without CTRL-C.
def do_tail( session, file )
session.open_channel do |channel|
channel.on_data do |ch, data|
puts "[#{file}] -> #{data}"
end
channel.exec "tail -f #{file}"
end
end
Net::SSH.start("server", "user", :keys => ["/user/.ssh/id_dsa"]) do |session|
do_tail session, "/var/log/apache2/error.log"
do_tail session, "/var/log/apache2/access.log"
session.loop
end
UPDATE
The -f takes over I/O and makes it difficult to exit that ssh channel. I decided to move towards the suggestions and modify it. Here is the result in case someone else would like help on this topic.
require 'rubygems'
require 'net/ssh'
def exit?
begin
while input = STDIN.read_nonblock(1)
return true if input == 'q'
end
false
rescue Errno::EINTR
false
rescue Errno::EAGAIN
false
rescue EOFError
true
end
end
def do_tail( session, file )
session.open_channel do |channel|
channel.on_data do |ch, data|
puts "[#{file}]\n\n#{data}"
end
channel.exec "tail -n22 #{file}"
end
end
def loggy
iteration = 0
loop do
iteration = (iteration + 1)
Net::SSH.start("server", "user", :keys => ["/user/.ssh/id_dsa"]) do |session|
do_tail session, "/var/log/apache2/error.log"
end
puts "\n\nType 'q' and <ENTER> to exit log stream when you are done!\n\n"
sleep 5
break if exit? or iteration == 3
end
end
loggy
loop do
puts "\nDo you need to view more of the log? (y/n)\n"
confirm = gets.chomp
if confirm =="y"
loggy
else
end
break if confirm == "n"
end
puts "Part Deaux!"
You've given the -f command line option to tail(1). That explicitly instructs tail(1) to exit when the user types ^C or otherwise kills the program. If you just want a specific amount of the file to be shown and not followed, then you might wish to use the -n command line option instead:
channel.exec "tail -n 24 #{file}"
24 will show roughly one terminal's worth of data, though if your terminals are larger or smaller -- or you're interested in different amounts of data -- then you might wish to tweak it further.
tail(1) is powerful; it'd be worth reading its documentation just in case there's an even better way to do what you're trying to accomplish.
The solution I found was to use less instead of tail. Try this:
less +F filename; echo 'after less'
When you hit ctrl+c, q in less it will quit and then echo what you want.

privlage issues when attempting to run the passenger-status command from within a rubyscript

I wrote a collectd plugin in ruby that is suppose to check passengers status and report back the various metrics. When I test my script on the all works well, but when I attempt to run my script through collectd it fails with the following message.
"ERROR: You are not authorized to query the status for this Phusion Passenger instance. Please try again with 'sudo'."
I then changed my ruby script to use the sudo command for passenger status which resulted in
"exec plugin: exec_read_one: error = sudo: sorry, you must have a tty to run sudo"
I then tried getting collectd to run the script as root but I got the following
"exec plugin: Cowardly refusing to exec program as root."
I am not sure what else I can try. the command that is failing when used by a user other that root is passenger-status
Here is the script
#!/usr/bin/env ruby
require 'getoptlong'
# The name of the collectd plugin, something like apache, memory, mysql, interface, ...
PLUGIN_NAME = 'passenger-status'
def usage
puts("#{$0} -h [-i ]")
exit
end
# Main
begin
# Sync stdout so that it will flush to collectd properly.
$stdout.sync = true
# Parse command line options
hostname = nil
sampling_interval = 20 # sec, Default value
opts = GetoptLong.new(
[ '--hostid', '-h', GetoptLong::REQUIRED_ARGUMENT ],
[ '--sampling-interval', '-i', GetoptLong::OPTIONAL_ARGUMENT ]
)
opts.each do |opt, arg|
case opt
when '--hostid'
hostname = arg
when '--sampling-interval'
sampling_interval = arg.to_i
end
end
usage if !hostname
# Collection loop
while true do
start_run = Time.now.to_i
next_run = start_run + sampling_interval
# collectd data and print the values
data = `passenger-status`
max = data.match(/max (.*)/).to_s.split.last
count = data.match(/count (.*)/).to_s.split.last
active = data.match(/active (.*)/).to_s.split.last
inactive = data.match(/inactive (.*)/).to_s.split.last
waiting = data.match(/Waiting on global queue: ([\d]+)/).to_s.split.last
puts("PUTVAL #{hostname}/#{PLUGIN_NAME}/gauge-max_allowed_connections #{start_run}:#{max}")
puts("PUTVAL #{hostname}/#{PLUGIN_NAME}/gauge-thread_count #{start_run}:#{count}")
puts("PUTVAL #{hostname}/#{PLUGIN_NAME}/gauge-threads_active #{start_run}:#{active}")
puts("PUTVAL #{hostname}/#{PLUGIN_NAME}/gauge-threads_inactive #{start_run}:#{inactive}")
puts("PUTVAL #{hostname}/#{PLUGIN_NAME}/gauge-waiting_in_queue #{start_run}:#{waiting}")
# sleep to make the interval
while((time_left = (next_run - Time.now.to_i)) > 0) do
sleep(time_left)
end
end
end
Looking into the phusion_passenger/admin_tools/server_instance.rb file I was able to determine what file passenger was using to make the determination weather a user would be able to run the passenger-status command or not.
filename = "#{#generation_path}/passenger-status-password.txt"
password = File.open(filename, "rb") do |f|
f.read
end
rescue Errno::EACCES
raise RoleDeniedError
the file it was trying to read was passenger-status-password.txt this file is located in the /tmp/passenger.1.0.19198/generation-1/directory for Passenger version 3.0.9 on CentOS 5.7. I chmod'd the file to 644 and this fixed the issue. this solution also applies if someone wants to run the passenger-status command without sudo.

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!
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.

How to proxy a shell process in ruby

I'm creating a script to wrap jdb (java debugger). I essentially want to wrap this process and proxy the user interaction. So I want it to:
start jdb from my script
send the output of jdb to stdout
pause and wait for input when jdb does
when the user enters commands, pass it to jdb
At the moment I really want a pass thru to jdb. The reason for this is to initialize the process with specific parameters and potentially add more commands in the future.
Update:
Here's the shell of what ended up working for me using expect:
PTY.spawn("jdb -attach 1234") do |read,write,pid|
write.sync = true
while (true) do
read.expect(/\r\r\n> /) do |s|
s = s[0].split(/\r\r\n/)
s.pop # get rid of prompt
s.each { |line| puts line }
print '> '
STDOUT.flush
write.print(STDIN.gets)
end
end
end
Use Open3.popen3(). e.g.:
Open3.popen3("jdb args") { |stdin, stdout, stderr|
# stdin = jdb's input stream
# stdout = jdb's output stream
# stderr = jdb's 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
stdin.puts "blah"
threads.each{|t| t.join()} #in order to cleanup when you're done.
}
I've given you examples for threads, but you of course want to be responsive to what jdb is doing. The above is merely a skeleton for how you open the process and handle communication with it.
The Ruby standard library includes expect, which is designed for just this type of problem. See the documentation for more information.

Resources