lib/rye/box.rb in rye-0.8.19 vs lib/rye/box.rb in rye-0.9.0

- old
+ new

@@ -1,8 +1,10 @@ +require 'annoy' +require 'readline' - module Rye + DEBUG = false unless defined?(Rye::DEBUG) # = Rye::Box # # The Rye::Box class represents a machine. All system # commands are made through this class. @@ -27,16 +29,22 @@ # Net::SSH channel is waiting on_extended_data (a prompt). #++ class Box include Rye::Cmd + attr_accessor :rye_shell + attr_accessor :rye_pty + def host; @rye_host; end def opts; @rye_opts; end def safe; @rye_safe; end def user; @rye_user; end def root?; user.to_s == "root" end + def templates; @rye_templates; end + def templates?; !@rye_templates.nil?; end + def enable_sudo; @rye_sudo = true; end def disable_sudo; @rye_sudo = false; end def sudo?; @rye_sudo == true end # Returns the current value of the stash +@rye_stash+ @@ -62,17 +70,18 @@ def current_working_directory; @rye_current_working_directory; end # The most recent valud for umask (or 0022) def current_umask; @rye_current_umask; end - def info; @rye_info; end - def debug; @rye_debug; end - def error; @rye_error; end + def info?; !@rye_info.nil?; end + def debug?; !@rye_debug.nil?; end + def error?; !@rye_error.nil?; end def ostype=(val); @rye_ostype = val; end def impltype=(val); @rye_impltype = val; end def pre_command_hook=(val); @rye_pre_command_hook = val; end + def stdout_hook=(val); @rye_stdout_hook = val; end def post_command_hook=(val); @rye_post_command_hook = val; end # A Hash. The keys are exception classes, the values are Procs to execute def exception_hook=(val); @rye_exception_hook = val; end # * +host+ The hostname to connect to. Default: localhost. @@ -87,10 +96,11 @@ # * :info => an IO object to print Rye::Box command info to. Default: nil # * :debug => an IO object to print Rye::Box debugging info to. Default: nil # * :error => an IO object to print Rye::Box errors to. Default: STDERR # * :getenv => pre-fetch +host+ environment variables? (default: true) # * :password => the user's password (ignored if there's a valid private key) + # * :templates => the template engine to use for uploaded files. One of: :erb (default) # * :sudo => Run all commands via sudo (default: false) # # NOTE: +opts+ can also contain any parameter supported by # Net::SSH.start that is not already mentioned above. # @@ -107,15 +117,16 @@ # These opts are use by Rye::Box and also passed to Net::SSH @rye_opts = { :safe => true, :port => ssh_opts[:port], - :keys => [], + :keys => Rye.keys, :info => nil, :debug => nil, :error => STDERR, :getenv => true, + :templates => :erb, :quiet => false }.merge(opts) # Close the SSH session before Ruby exits. This will do nothing # if disconnect has already been called explicitly. @@ -126,20 +137,28 @@ @rye_safe, @rye_debug = @rye_opts.delete(:safe), @rye_opts.delete(:debug) @rye_info, @rye_error = @rye_opts.delete(:info), @rye_opts.delete(:error) @rye_getenv = {} if @rye_opts.delete(:getenv) # Enable getenv with a hash @rye_ostype, @rye_impltype = @rye_opts.delete(:ostype), @rye_opts.delete(:impltype) @rye_quiet, @rye_sudo = @rye_opts.delete(:quiet), @rye_opts.delete(:sudo) + @rye_templates = @rye_opts.delete(:templates) - # Just in case someone sends a true value rather than IO object - @rye_debug = STDERR if @rye_debug == true - @rye_error = STDERR if @rye_error == true - @rye_info = STDOUT if @rye_info == true + # Store the state of the terminal + @rye_stty_save = `stty -g`.chomp rescue nil + unless @rye_templates.nil? + require @rye_templates.to_s # should be :erb + end + @rye_opts[:logger] = Logger.new(@rye_debug) if @rye_debug # Enable Net::SSH debugging @rye_opts[:paranoid] = true unless @rye_safe == false # See Net::SSH.start @rye_opts[:keys] = [@rye_opts[:keys]].flatten.compact + # Just in case someone sends a true value rather than IO object + @rye_debug = STDERR if @rye_debug == true || DEBUG + @rye_error = STDERR if @rye_error == true + @rye_info = STDOUT if @rye_info == true + # Add the given private keys to the keychain that will be used for @rye_host add_keys(@rye_opts[:keys]) # We don't want Net::SSH to handle the keypairs. This may change # but for we're letting ssh-agent do it. @@ -221,16 +240,27 @@ @rye_user = newuser disconnect end - # Open an interactive SSH session. This only works if STDIN.tty? - # returns true. Otherwise it returns the SSH command that would - # have been run. This requires the SSH command-line executable (ssh). - # * +run+ when set to false, it will return the SSH command as a String - # and not open an SSH session. + # If STDIN.tty? is true (i.e. if we're connected to a terminal + # with a human at the helm), this will open an SSH connection + # via the regular SSH command (via a call to system). This + # requires the SSH command-line executable (ssh). # + # If STDIN.tty? is false or +run+ is false, this will return + # the SSH command (a String) that would have been run. + # + # NOTE: As of Rye 0.9 you can run interactive sessions with + # rye by calling any shell method without arguments. + # + # e.g. + # + # rbox = Rye::Box.new 'somemachine' + # rbox.bash + # + # TODO: refactor to use net_ssh_exec! in 0.9 def interactive_ssh(run=true) debug "interactive_ssh with keys: #{Rye.keys.inspect}" run = false unless STDIN.tty? cmd = Rye.prepare_command("ssh", "#{@rye_user}@#{@rye_host}") return cmd unless run @@ -247,15 +277,15 @@ return @rye_opts[:keys] end additional_keys = [additional_keys].flatten.compact || [] return if additional_keys.empty? ret = Rye.add_keys(additional_keys) - if ret.is_a?(Rye::Rap) - debug "ssh-add exit_code: #{ret.exit_code}" - debug "ssh-add stdout: #{ret.stdout}" - debug "ssh-add stderr: #{ret.stderr}" - end + #if ret.is_a?(Rye::Rap) + # debug "ssh-add exit_status: #{ret.exit_status}" + # debug "ssh-add stdout: #{ret.stdout}" + # debug "ssh-add stderr: #{ret.stderr}" + #end self # MUST RETURN self end alias :add_key :add_keys # Return the value of uname in lowercase @@ -377,114 +407,10 @@ end @rye_guessed_homes[this_user] = "#{user_defaults['HOME']}/#{this_user}" end - # Copy the local public keys (as specified by Rye.keys) to - # this box into ~/.ssh/authorized_keys and ~/.ssh/authorized_keys2. - # Returns a Rye::Rap object. The private keys files used to generate - # the public keys are contained in stdout. - # Raises a Rye::ComandError if the home directory doesn't exit. - # NOTE: authorize_keys_remote disables safe-mode for this box while it runs - # which will hit you funky style if your using a single instance - # of Rye::Box in a multithreaded situation. - # - def authorize_keys_remote(other_user=nil) - if other_user.nil? - this_user = opts[:user] - homedir = self.quietly { pwd }.first - else - this_user = other_user - # The homedir path is important b/c this is where we're going to - # look for the .ssh directory. That's where auth love is stored. - homedir = self.guess_user_home(this_user) - end - - added_keys = [] - rap = Rye::Rap.new(self) - - prevdir = self.current_working_directory - - unless self.file_exists?(homedir) - rap.add_exit_code(1) - rap.add_stderr("Path does not exist: #{homedir}") - raise Rye::CommandError.new(rap) - end - - # Let's go into the user's home directory that we now know exists. - self.cd homedir - - files = ['.ssh/authorized_keys', '.ssh/authorized_keys2', '.ssh/identity'] - files.each do |akey_path| - if self.file_exists?(akey_path) - # TODO: Make Rye::Cmd.incremental_backup - self.cp(akey_path, "#{akey_path}-previous") - authorized_keys = self.file_download("#{homedir}/#{akey_path}") - end - authorized_keys ||= StringIO.new - - Rye.keys.each do |path| - - info "# Adding public key for #{path}" - k = Rye::Key.from_file(path).public_key.to_ssh2 - authorized_keys.puts k - end - - # Remove duplicate authorized keys - authorized_keys.rewind - uniqlines = authorized_keys.readlines.uniq.join - authorized_keys = StringIO.new(uniqlines) - # We need to rewind so that all of the StringIO object is uploaded - authorized_keys.rewind - - full_path = "#{homedir}/#{akey_path}" - temp_path = "/tmp/rye-#{user}-#{File.basename(akey_path)}" - - sudo do - mkdir :p, :m, '700', File.dirname(akey_path) - file_upload authorized_keys, temp_path - mv temp_path, full_path - chmod '0600', akey_path - chown :R, this_user.to_s, File.dirname(akey_path) - end - end - - # And let's return to the directory we came from. - self.cd prevdir - - rap.add_exit_code(0) - rap - end - require 'fileutils' - # Authorize the current user to login to the local machine via - # SSH without a password. This is the same functionality as - # authorize_keys_remote except run with local shell commands. - def authorize_keys_local - added_keys = [] - ssh_dir = File.join(Rye.sysinfo.home, '.ssh') - Rye.keys.each do |path| - debug "# Public key for #{path}" - k = Rye::Key.from_file(path).public_key.to_ssh2 - FileUtils.mkdir ssh_dir unless File.exists? ssh_dir - - authkeys_file = File.join(ssh_dir, 'authorized_keys') - - debug "Writing to #{authkeys_file}" - File.open(authkeys_file, 'a') {|f| f.write("#{$/}#{k}") } - File.open("#{authkeys_file}2", 'a') {|f| f.write("#{$/}#{k}") } - - unless Rye.sysinfo.os == :windows - Rye.shell(:chmod, '700', ssh_dir) - Rye.shell(:chmod, '0600', authkeys_file) - Rye.shell(:chmod, '0600', "#{authkeys_file}2") - end - - added_keys << path - end - added_keys - end - # A handler for undefined commands. # Raises Rye::CommandNotFound exception. def method_missing(cmd, *args, &block) if @rye_safe ex = Rye::CommandNotFound.new(cmd.to_s) @@ -516,10 +442,21 @@ def pre_command_hook(&block) @rye_pre_command_hook = block if block @rye_pre_command_hook end + # Supply a block to be called every time a command receives STDOUT data. + # + # e.g. + # rbox.stdout_hook do |content| + # ... + # end + def stdout_hook(&block) + @rye_stdout_hook = block if block + @rye_stdout_hook + end + # Supply a block to be called whenever there's an Exception. It's called # with 1 argument: the exception class. If the exception block returns # :retry, the command will be executed again. # # e.g. @@ -557,11 +494,11 @@ # end # # Returns the return value of the block. # def batch(*args, &block) - self.instance_exec *args, &block + self.instance_exec(*args, &block) end # Like batch, except it disables safe mode before executing the block. # After executing the block, safe mode is returned back to whichever # state it was previously in. In other words, this method won't enable @@ -571,10 +508,11 @@ disable_safe_mode ret = self.instance_exec *args, &block @rye_safe = previous_state ret end + alias_method :wildly, :unsafely # See unsafely (except in reverse) def safely(*args, &block) previous_state = @rye_safe enable_safe_mode @@ -605,11 +543,11 @@ # # If no block is specified then sudo is called just like a regular # command. def sudo(*args, &block) if block.nil? - __allow('sudo', args); + run_command('sudo', args); else previous_state = @rye_sudo enable_sudo ret = self.instance_exec *args, &block @rye_sudo = previous_state @@ -701,19 +639,23 @@ # Close the SSH session with +@rye_host+. This is called # automatically at exit if the connection is open. def disconnect return unless @rye_ssh && !@rye_ssh.closed? begin - Timeout::timeout(10) do - @rye_ssh.loop(0.3) { @rye_ssh.busy?; } + if @rye_ssh.busy?; + info "Is something still running? (ctrl-C to exit)" + Timeout::timeout(10) do + @rye_ssh.loop(0.3) { @rye_ssh.busy?; } + end end + debug "Closing connection to #{@rye_ssh.host}" + @rye_ssh.close rescue SystemCallError, Timeout::Error => ex - error "Disconnect timeout (was something still running?)" + error "Disconnect timeout" + rescue Interrupt + debug "Exiting..." end - - debug "Closing connection to #{@rye_ssh.host}" - @rye_ssh.close end private @@ -749,15 +691,17 @@ # $ ls -l 'arg1' 'arg2' # # This method will try to connect to the host automatically # but if it fails it will raise a Rye::NotConnected exception. # - def run_command(*args) - debug "run_command with keys: #{Rye.keys.inspect}" + def run_command(*args, &blk) + debug "run_command" cmd, args = prep_args(*args) + #p [:run_command, cmd, blk.nil?] + connect if !@rye_ssh || @rye_ssh.closed? raise Rye::NotConnected, @rye_host unless @rye_ssh && !@rye_ssh.closed? cmd_clean = Rye.escape(@rye_safe, cmd, args) @@ -767,11 +711,11 @@ # Add the current working directory before the command if supplied. # The command will otherwise run in the user's home directory. if @rye_current_working_directory cwd = Rye.escape(@rye_safe, 'cd', @rye_current_working_directory) - cmd_internal = [cwd, cmd_internal].join(' && ') + cmd_internal = '(%s; %s)' % [cwd, cmd_internal] end # ditto (same explanation as cwd) if @rye_current_umask cwd = Rye.escape(@rye_safe, 'umask', @rye_current_umask) @@ -795,26 +739,28 @@ end rap = Rye::Rap.new(self) rap.cmd = cmd_clean - stdout, stderr, ecode, esignal = net_ssh_exec!(cmd_internal) + channel = net_ssh_exec!(cmd_internal, &blk) - rap.add_stdout(stdout || '') - rap.add_stderr(stderr || '') - rap.add_exit_code(ecode) - rap.exit_signal = esignal + if channel[:exception] + rap = channel[:exception].rap + else + rap.add_stdout(channel[:stdout].read || '') + rap.add_stderr(channel[:stderr].read || '') + rap.add_exit_status(channel[:exit_status]) + rap.exit_signal = channel[:exit_signal] + end - #info "stdout: #{rap.stdout}" - #info "stderr: #{rap.stderr}" - #info "exit_code: #{rap.exit_code}" + debug "RESULT: %s " % [rap.inspect] # It seems a convention for various commands to return -1 - # when something only mildly concerning happens. ls even - # returns -1 for apparently no reason sometimes. In any - # case, the real errors are the ones greater than zero - raise Rye::CommandError.new(rap) if ecode != 0 + # when something only mildly concerning happens. (ls even + # returns -1 for apparently no reason sometimes). Anyway, + # the real errors are the ones that are greater than zero. + raise Rye::Err.new(rap) if rap.exit_status != 0 rescue Exception => ex return rap if @rye_quiet choice = nil @rye_exception_hook.each_pair do |klass,act| @@ -824,11 +770,18 @@ end if choice == :retry retry elsif choice == :skip # do nothing - else + elsif choice == :interactive && !@rye_shell + @rye_shell = true + previous_state = @rye_sudo + disable_sudo + bash + @rye_sudo = previous_state + @rye_shell = false + elsif !ex.is_a?(Interrupt) raise ex, ex.message end end if !@rye_quiet && @rye_post_command_hook.is_a?(Proc) @@ -856,92 +809,237 @@ a end [cmd, args] end - # Executes +command+ via SSH - # Returns an Array with 4 elements: [stdout, stderr, exit code, exit signal] - # NOTE: This method needs to be replaced to fully support interactive - # commands. This implementation is weird because it's getting just STDOUT and - # STDERR responses (check value of "type"). on_data and on_extended_data method - # hooks are not used. See the following threads for implementation ideas: - # - # http://www.ruby-forum.com/topic/141814 - # http://www.ruby-forum.com/topic/169997 - # - def net_ssh_exec!(command) + def net_ssh_exec!(cmd, &blk) + debug ":net_ssh_exec #{cmd} (has blk: #{!blk.nil?}; pty: #{@rye_pty}; shell: #{@rye_shell})" + + pty_opts = { :term => "xterm", + :chars_wide => 80, + :chars_high => 24, + :pixels_wide => 640, + :pixels_high => 480, + :modes => {} } + + channel = @rye_ssh.open_channel do |channel| + if self.rye_shell && blk.nil? + channel.request_pty(pty_opts) do |ch,success| + self.rye_pty = success + raise Rye::NoPty if !success + end + end + channel.exec(cmd, &create_channel) + channel[:state] = :start_session + channel[:block] = blk + end + + @rye_channels ||= [] + @rye_channels << channel + + @rye_ssh.loop(0.1) do + break if channel.nil? || !channel.active? + !channel.eof? # otherwise keep returning true + end + + channel + end + + + def state_wait_for_command(channel) + debug :wait_for_command + end - block ||= Proc.new do |channel, type, data, tt| + def state_start_session(channel) + debug "#{:start_session} [blk: #{!channel[:block].nil?}] [pty: #{@rye_pty}] [shell: #{@rye_shell}]" + channel[:state] = nil + channel[:state] = :run_block if channel[:block] + channel[:state] = :await_response if @rye_pty + end + + def state_await_response(channel) + debug :await_response + @await_response_counter ||= 0 + if channel[:stdout].available > 0 || channel[:stderr].available > 0 + channel[:state] = :read_response + elsif @await_response_counter > 50 + @await_response_counter = 0 + channel[:state] = :await_input + end + @await_response_counter += 1 + end + + def state_read_response(channel) + debug :read_response + if channel[:stdout].available > 0 || channel[:stderr].available > 0 - channel[:stdout] ||= "" - channel[:stderr] ||= "" + stdout = channel[:stdout].read if channel[:stdout].available > 0 + stderr = channel[:stderr].read if channel[:stderr].available > 0 + print stdout if stdout + print stderr if stderr - if type == :stderr - # NOTE: Use sudo to test this since it prompts for a passwords. - # Use sudo -K to kill the user's timestamp (ask for a password every time) - if data =~ /password:/i - ret = Annoy.get_user_input("Password: ", '*') - raise Rye::NoPassword if ret.nil? - channel.send_data "#{ret}\n" - else - channel[:stderr] << data + if channel[:stack].empty? + channel[:state] = :await_input + elsif channel[:stdout].available > 0 || channel[:stderr].available > 0 + channel[:state] = :read_response + else + channel[:state] = :send_data + end + else + channel[:state] = :await_response + end + + end + + def state_send_data(channel) + debug :send_data + cmd = channel[:stack].shift + debug "sending #{cmd.inspect}" + channel[:state] = :await_response + channel.send_data("#{cmd}\n") unless channel.eof? + end + + def state_await_input(channel) + debug :await_input + if channel[:stdout].available > 0 + channel[:state] = :read_response + else + ret = nil + if channel[:prompt] && (channel[:prompt] =~ /pass/i) + ret = Annoy.get_user_input("#{channel[:prompt]} ", echo='*', period=30) + channel[:prompt] = nil end + begin + list = self.commands.sort - # If someone tries to open an interactive ssh session - # through a regular Rye::Box command, Net::SSH will - # return the following error and appear to hang. We - # catch it and raise the appropriate exception. - raise Rye::NoPty if data =~ /Pseudo-terminal will not/ - elsif type == :stdout - if data =~ /Select gem to uninstall/i - puts data - ret = Annoy.get_user_input('') - raise "No input given" if ret.nil? - channel.send_data "#{ret}\n" - else - channel[:stdout] << data + comp = proc { |s| + # TODO: Something here for files + list.grep( /^#{Regexp.escape(s)}/ ) + } + + Readline.completion_append_character = " " + Readline.completion_proc = comp + + ret = Readline.readline(channel[:prompt] || '', true) + #ret = STDIN.gets + + if ret.nil? + channel[:state] = :exit + else + channel[:stack] << ret.chomp + channel[:state] = :send_data + end + rescue Interrupt => e + channel[:state] = :exit end + channel[:prompt] = nil end - + end + + def state_ignore_response(channel) + debug :ignore_response + @ignore_response_counter ||= 0 + if channel[:stdout].available > 0 + @await_response_counter = 0 + channel[:stdout].read + channel[:state] = :process + elsif @ignore_response_counter > 2 + @await_response_counter = 0 + channel[:state] = :process end - - channel = @rye_ssh.exec(command, &block) - - channel.on_request("exit-status") do |ch, data| - # Anything greater than 0 is an error - channel[:exit_code] = data.read_long + @ignore_response_counter += 1 + end + + def state_exit(channel) + debug :exit_state + channel[:state] = nil + if rye_shell && (!channel.eof? || !channel.closing?) + puts + channel.send_data("exit\n") + else + channel.eof! end - channel.on_request("exit-signal") do |ch, data| - # This should be the POSIX SIGNAL that ended the process - channel[:exit_signal] = data.read_long + end + + # TODO: implement callback in create_channel Proc + ##def state_handle_error(channel) + ## debug :handle_error + ## channel[:state] = nil + ## if rye_shell && (!channel.eof? || !channel.closing?) + ## puts + ## channel.send_data("exit\n") + ## else + ## channel.eof! + ## end + ##end + + + def state_run_block(channel) + debug :run_block + channel[:state] = nil + blk = channel[:block] + channel[:block] = nil + begin + instance_eval &blk + rescue => ex + channel[:exception] = ex end - - channel.wait # block until we get a response - channel.request_pty do |ch, success| - raise Rye::NoPty if !success + channel[:state] = :exit + end + + def create_channel() + Proc.new do |channel,success| + channel[:stdout ] = Net::SSH::Buffer.new + channel[:stderr ] = Net::SSH::Buffer.new + channel[:stack] ||= [] + channel.on_close { |ch| + channel[:handler] = ":on_close" + } + channel.on_data { |ch, data| + channel[:handler] = ":on_data" + @rye_stdout_hook.call(data) if !@rye_pty && !@rye_quiet && @rye_stdout_hook.kind_of?(Proc) + if rye_pty && data =~ /password/i + channel[:prompt] = data + channel[:state] = :await_input + else + channel[:stdout].append(data) + end + } + channel.on_extended_data { |ch, type, data| + channel[:handler] = ":on_extended_data" + if rye_pty && data =~ /\Apassword/i + channel[:prompt] = data + channel[:state] = :await_input + else + channel[:stderr].append(data) + end + } + channel.on_request("exit-status") { |ch, data| + channel[:handler] = ":on_request (exit-status)" + channel[:exit_status] = data.read_long + } + channel.on_request("exit-signal") do |ch, data| + channel[:handler] = ":on_request (exit-signal)" + # This should be the POSIX SIGNAL that ended the process + channel[:exit_signal] = data.read_long + end + channel.on_process { + channel[:handler] = :on_process + print channel[:stderr].read if channel[:stderr].available > 0 + begin + send("state_#{channel[:state]}", channel) unless channel[:state].nil? + rescue Interrupt + debug :on_process_interrupt + channel[:state] = :exit + end + } end - - ## I'm getting weird behavior with exit codes. Sometimes - ## a command which usually returns an exit code will not - ## return one the next time it's run. The following crap - ## was from the debugging. - ##Kernel.sleep 5 - ###channel.close - #channel.eof! - ##p [:active, channel.active?] - ##p [:closing, channel.closing?] - ##p [:eof, channel.eof?] - - channel[:exit_code] ||= 0 - channel[:exit_code] &&= channel[:exit_code].to_i - channel[:stderr].gsub!(/bash: line \d+:\s+/, '') if channel[:stderr] - - [channel[:stdout], channel[:stderr], channel[:exit_code], channel[:exit_signal]] end + # * +direction+ is one of :upload, :download # * +recursive+ should be true for directories and false for files. # * +files+ is an Array of file paths, the content is direction specific. # For downloads, +files+ is a list of files to download. The last element # must be the local directory to download to. If downloading a single file @@ -976,10 +1074,11 @@ else target = files.pop # The last path is the download target. end elsif direction == :upload +# p :UPLOAD, @rye_templates raise "Cannot upload to a StringIO object" if target.is_a?(StringIO) if files.size == 1 target = self.getenv['HOME'] || guess_user_home debug "Assuming upload to #{target}" else @@ -987,11 +1086,13 @@ end # Expand fileglobs (e.g. path/*.rb becomes [path/1.rb, path/2.rb]). # This should happen after checking files.size to determine the target unless @rye_safe - files.collect! { |file| Dir.glob File.expand_path(file) } + files.collect! { |file| + file.is_a?(StringIO) ? file : Dir.glob(File.expand_path(file)) + } files.flatten! end end # Fail early. We check whether the StringIO object is available to read @@ -1002,11 +1103,10 @@ # If a StringIO object is at end of file, SCP will hang. (TODO: SCP) file.rewind if file.eof? end end - info "#{direction.to_s} to: #{target}" debug "FILES: " << files.join(', ') # Make sure the target directory exists. We can do this only when # there's more than one file because "target" could be a file name if files.size > 1 && !target.is_a?(StringIO) @@ -1018,25 +1118,19 @@ transfers = [] prev = "" files.each do |file| debug file.to_s prev = "" - unless @rye_quiet - tmp_file = file.is_a?(StringIO) ? '<string>' : file - tmp_target = target.is_a?(StringIO) ? '<string>' : target - STDOUT.puts "[#{direction}] #{tmp_file} #{tmp_target}" - end + line = nil transfers << scp.send(direction, file, target, :recursive => recursive) do |ch, n, s, t| line = "%-50s %6d/%-6d bytes" % [n, s, t] spaces = (prev.size > line.size) ? ' '*(prev.size - line.size) : '' - pinfo "%s %s \r" % [line, spaces] # update line: "file: sent/total" + pinfo "[%s] %s %s %s" % [direction, line, spaces, s == t ? "\n" : "\r"] # update line: "file: sent/total" @rye_info.flush if @rye_info # make sure every line is printed prev = line end end transfers.each { |t| t.wait } # Run file transfers in parallel - pinfo (' '*prev.size) << "\r" - info $/ end target.is_a?(StringIO) ? target : nil end