lib/ztk/command.rb in ztk-1.6.2 vs lib/ztk/command.rb in ztk-1.6.3

- old
+ new

@@ -1,9 +1,5 @@ -require 'ostruct' -require 'timeout' -require 'socket' - module ZTK # Command Error Class # # @author Zachary Patten <zachary AT jovelabs DOT com> @@ -20,11 +16,23 @@ # cmd = ZTK::Command.new(:ui => ui, :silence => true) # puts cmd.exec("hostname", :silence => false).inspect # # @author Zachary Patten <zachary AT jovelabs DOT com> class Command < ZTK::Base + require 'ostruct' + require 'timeout' + require 'socket' + require 'ztk/command/download' + require 'ztk/command/exec' + require 'ztk/command/private' + require 'ztk/command/upload' + + include ZTK::Command::Download + include ZTK::Command::Exec + include ZTK::Command::Upload + # @param [Hash] configuration Sets the overall default configuration for the # class. For example, all calls to *exec* against this instance will use # the configuration options specified here by default. These options can # be overriden on a per call basis as well. # @option configuration [Integer] :timeout (600) How long in seconds before @@ -44,171 +52,17 @@ :timeout => 600, :ignore_exit_status => false, :exit_code => 0, :silence => false }.merge(configuration)) + config.ui.logger.debug { "config=#{config.send(:table).inspect}" } end - # Execute Command - # - # @example Execute a command: - # cmd = ZTK::Command.new(:silence => true) - # puts cmd.exec("hostname", :silence => false).inspect - # - # @param [String] command The command to execute. - # @param [Hash] options The options hash for executing the command. - # @option options [Integer] :timeout (600) How long in seconds before - # the command will timeout. - # @option options [Boolean] :ignore_exit_status (false) Whether or not - # we should ignore the exit status of the the process we spawn. By - # default we do not ignore the exit status and throw an exception if it is - # non-zero. - # @option options [Integer] :exit_code (0) The exit code we expect the - # process to return. This is ignore if *ignore_exit_status* is true. - # @option options [Boolean] :silence (false) Whether or not we should - # squelch the output of the process. The output will always go to the - # logging device supplied in the ZTK::UI object. The output is always - # available in the return value from the method additionally. - # - # @return [OpenStruct#output] The output of the command, both STDOUT and - # STDERR combined. - # @return [OpenStruct#exit_code] The exit code of the process. - def exec(command, options={}) - options = OpenStruct.new(config.send(:table).merge(options)) - options.ui.logger.debug { "config=#{options.send(:table).inspect}" } - options.ui.logger.debug { "options=#{options.send(:table).inspect}" } - options.ui.logger.info { "command(#{command.inspect})" } - - if options.replace_current_process - options.ui.logger.fatal { "REPLACING CURRENT PROCESS - GOODBYE!" } - Kernel.exec(command) - end - - output = "" - exit_code = -1 - stdout_header = false - stderr_header = false - - parent_stdout_reader, child_stdout_writer = IO.pipe - parent_stderr_reader, child_stderr_writer = IO.pipe - - start_time = Time.now.utc - - pid = Process.fork do - parent_stdout_reader.close - parent_stderr_reader.close - - STDOUT.reopen(child_stdout_writer) - STDERR.reopen(child_stderr_writer) - STDIN.reopen("/dev/null") - - child_stdout_writer.close - child_stderr_writer.close - - Kernel.exec(command) - end - child_stdout_writer.close - child_stderr_writer.close - - reader_writer_key = {parent_stdout_reader => :stdout, parent_stderr_reader => :stderr} - reader_writer_map = {parent_stdout_reader => options.ui.stdout, parent_stderr_reader => options.ui.stderr} - - direct_log(:info) { log_header("COMMAND") } - direct_log(:info) { "#{command.inspect}\n" } - direct_log(:info) { log_header("STARTED") } - - begin - Timeout.timeout(options.timeout) do - loop do - pipes = IO.select(reader_writer_map.keys, [], reader_writer_map.keys).first - pipes.each do |pipe| - data = pipe.read - - if (data.nil? || data.empty?) - sleep(0.1) - next - end - - case reader_writer_key[pipe] - when :stdout then - if !stdout_header - direct_log(:info) { log_header("STDOUT") } - stdout_header = true - stderr_header = false - end - reader_writer_map[pipe].write(data) unless options.silence - direct_log(:info) { data } - - when :stderr then - if !stderr_header - direct_log(:warn) { log_header("STDERR") } - stderr_header = true - stdout_header = false - end - reader_writer_map[pipe].write(data) unless options.silence - direct_log(:warn) { data } - end - - output += data - - options.on_progress.nil? or options.on_progress.call - end - - break if reader_writer_map.keys.all?{ |reader| reader.eof? } - end - end - rescue Timeout::Error => e - direct_log(:fatal) { log_header("TIMEOUT") } - log_and_raise(CommandError, "Process timed out after #{options.timeout} seconds!") - end - - Process.waitpid(pid) - exit_code = $?.exitstatus - direct_log(:info) { log_header("STOPPED") } - - parent_stdout_reader.close - parent_stderr_reader.close - - options.ui.logger.debug { "exit_code(#{exit_code})" } - - if !options.ignore_exit_status && (exit_code != options.exit_code) - log_and_raise(CommandError, "exec(#{command.inspect}, #{options.inspect}) failed! [#{exit_code}]") - end - OpenStruct.new(:command => command, :output => output, :exit_code => exit_code) - end - - # Not Supported - # @raise [CommandError] Not Supported - def upload(*args) - log_and_raise(CommandError, "Not Supported") - end - - # Not Supported - # @raise [CommandError] Not Supported - def download(*args) - log_and_raise(CommandError, "Not Supported") - end - - private - # Returns a string in the format of "user@hostname" for the current - # shell. - def tag - @@hostname ||= Socket.gethostname.split('.').first.strip - "#{ENV['USER']}@#{@@hostname}" - end - - # Formats a header suitable for writing to the direct logger when logging - # sessions. - def log_header(what) - count = 8 - sep = ("=" * count) - header = [sep, "[ #{what} ]", sep, "[ #{tag} ]", sep, "[ #{what} ]", sep].join - "#{header}\n" - end + include ZTK::Command::Private end end