213 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
		
		
			
		
	
	
			213 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| 
								 | 
							
								require 'constants'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class ShellExecutionException < RuntimeError
							 | 
						||
| 
								 | 
							
								  attr_reader :shell_result
							 | 
						||
| 
								 | 
							
								  def initialize(shell_result)
							 | 
						||
| 
								 | 
							
								    @shell_result = shell_result
							 | 
						||
| 
								 | 
							
								  end
							 | 
						||
| 
								 | 
							
								end
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class ToolExecutor
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  constructor :configurator, :tool_executor_helper, :streaminator, :system_wrapper
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  def setup
							 | 
						||
| 
								 | 
							
								    @tool_name  = ''
							 | 
						||
| 
								 | 
							
								    @executable = ''
							 | 
						||
| 
								 | 
							
								  end
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  # build up a command line from yaml provided config
							 | 
						||
| 
								 | 
							
								  def build_command_line(tool_config, *args)
							 | 
						||
| 
								 | 
							
								    @tool_name  = tool_config[:name]
							 | 
						||
| 
								 | 
							
								    @executable = tool_config[:executable]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    command = {}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # basic premise is to iterate top to bottom through arguments using '$' as 
							 | 
						||
| 
								 | 
							
								    #  a string replacement indicator to expand globals or inline yaml arrays
							 | 
						||
| 
								 | 
							
								    #  into command line arguments via substitution strings
							 | 
						||
