lib/rye/box.rb in rye-0.3.2 vs lib/rye/box.rb in rye-0.4
- old
+ new
@@ -31,11 +31,13 @@
attr_accessor :host
attr_accessor :safe
attr_accessor :opts
-
+ # The most recent value from Box.cd or Box.[]
+ attr_reader :current_working_directory
+
# * +host+ The hostname to connect to. The default is localhost.
# * +opts+ a hash of optional arguments.
#
# The +opts+ hash excepts the following keys:
#
@@ -90,20 +92,21 @@
Rye::Cmd.instance_methods
end
alias :commands :can
alias :cmds :can
-
+
+
# Change the current working directory (sort of).
#
# I haven't been able to wrangle Net::SSH to do my bidding.
# "My bidding" in this case, is maintaining an open channel between commands.
- # I'm using Net::SSH::Connection::Session#exec! for all commands
+ # I'm using Net::SSH::Connection::Session#exec for all commands
# which is like a funky helper method that opens a new channel
# each time it's called. This seems to be okay for one-off
# commands but changing the directory only works for the channel
- # it's executed in. The next time exec! is called, there's a
+ # it's executed in. The next time exec is called, there's a
# new channel which is back in the default (home) directory.
#
# Long story short, the work around is to maintain the current
# directory locally and send it with each command.
#
@@ -115,11 +118,10 @@
@current_working_directory = key
self
end
alias :cd :'[]'
-
# Open an SSH session with +@host+. This called automatically
# when you the first comamnd is run if it's not already connected.
# Raises a Rye::NoHost exception if +@host+ is not specified.
def connect
raise Rye::NoHost unless @host
@@ -161,151 +163,165 @@
# See Rye.keys
def keys
Rye.keys
end
+ # Returns +@host+
+ def to_s
+ @host
+ end
- # Takes a command with arguments and returns it in a
- # single String with escaped args and some other stuff.
- #
- # * +cmd+ The shell command name or absolute path.
- # * +args+ an Array of command arguments.
- #
- # The command is searched for in the local PATH (where
- # Rye is running). An exception is raised if it's not
- # found. NOTE: Because this happens locally, you won't
- # want to use this method if the environment is quite
- # different from the remote machine it will be executed
- # on.
- #
- # The command arguments are passed through Escape.shell_command
- # (that means you can't use environment variables or asterisks).
- #
- def Box.prepare_command(cmd, *args)
- args &&= [args].flatten.compact
- cmd = Rye::Box.which(cmd)
- raise CommandNotFound.new(cmd || 'nil') unless cmd
- Rye::Box.escape(@safe, cmd, *args)
+ def inspect
+ %q{#<%s:%s cwd=%s env=%s safe=%s opts=%s>} %
+ [self.class.to_s, self.host,
+ @current_working_directory, (@current_environment_variables || '').inspect,
+ self.safe, self.opts.inspect]
end
- # An all ruby implementation of unix "which" command.
- #
- # * +executable+ the name of the executable
- #
- # Returns the absolute path if found in PATH otherwise nil.
- def Box.which(executable)
- return unless executable.is_a?(String)
- #return executable if File.exists?(executable) # SHOULD WORK, MUST TEST
- shortname = File.basename(executable)
- dir = Rye.sysinfo.paths.select do |path| # dir contains all of the
- next unless File.exists? path # occurrences of shortname
- Dir.new(path).entries.member?(shortname) # found in the paths.
- end
- File.join(dir.first, shortname) unless dir.empty? # Return just the first
+ # Compares itself with the +other+ box. If the hostnames
+ # are the same, this will return true. Otherwise false.
+ def ==(other)
+ @host == other.host
end
- # Execute a local system command (via the shell, not SSH)
- #
- # * +cmd+ the executable path (relative or absolute)
- # * +args+ Array of arguments to be sent to the command. Each element
- # is one argument:. i.e. <tt>['-l', 'some/path']</tt>
+ # Returns the host SSH keys for this box
+ def host_key
+ raise "No host" unless @host
+ Rye.remote_host_keys(@host)
+ end
+
+ # Copy the local public keys (as specified by Rye.keys) to
+ # this box into ~/.ssh/authorized_keys and ~/.ssh/authorized_keys2.
+ # Returns an Array of the private keys files used to generate the public keys.
#
- # NOTE: shell is a bit paranoid so it escapes every argument. This means
- # you can only use literal values. That means no asterisks too.
+ # NOTE: authorize_keys disables safe-mode for this box while it runs.
#
- def Box.shell(cmd, args=[])
- # TODO: allow stdin to be send to cmd
- cmd = Box.prepare_command(cmd, args)
- cmd << " 2>&1" # Redirect STDERR to STDOUT. Works in DOS also.
- handle = IO.popen(cmd, "r")
- output = handle.read.chomp
- handle.close
- output
+ def authorize_keys
+ added_keys = []
+ opts[:safe] = false
+ Rye.keys.each do |key|
+ path = key[2]
+ debug "# Public key for #{path}"
+ k = Rye::Key.from_file(path).public_key.to_ssh2
+ self.mkdir('-p', '~/.ssh') # Silently create dir if it doesn't exist
+ self.echo("'#{k}' >> ~/.ssh/authorized_keys")
+ self.echo("'#{k}' >> ~/.ssh/authorized_keys2")
+ self.chmod('-R', '0600', '.ssh')
+ added_keys << path
+ end
+ opts[:safe] = true
+ added_keys
end
- # Creates a string from +cmd+ and +args+. If +safe+ is true
- # it will send them through Escape.shell_command otherwise
- # it will return them joined by a space character.
- def Box.escape(safe, cmd, *args)
- args = args.flatten.compact || []
- safe ? Escape.shell_command(cmd, *args).to_s : [cmd, args].flatten.compact.join(' ')
+ private
+
+
+ def debug(msg); @debug.puts msg if @debug; end
+ def error(msg); @error.puts msg if @error; end
+
+
+ # Add the current environment variables to the beginning of +cmd+
+ def prepend_env(cmd)
+ return cmd unless @current_environment_variables.is_a?(Hash)
+ env = ''
+ @current_environment_variables.each_pair do |n,v|
+ env << "export #{n}=#{Escape.shell_single_word(v)}; "
+ end
+ [env, cmd].join(' ')
end
- private
-
-
- def debug(msg); @debug.puts msg if @debug; end
- def error(msg); @error.puts msg if @error; end
+ # Execute a command over SSH
+ #
+ # * +args+ is a command name and list of arguments.
+ # The command name is the literal name of the command
+ # that will be executed in the remote shell. The arguments
+ # will be thoroughly escaped and passed to the command.
+ #
+ # rbox = Rye::Box.new
+ # rbox.ls '-l', 'arg1', 'arg2'
+ #
+ # is equivalent to
+ #
+ # $ 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)
+ connect if !@ssh || @ssh.closed?
+ args = args.flatten.compact
+ args = args.first.split(/\s+/) if args.size == 1
+ cmd = args.shift
- # Add the current environment variables to the beginning of +cmd+
- def prepend_env(cmd)
- return cmd unless @current_environment_variables.is_a?(Hash)
- env = ''
- @current_environment_variables.each_pair do |n,v|
- env << "export #{n}=#{Escape.shell_single_word(v)}; "
- end
- [env, cmd].join(' ')
+ # Symbols to switches. :l -> -l, :help -> --help
+ args.collect! do |a|
+ a = "-#{a}" if a.is_a?(Symbol) && a.to_s.size == 1
+ a = "--#{a}" if a.is_a?(Symbol)
+ a
end
+ raise Rye::NotConnected, @host unless @ssh && !@ssh.closed?
- # Execute a command over SSH
- #
- # * +args+ is a command name and list of arguments.
- # The command name is the literal name of the command
- # that will be executed in the remote shell. The arguments
- # will be thoroughly escaped and passed to the command.
- #
- # rbox = Rye::Box.new
- # rbox.ls '-l', 'arg1', 'arg2'
- #
- # is equivalent to
- #
- # $ 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 add_command(*args)
- connect if !@ssh || @ssh.closed?
- args = args.first.split(/\s+/) if args.size == 1
- cmd, args = args.flatten.compact
-
- raise Rye::NotConnected, @host unless @ssh && !@ssh.closed?
-
- cmd_clean = Rye::Box.escape(@safe, cmd, args)
- cmd_clean = prepend_env(cmd_clean)
- if @current_working_directory
- cwd = Rye::Box.escape(@safe, 'cd', @current_working_directory)
- cmd_clean = [cwd, cmd_clean].join('; ')
- end
- debug "Executing: %s" % cmd_clean
- stdout, stderr = net_ssh_exec! cmd_clean
- rap = Rye::Rap.new(self)
- rap.add_stdout(stdout || '')
- rap.add_stderr(stderr || '')
- rap
+ cmd_clean = Rye.escape(@safe, cmd, args)
+ cmd_clean = prepend_env(cmd_clean)
+ if @current_working_directory
+ cwd = Rye.escape(@safe, 'cd', @current_working_directory)
+ cmd_clean = [cwd, cmd_clean].join(' && ')
end
- alias :cmd :add_command
- # Executes +command+ via SSH
- # Returns an Array with two elements, [stdout, stderr], representing
- # the STDOUT and STDERR output by the command. They're Strings.
- def net_ssh_exec!(command)
- block ||= Proc.new do |ch, type, data|
- ch[:stdout] ||= ""
- ch[:stderr] ||= ""
- ch[:stdout] << data if type == :stdout
- ch[:stderr] << data if type == :stderr
+ debug "Executing: %s" % cmd_clean
+ stdout, stderr, ecode, esignal = net_ssh_exec! cmd_clean
+ rap = Rye::Rap.new(self)
+ rap.add_stdout(stdout || '')
+ rap.add_stderr(stderr || '')
+ rap.exit_code = ecode
+ rap.exit_signal = esignal
+ rap.cmd = cmd
+
+ raise Rye::CommandError.new(rap) if ecode > 0
+
+ rap
+ end
+ alias :cmd :run_command
+
+ # Executes +command+ via SSH
+ # Returns an Array with 4 elements: [stdout, stderr, exit code, exit signal]
+ def net_ssh_exec!(command)
+ block ||= Proc.new do |channel, type, data|
+ channel[:stdout] ||= ""
+ channel[:stderr] ||= ""
+ channel[:stdout] << data if type == :stdout
+ channel[:stderr] << data if type == :stderr
+ channel.on_request("exit-status") do |ch, data|
+ # Anything greater than 0 is an error
+ channel[:exit_code] = data.read_long
end
-
- channel = @ssh.exec(command, &block)
- channel.wait # block until we get a response
-
- [channel[:stdout], channel[:stderr]]
+ 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
+ # For long-running commands like top, this will print the output.
+ # It cool, but we'd also need to enable STDIN to interact with
+ # command.
+ #channel.on_data do |ch, data|
+ # puts "got stdout: #{data}"
+ # channel.send_data "something for stdin\n"
+ #end
end
+ channel = @ssh.exec(command, &block)
+ channel.wait # block until we get a response
+ 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
+
+
end
end