require "English" require "open3" require "socket" require "etc" require "logger" # This module provides classes for the Makit gem. module Makit # This class provide methods running commands. # class CommandRunner attr_accessor :show_output_on_success, :log_to_artifacts, :commands def initialize @show_output_on_success = false @log_to_artifacts = false @commands = [] end def run(command_request) raise "Invalid command_request" unless command_request.is_a? Makit::V1::CommandRequest command = execute(command_request) show_output = true exit_on_error = true log_to_artifacts(command) if @log_to_artifacts if command.exit_code != 0 puts Makit::CommandRunner.get_command_summary(command) + " (exit code #{command.exit_code})".colorize(:default) puts " directory: #{command.directory}\n" puts " duration: #{command.duration.seconds} seconds\n" puts Makit::Humanize::indent_string(command.output, 2) if command.output.length > 0 puts Makit::Humanize::indent_string(command.error, 2) if command.error.length > 0 exit 1 if command_request.exit_on_error else puts Makit::CommandRunner.get_command_summary(command) + " (#{command.duration.seconds} seconds)".colorize(:cyan) puts Makit::Humanize::indent_string(command.output, 2).colorize(:default) if show_output_on_success end commands.push(command) command end def log_to_artifacts(command) dir = File.join(Makit::Directories::PROJECT_ARTIFACTS, "commands") FileUtils.mkdir_p(dir) unless Dir.exist?(dir) filename_friendly_timestamp = Time.now.strftime("%Y.%m.%d_%H%M%S") log_filename = File.join(dir, "#{filename_friendly_timestamp}.json") # serialize to protobuf json json = command.to_json pretty_json = JSON.pretty_generate(JSON.parse(json)) File.write(log_filename, pretty_json) end # Run a command and return a Makit::V1::Command. def try(args) request = parse_command_request(args) request.exit_on_error = false run(request) #run2(args, false) end # Show the output of a command and return a Makit::V1::Command. def show(args) request = parse_args(args) command = execute(request) show_output = true exit_on_error = true Makit::LOGGER.info(Makit::CommandRunner.get_command_summary(command)) show_output = true if command.exit_code != 0 Makit::LOGGER.info(indent_string("\n#{command.output}\n#{command.error}\n".strip, 2)) if show_output exit(command.exit_code) if exit_on_error && command.exit_code != 0 # unless process_status.success? command end # Parse and return a Makit::V1::CommandRequest. def parse_command_request(source) return Makit::V1::CommandRequest.new(source) if source.is_a? Hash return source if source.is_a? Makit::V1::CommandRequest if source.is_a? String return parse_args(source) end raise "Invalid source" unless source.is_a? Makit::V1::CommandRequest end def parse_command_request_from_hash(hash) raise "Invalid hash" unless hash.is_a? Hash Makit::V1::CommandRequest.new(hash) end def parse_command_request_from_string(source) raise "Invalid source" unless source.is_a? String words = source.split(" ") hash = { name: words.shift, arguments: words, exit_on_error: true, } end # Parse the command line arguments into a Makit::V1::CommandRequest. def parse_args(args) #raise "No command specified" if args.empty? if args.is_a? Makit::V1::CommandRequest args else if args.is_a? String args = args.split(" ") if (args.length == 1) hash = { name: args[0], arguments: [], exit_on_error: true, } Makit::V1::CommandRequest.new(hash) else hash = { name: args.shift, arguments: args, exit_on_error: true, } Makit::V1::CommandRequest.new(hash) end else Makit::V1::CommandRequest.new(args) end end end def get_path_name(name) # replace all characters that a not valid in a filename with an underscore name.gsub(/[^0-9a-z]/i, "_") end # Given a Makit::V1::CommandRequest, execute the command and return a Makit::V1::Command. def execute(args) command_request = parse_args(args) command_request.directory = Dir.pwd if command_request.directory.nil? command_request.directory = Dir.pwd if command_request.directory.length == 0 result = Makit::V1::Command.new(name: command_request.name) command_request.arguments.each do |arg| result.arguments.push(arg) end command = "#{command_request.name} #{command_request.arguments.join(" ")}" result.directory = command_request.directory start = Time.now filename_friendly_timestamp = Time.now.strftime("%Y.%m.%d_%H%M%S") log_filename = File.join(Makit::Directories::LOG, "#{filename_friendly_timestamp}.log") # assign a directory variable to the current working directory, if not specified, # otherwise assign the specified directory command_request.directory = Dir.pwd if command_request.directory.nil? command_request.directory = Dir.pwd if command_request.directory.length == 0 raise "Invalid directory" unless Dir.exist?(command_request.directory) result.started_at = Google::Protobuf::Timestamp.new(seconds: start.to_i, nanos: start.nsec.to_i) ############# execute the command (output, error, exit_code) = execute_command(command, command_request.directory, command_request.timeout) result.output = output.force_encoding("ASCII-8BIT") result.error = error.force_encoding("ASCII-8BIT") result.exit_code = exit_code.nil? ? 0 : exit_code elapsed_time = Time.now - start seconds = elapsed_time.to_i nanos = ((elapsed_time - seconds) * 1_000_000_000).to_i result.duration = Google::Protobuf::Duration.new(seconds: seconds, nanos: nanos) result end # pure function to execute a command # returns (stdout, stderr, exit_code) or raise an exception def execute_command(command, directory, timeout) original_directory = Dir.pwd begin output = nil error = nil process_status = nil Dir.chdir(directory) do output, error, process_status = Open3.capture3(command) end return [output, error, process_status.exitstatus] rescue => e # restore the original working directory Dir.chdir(original_directory) message_parts = [] message_parts << "failed to execute #{command}" message_parts << "directory: #{directory}" message_parts << "timeout: #{timeout}" unless timeout.nil? message_parts << "error: #{e.message}" message = message_parts.join("\n") + "\n" return ["", message, 1] #raise Makit::Error, message end end def execute_command_request(command_request) # if the command_request is not a Makit::V1::CommandRequest, raise an error raise "Invalid command_request" unless command_request.is_a? Makit::V1::CommandRequest args = Array.new command_request.arguments.each do |arg| args.push(arg) end result = Makit::V1::Command.new({ name: command_request.name, arguments: args, started_at: Google::Protobuf::Timestamp.new({ seconds: Time.now.to_i, nanos: Time.now.nsec }), }) begin rescue => e end end def indent_string(input_string, indent_spaces) indentation = " " * indent_spaces input_string.lines.map { |line| indentation + line }.join end def self.get_command_summary(command) symbol = Makit::Symbols.warning symbol = Makit::Symbols.checkmark if !command.exit_code.nil? && command.exit_code.zero? symbol = Makit::Symbols.error if command.exit_code != 0 summary = "#{symbol} #{command.name.colorize(:yellow)} #{command.arguments.join(" ")}" if summary.length > 80 summary = summary.to_lines(80, command.name.length + 3) end summary end end # class CommandRunner end # module Makit