tool_executor.rb 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. require 'ceedling/constants'
  2. require 'benchmark'
  3. class ShellExecutionException < RuntimeError
  4. attr_reader :shell_result
  5. def initialize(shell_result)
  6. @shell_result = shell_result
  7. end
  8. end
  9. class ToolExecutor
  10. constructor :configurator, :tool_executor_helper, :streaminator, :system_wrapper
  11. def setup
  12. @tool_name = ''
  13. @executable = ''
  14. end
  15. # build up a command line from yaml provided config
  16. # @param extra_params is an array of parameters to append to executable
  17. def build_command_line(tool_config, extra_params, *args)
  18. @tool_name = tool_config[:name]
  19. @executable = tool_config[:executable]
  20. command = {}
  21. # basic premise is to iterate top to bottom through arguments using '$' as
  22. # a string replacement indicator to expand globals or inline yaml arrays
  23. # into command line arguments via substitution strings
  24. # executable must be quoted if it includes spaces (common on windows)
  25. executable = @tool_executor_helper.osify_path_separators( expandify_element(@executable, *args) )
  26. executable = "\"#{executable}\"" if executable.include?(' ')
  27. command[:line] = [
  28. executable,
  29. extra_params.join(' ').strip,
  30. build_arguments(tool_config[:arguments], *args),
  31. ].reject{|s| s.nil? || s.empty?}.join(' ').strip
  32. command[:options] = {
  33. :stderr_redirect => @tool_executor_helper.stderr_redirection(tool_config, @configurator.project_logging),
  34. :background_exec => tool_config[:background_exec]
  35. }
  36. return command
  37. end
  38. # shell out, execute command, and return response
  39. def exec(command, options={}, args=[])
  40. options[:boom] = true if (options[:boom].nil?)
  41. options[:stderr_redirect] = StdErrRedirect::NONE if (options[:stderr_redirect].nil?)
  42. options[:background_exec] = BackgroundExec::NONE if (options[:background_exec].nil?)
  43. # build command line
  44. command_line = [
  45. @tool_executor_helper.background_exec_cmdline_prepend( options ),
  46. command.strip,
  47. args,
  48. @tool_executor_helper.stderr_redirect_cmdline_append( options ),
  49. @tool_executor_helper.background_exec_cmdline_append( options ),
  50. ].flatten.compact.join(' ')
  51. @streaminator.stderr_puts("Verbose: #{__method__.to_s}(): #{command_line}", Verbosity::DEBUG)
  52. shell_result = {}
  53. # depending on background exec option, we shell out differently
  54. time = Benchmark.realtime do
  55. if (options[:background_exec] != BackgroundExec::NONE)
  56. shell_result = @system_wrapper.shell_system( command_line, options[:boom] )
  57. else
  58. shell_result = @system_wrapper.shell_backticks( command_line, options[:boom] )
  59. end
  60. end
  61. shell_result[:time] = time
  62. #scrub the string for illegal output
  63. unless shell_result[:output].nil?
  64. shell_result[:output] = shell_result[:output].scrub if "".respond_to?(:scrub)
  65. shell_result[:output].gsub!(/\033\[\d\dm/,'')
  66. end
  67. @tool_executor_helper.print_happy_results( command_line, shell_result, options[:boom] )
  68. @tool_executor_helper.print_error_results( command_line, shell_result, options[:boom] )
  69. # go boom if exit code isn't 0 (but in some cases we don't want a non-0 exit code to raise)
  70. raise ShellExecutionException.new(shell_result) if ((shell_result[:exit_code] != 0) and options[:boom])
  71. return shell_result
  72. end
  73. private #############################
  74. def build_arguments(config, *args)
  75. build_string = ''
  76. return nil if (config.nil?)
  77. # iterate through each argument
  78. # the yaml blob array needs to be flattened so that yaml substitution
  79. # is handled correctly, since it creates a nested array when an anchor is
  80. # dereferenced
  81. config.flatten.each do |element|
  82. argument = ''
  83. case(element)
  84. # if we find a simple string then look for string replacement operators
  85. # and expand with the parameters in this method's argument list
  86. when String then argument = expandify_element(element, *args)
  87. # if we find a hash, then we grab the key as a substitution string and expand the
  88. # hash's value(s) within that substitution string
  89. when Hash then argument = dehashify_argument_elements(element)
  90. end
  91. build_string.concat("#{argument} ") if (argument.length > 0)
  92. end
  93. build_string.strip!
  94. return build_string if (build_string.length > 0)
  95. return nil
  96. end
  97. # handle simple text string argument & argument array string replacement operators
  98. def expandify_element(element, *args)
  99. match = //
  100. to_process = nil
  101. args_index = 0
  102. # handle ${#} input replacement
  103. if (element =~ TOOL_EXECUTOR_ARGUMENT_REPLACEMENT_PATTERN)
  104. args_index = ($2.to_i - 1)
  105. if (args.nil? or args[args_index].nil?)
  106. @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' expected valid argument data to accompany replacement operator #{$1}.", Verbosity::ERRORS)
  107. raise
  108. end
  109. match = /#{Regexp.escape($1)}/
  110. to_process = args[args_index]
  111. end
  112. # simple string argument: replace escaped '\$' and strip
  113. element.sub!(/\\\$/, '$')
  114. element.strip!
  115. # handle inline ruby execution
  116. if (element =~ RUBY_EVAL_REPLACEMENT_PATTERN)
  117. element.replace(eval($1))
  118. end
  119. build_string = ''
  120. # handle array or anything else passed into method to be expanded in place of replacement operators
  121. case (to_process)
  122. when Array then to_process.each {|value| build_string.concat( "#{element.sub(match, value.to_s)} " ) } if (to_process.size > 0)
  123. else build_string.concat( element.sub(match, to_process.to_s) )
  124. end
  125. # handle inline ruby string substitution
  126. if (build_string =~ RUBY_STRING_REPLACEMENT_PATTERN)
  127. build_string.replace(@system_wrapper.module_eval(build_string))
  128. end
  129. return build_string.strip
  130. end
  131. # handle argument hash: keys are substitution strings, values are data to be expanded within substitution strings
  132. def dehashify_argument_elements(hash)
  133. build_string = ''
  134. elements = []
  135. # grab the substitution string (hash key)
  136. substitution = hash.keys[0].to_s
  137. # grab the string(s) to squirt into the substitution string (hash value)
  138. expand = hash[hash.keys[0]]
  139. if (expand.nil?)
  140. @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' could not expand nil elements for substitution string '#{substitution}'.", Verbosity::ERRORS)
  141. raise
  142. end
  143. # array-ify expansion input if only a single string
  144. expansion = ((expand.class == String) ? [expand] : expand)
  145. expansion.each do |item|
  146. # code eval substitution
  147. if (item =~ RUBY_EVAL_REPLACEMENT_PATTERN)
  148. elements << eval($1)
  149. # string eval substitution
  150. elsif (item =~ RUBY_STRING_REPLACEMENT_PATTERN)
  151. elements << @system_wrapper.module_eval(item)
  152. # global constants
  153. elsif (@system_wrapper.constants_include?(item))
  154. const = Object.const_get(item)
  155. if (const.nil?)
  156. @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' found constant '#{item}' to be nil.", Verbosity::ERRORS)
  157. raise
  158. else
  159. elements << const
  160. end
  161. elsif (item.class == Array)
  162. elements << item
  163. elsif (item.class == String)
  164. @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' cannot expand nonexistent value '#{item}' for substitution string '#{substitution}'.", Verbosity::ERRORS)
  165. raise
  166. else
  167. @streaminator.stderr_puts("ERROR: Tool '#{@tool_name}' cannot expand value having type '#{item.class}' for substitution string '#{substitution}'.", Verbosity::ERRORS)
  168. raise
  169. end
  170. end
  171. # expand elements (whether string or array) into substitution string & replace escaped '\$'
  172. elements.flatten!
  173. elements.each do |element|
  174. build_string.concat( substitution.sub(/([^\\]*)\$/, "\\1#{element}") ) # don't replace escaped '\$' but allow us to replace just a lonesome '$'
  175. build_string.gsub!(/\\\$/, '$')
  176. build_string.concat(' ')
  177. end
  178. return build_string.strip
  179. end
  180. end