| 
								 | 
							
								    command[:line] = [
							 | 
						||
| 
								 | 
							
								      @tool_executor_helper.osify_path_separators( expandify_element(@executable, *args) ),
							 | 
						||
| 
								 | 
							
								      build_arguments(tool_config[:arguments], *args),
							 | 
						||
| 
								 | 
							
								      ].join(' ').strip
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    command[:options] = {
							 | 
						||
| 
								 | 
							
								      :stderr_redirect => @tool_executor_helper.stderr_redirection(tool_config, @configurator.project_logging),
							 | 
						||
| 
								 | 
							
								      :background_exec => tool_config[:background_exec]
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    return command
							 | 
						||
| 
								 | 
							
								  end
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  # shell out, execute command, and return response
							 | 
						||
| 
								 | 
							
								  def exec(command, options={}, args=[])
							 | 
						||
| 
								 | 
							
								    options[:boom] = true if (options[:boom].nil?)
							 | 
						||
| 
								 | 
							
								    options[:stderr_redirect] = StdErrRedirect::NONE if (options[:stderr_redirect].nil?)
							 | 
						||
| 
								 | 
							
								    options[:background_exec] = BackgroundExec::NONE if (options[:background_exec].nil?)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # build command line
							 | 
						||
| 
								 | 
							
								    command_line = [
							 | 
						||
| 
								 | 
							
								      @tool_executor_helper.background_exec_cmdline_prepend( options ),
							 | 
						||
| 
								 | 
							
								      command.strip,
							 | 
						||
| 
								 | 
							
								      args,
							 | 
						||
| 
								 | 
							
								      @tool_executor_helper.stderr_redirect_cmdline_append( options ),
							 | 
						||
| 
								 | 
							
								      @tool_executor_helper.background_exec_cmdline_append( options ),
							 | 
						||
| 
								 | 
							
								      ].flatten.compact.join(' ')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    shell_result = {}
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    # depending on background exec option, we shell out differently
							 | 
						||
| 
								 | 
							
								    if (options[:background_exec] != BackgroundExec::NONE)
							 | 
						||
| 
								 | 
							
								      shell_result = @system_wrapper.shell_system( command_line )
							 | 
						||
| 
								 | 
							
								    else
							 | 
						||
| 
								 | 
							
								      shell_result = @system_wrapper.shell_backticks( command_line )
							 | 
						||
| 
								 | 
							
								    end
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    @tool_executor_helper.print_happy_results( command_line, shell_result, options[:boom] )
							 | 
						||
| 
								 | 
							
								    @tool_executor_helper.print_error_results( command_line, shell_result, options[:boom] )
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    # go boom if exit code isn't 0 (but in some cases we don't want a non-0 exit code to raise)
							 | 
						||
| 
								 | 
							
								    raise ShellExecutionException.new(shell_result) if ((shell_result[:exit_code] != 0) and options[:boom])
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    return shell_result
							 | 
						||
| 
								 | 
							
								  end
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  private #############################
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  def build_arguments(config, *args)
							 | 
						||
| 
								 | 
							
								    build_string = ''
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    return nil if (config.nil?)
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    # iterate through each argument
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # the yaml blob array needs to be flattened so that yaml substitution
							 | 
						||
| 
								 | 
							
								    # is handled correctly, since it creates a nested array when an anchor is
							 | 
						||
| 
								 | 
							
								    # dereferenced
							 | 
						||
| 
								 | 
							
								    config.flatten.each do |element|
							 | 
						||
| 
								 | 
							
								      argument = ''
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      case(element)
							 | 
						||
| 
								 | 
							
								        # if we find a simple string then look for string replacement operators
							 | 
						||
| 
								 | 
							
								        #  and expand with the parameters in this method's argument list
							 | 
						||
| 
								 | 
							
								        when String then argument = expandify_element(element, *args)
							 | 
						||
| 
								 | 
							
								        # if we find a hash, then we grab the key as a substitution string and expand the
							 | 
						||
| 
								 | 
							
								        #  hash's value(s) within that substitution string
							 | 
						||
| 
								 | 
							
								        when Hash   then argument = dehashify_argument_elements(element)
							 | 
						||
| 
								 | 
							
								      end
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      build_string.concat("#{argument} ") if (argument.length > 0)
							 | 
						||
| 
								 | 
							
								    end
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    build_string.strip!
							 | 
						||
| 
								 | 
							
								    return build_string if (build_string.length > 0)
							 | 
						||
| 
								 | 
							
								    return nil
							 | 
						||
| 
								 | 
							
								  end
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  # handle simple text string argument & argument array string replacement operators
							 | 
						||
| 
								 | 
							
								  def expandify_element(element, *args)
							 | 
						||
| 
								 | 
							
								    match = //
							 | 
						||
| 
								 | 
							
								    to_process = nil
							 | 
						||
| 
								 | 
							
								    args_index = 0
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # handle ${#} input replacement
							 | 
						||
| 
								 | 
							
								    if (element =~ TOOL_EXECUTOR_ARGUMENT_REPLACEMENT_PATTERN)
							 | 
						||
| 
								 | 
							
								      args_index = ($2.to_i - 1)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (args.nil? or args[args_index].nil?)
							 | 
						||
| 
								 | 
							
								        @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' expected valid argument data to accompany replacement operator #{$1}.", Verbosity::ERRORS)
							 | 
						||
| 
								 | 
							
								        raise
							 | 
						||
| 
								 | 
							
								      end
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      match = /#{Regexp.escape($1)}/
							 | 
						||
| 
								 | 
							
								      to_process = args[args_index]
							 | 
						||
| 
								 | 
							
								    end
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								    # simple string argument: replace escaped '\$' and strip
							 | 
						||
| 
								 | 
							
								    element.sub!(/\\\$/, '$')
							 | 
						||
| 
								 | 
							
								    element.strip!
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # handle inline ruby execution
							 | 
						||
| 
								 | 
							
								    if (element =~ RUBY_EVAL_REPLACEMENT_PATTERN)
							 | 
						||
| 
								 | 
							
								      element.replace(eval($1))
							 | 
						||
| 
								 | 
							
								    end
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    build_string = ''
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # handle array or anything else passed into method to be expanded in place of replacement operators
							 | 
						||
| 
								 | 
							
								    case (to_process)
							 | 
						||
| 
								 | 
							
								      when Array then to_process.each {|value| build_string.concat( "#{element.sub(match, value.to_s)} " ) } if (to_process.size > 0)
							 | 
						||
| 
								 | 
							
								      else build_string.concat( element.sub(match, to_process.to_s) )
							 | 
						||
| 
								 | 
							
								    end
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # handle inline ruby string substitution
							 | 
						||
| 
								 | 
							
								    if (build_string =~ RUBY_STRING_REPLACEMENT_PATTERN)
							 | 
						||
| 
								 | 
							
								      build_string.replace(@system_wrapper.module_eval(build_string))
							 | 
						||
| 
								 | 
							
								    end
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    return build_string.strip
							 | 
						||
| 
								 | 
							
								  end
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  # handle argument hash: keys are substitution strings, values are data to be expanded within substitution strings
							 | 
						||
| 
								 | 
							
								  def dehashify_argument_elements(hash)
							 | 
						||
| 
								 | 
							
								    build_string = ''
							 | 
						||
| 
								 | 
							
								    elements = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # grab the substitution string (hash key)
							 | 
						||
| 
								 | 
							
								    substitution = hash.keys[0].to_s
							 | 
						||
| 
								 | 
							
								    # grab the string(s) to squirt into the substitution string (hash value)
							 | 
						||
| 
								 | 
							
								    expand = hash[hash.keys[0]]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (expand.nil?)
							 | 
						||
| 
								 | 
							
								      @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' could not expand nil elements for substitution string '#{substitution}'.", Verbosity::ERRORS)
							 | 
						||
| 
								 | 
							
								      raise
							 | 
						||
| 
								 | 
							
								    end
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    # array-ify expansion input if only a single string
							 | 
						||
| 
								 | 
							
								    expansion = ((expand.class == String) ? [expand] : expand)
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    expansion.each do |item|
							 | 
						||
| 
								 | 
							
								      # code eval substitution
							 | 
						||
| 
								 | 
							
								      if (item =~ RUBY_EVAL_REPLACEMENT_PATTERN)
							 | 
						||
| 
								 | 
							
								        elements << eval($1)
							 | 
						||
| 
								 | 
							
								      # string eval substitution
							 | 
						||
| 
								 | 
							
								      elsif (item =~ RUBY_STRING_REPLACEMENT_PATTERN)
							 | 
						||
| 
								 | 
							
								        elements << @system_wrapper.module_eval(item)
							 | 
						||
| 
								 | 
							
								      # global constants
							 | 
						||
| 
								 | 
							
								      elsif (@system_wrapper.constants_include?(item))
							 | 
						||
| 
								 | 
							
								        const = Object.const_get(item)
							 | 
						||
| 
								 | 
							
								        if (const.nil?)
							 | 
						||
| 
								 | 
							
								          @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' found constant '#{item}' to be nil.", Verbosity::ERRORS)
							 | 
						||
| 
								 | 
							
								          raise
							 | 
						||
| 
								 | 
							
								        else
							 | 
						||
| 
								 | 
							
								          elements << const
							 | 
						||
| 
								 | 
							
								        end
							 | 
						||
| 
								 | 
							
								      elsif (item.class == Array)
							 | 
						||
| 
								 | 
							
								        elements << item
							 | 
						||
| 
								 | 
							
								      elsif (item.class == String)
							 | 
						||
| 
								 | 
							
								        @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' cannot expand nonexistent value '#{item}' for substitution string '#{substitution}'.", Verbosity::ERRORS)
							 | 
						||
| 
								 | 
							
								        raise        
							 | 
						||
| 
								 | 
							
								      else
							 | 
						||
| 
								 | 
							
								        @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' cannot expand value having type '#{item.class}' for substitution string '#{substitution}'.", Verbosity::ERRORS)
							 | 
						||
| 
								 | 
							
								        raise        
							 | 
						||
| 
								 | 
							
								      end
							 | 
						||
| 
								 | 
							
								    end
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    # expand elements (whether string or array) into substitution string & replace escaped '\$'
							 | 
						||
| 
								 | 
							
								    elements.flatten!
							 | 
						||
| 
								 | 
							
								    elements.each do |element|
							 | 
						||
| 
								 | 
							
								      build_string.concat( substitution.sub(/([^\\]*)\$/, "\\1#{element}") ) # don't replace escaped '\$' but allow us to replace just a lonesome '$'
							 | 
						||
| 
								 | 
							
								      build_string.gsub!(/\\\$/, '$')
							 | 
						||
| 
								 | 
							
								      build_string.concat(' ')
							 | 
						||
| 
								 | 
							
								    end
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return build_string.strip
							 | 
						||
| 
								 | 
							
								  end
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								end
							 |