# encoding: utf-8 # Classes concerning command execution module CommandExec # Run commands class Command # @!attribute [rw] log_file # Set/Get log file for command # # @!attribute [rw] options # Set/Get options for the command # # @!attribute [rw] parameter # Set/Get parameter for the command attr_accessor :log_file, :options , :parameter # @!attribute [r] result # Return the result of command execution # # @!attribute [r] path # Return path to the executable of the command # # @!attribute [r] working_directory # Return the working directory of the command attr_reader :result, :path, :working_directory # Create a new command to execute # # @param [Symbol] name # name of command # # @param [optional,Hash] opts # options for the command # # @option opts [String] :options ('') # options for the command # # @option opts [String] :working_directory (current working directory) # working_directory for the command # # @option opts [String] :log_file ('') # log file of the command # # @option opts [String,Array] :search_paths ($PATH) # where to search for the command (please mind the 's' at the end. # # @option opts [String,Array] :error_detection_on (:return_code) # what information should be considered for error detection, # available options are :return_code, :stderr, :stdout, :log_file. # You can use one or more of them. # # @option opts [Hash] :error_indicators # what keywords etc. should be considered as errors. # # You can define allowed or forbidden keywords or exit codes. # To search for errors in a log file you need to provide one. # # For each option you can provide a single word or an Array of words. # # ``` # :allowed_return_code => [0], # :forbidden_return_code => [], # :allowed_words_in_stderr => [], # :forbidden_words_in_stderr => [], # :allowed_words_in_stdout => [], # :forbidden_words_in_stdout => [], # :allowed_words_in_log_file => [], # :forbidden_words_in_log_file => [], # ``` # # @option opts [Symbol] :on_error_do # Oh, an error happend, what to do next? Raise an error (:raise_error), # Throw an error (:throw_error) or do nothing at all (:nothing, default). # # @option opts [Symbol] :run_via # Which runner should be used to execute the command: :open3 (default) # or :system. # # @option opts [Logger] :lib_logger # The logger which is used to output information generated by the # library. The logger which is provided needs to be compatible with api # of the Ruby `Logger`-class. # # @option opts [Symbol] :lib_log_level # What information should handled by the logger: # :debug, :info, :warn, :error, :fatal, :unknown. Additionally the # :silent-option is understood: do not output anything (@see README for # further information). def initialize(name,opts={}) @name = name @opts = { :options => '', :parameter => '', :working_directory => Dir.pwd, :log_file => '', :search_paths => ENV['PATH'].split(':'), :error_detection_on => [:return_code], :error_indicators => { :allowed_return_code => [0], :forbidden_return_code => [], # :allowed_words_in_stderr => [], :forbidden_words_in_stderr => [], # :allowed_words_in_stdout => [], :forbidden_words_in_stdout => [], # :allowed_words_in_log_file => [], :forbidden_words_in_log_file => [], }, :on_error_do => :nothing, :run_via => :open3, :lib_logger => Logger.new($stderr), :lib_log_level => :info, }.deep_merge opts @logger = @opts[:lib_logger] configure_logging @logger.debug @opts @options = @opts[:options] @path = resolve_path @name, @opts[:search_paths] @parameter = @opts[:parameter] @log_file = @opts[:log_file] *@error_detection_on = @opts[:error_detection_on] @error_indicators = @opts[:error_indicators] @on_error_do = @opts[:on_error_do] @run_via = @opts[:run_via] @working_directory = @opts[:working_directory] @result = nil end private # Find path to cmd # # @param [String] name # Name of command. It accepts :cmd, 'cmd', 'rel_path/cmd' or # '/fq_path/to/cmd'. When :cmd is used it searches 'search_paths' for the # executable. Whenn 'cmd' is used it looks for cmd in local dir. The same # happens when 'rel_path/cmd' is used. A full qualified path # '/fq_path/to/cmd' is used as normal. # # @param [Array] search_paths # Where to look for executables # # @return [String] fully qualified path to command def resolve_path(name,*search_paths) search_paths ||= ['/bin', '/usr/bin'] search_paths = search_paths.flatten if name.kind_of? Symbol path = search_paths.map{ |p| File.join(p, name.to_s) }.find {|p| File.exists? p } || "" else path = File.expand_path(name) end path end # Check if executable exists, if it's executable and is a file # # @raise [CommandExec::Exceptions::CommandNotFound] if command does not exist # @raise [CommandExec::Exceptions::CommandNotExecutable] if command is not executable # @raise [CommandExec::Exceptions::CommandIsNotAFile] if command is not a file def check_path unless exists? @logger.fatal("Command '#{@name}' not found.") raise Exceptions::CommandNotFound , "Command '#{@name}' not found." end unless executable? @logger.fatal("Command '#{@name}' not executable.") raise Exceptions::CommandNotExecutable , "Command '#{@name}' not executable." end unless file? @logger.fatal("Command '#{@name}' not a file.") raise Exceptions::CommandIsNotAFile, "Command '#{@name}' not a file." end end # Set appropriate log level def configure_logging case @opts[:lib_log_level] when :debug @logger.level = Logger::DEBUG when :error @logger.level = Logger::ERROR when :fatal @logger.level = Logger::FATAL when :info @logger.level = Logger::INFO when :unknown @logger.level = Logger::UNKNOWN when :warn @logger.level = Logger::WARN when :silent @logger.level = Logger::SILENT else @logger.level = Logger::INFO end @logger.debug "Logger configured with log level #{@logger.level}" nil end public #Is executable valid def valid? exists? and executable? and file? end # Does the command exist? # # @return [True,False] result of check def exists? File.exists? @path end # Is the command executable # # @return [True,False] result of check def executable? File.executable? @path end # Is the provided string a file # # @return [True,False] result of check def file? File.file? @path end # Output the textual representation of a command # # @return [String] command in text form def to_s cmd = '' cmd += @path cmd += @options.blank? ? "" : " #{@options}" cmd += @parameter.blank? ? "" : " #{@parameter}" @logger.debug cmd cmd end # Run the program # # @raise [CommandExec::Exceptions::CommandExecutionFailed] if an error # occured and `command_exec` should raise an exception in the case of an # error. # @throw [:command_execution_failed] if an error # occured and `command_exec` should throw an error (which you can catch) # in the case of an error def run process = CommandExec::Process.new(:lib_logger => @logger) process.log_file = @log_file if @log_file process.status = :success check_path process.start_time = Time.now case @run_via when :open3 Open3::popen3(to_s, :chdir => @working_directory) do |stdin, stdout, stderr, wait_thr| process.stdout = stdout.readlines.map(&:chomp) process.stderr = stderr.readlines.map(&:chomp) process.pid = wait_thr.pid process.return_code = wait_thr.value.exitstatus end when :system Dir.chdir(@working_directory) do system(to_s) process.stdout = [] process.stderr = [] process.pid = $?.pid process.return_code = $?.exitstatus end else Open3::popen3(to_s, :chdir => @working_directory) do |stdin, stdout, stderr, wait_thr| process.stdout = stdout.readlines.map(&:chomp) process.stderr = stderr.readlines.map(&:chomp) process.pid = wait_thr.pid process.return_code = wait_thr.value.exitstatus end end process.end_time = Time.now if @error_detection_on.include?(:return_code) if not @error_indicators[:allowed_return_code].include? process.return_code or @error_indicators[:forbidden_return_code].include? process.return_code @logger.debug "Error detection on return code found an error" process.status = :failed process.reason_for_failure = :return_code end end if @error_detection_on.include?(:stderr) and not process.status == :failed if error_occured?( @error_indicators[:forbidden_words_in_stderr], @error_indicators[:allowed_words_in_stderr], process.stderr) @logger.debug "Error detection on stderr found an error" process.status = :failed process.reason_for_failure = :stderr end end if @error_detection_on.include?(:stdout) and not process.status == :failed if error_occured?( @error_indicators[:forbidden_words_in_stdout], @error_indicators[:allowed_words_in_stdout], process.stdout) @logger.debug "Error detection on stdout found an error" process.status = :failed process.reason_for_failure = :stdout end end if @error_detection_on.include?(:log_file) and not process.status == :failed if error_occured?( @error_indicators[:forbidden_words_in_log_file], @error_indicators[:allowed_words_in_log_file], process.log_file) @logger.debug "Error detection on log file found an error" process.status = :failed process.reason_for_failure = :log_file end end @logger.debug "Result of command run #{process.status}" @result = process if process.status == :failed case @on_error_do when :nothing #nothing when :raise_error raise CommandExec::Exceptions::CommandExecutionFailed, "An error occured. Please check for reason via command.reason_for_failure and/or command.stdout, comand.stderr, command.log_file, command.return_code" when :throw_error throw :command_execution_failed else #nothing end end end # Find error in data # # @param [Array,String] forbidden_word # what are the forbidden words which indidcate an error # # @param [Array,String] exception # Is there any exception from that forbidden words, maybe a string # which contains the forbidden word, but is no error? # # @param [Array,String] data # Where to look for errors. # # @return [Boolean] Returns true if it finds an error def error_occured?(forbidden_word, exception, data ) error_found = false *forbidden_word = forbidden_word *exception = exception *data = data return false if forbidden_word.blank? return false if data.blank? forbidden_word.each do |word| data.each do |line| line.strip! #line includes word -> error #exception does not include line/substring of line -> error, if # includes line/substring of line -> no error if line.include? word and exception.find{ |e| line[e] }.blank? error_found = true break end end end error_found end # Run a command # # @see #initialize def self.execute(name,opts={}) command = new(name,opts) command.run command end end end