lib/makit/command_runner.rb in makit-0.0.5 vs lib/makit/command_runner.rb in makit-0.0.6

- old
+ new

@@ -1,318 +1,321 @@ -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 = true - @commands = [] - end - - def get_cache_filename(command) - # test if the command_request is a Makit::V1::CommandRequest - #if command_request.is_a? Makit::V1::CommandRequest || command_request.is_a? Makit::V1::Command - # also replacing any path delimiters with an underscore - int_hash = Digest::SHA256.hexdigest("{command.name}.#{command.arguments.join(" ")}") - # int_hash - #int_hash = command_request.to_hash - hash_string = "#{int_hash}"[0, 8] - cache_filename = Makit::Directories::PROJECT_ARTIFACTS + - "/commands/cache/#{hash_string}.pb" - # create the directory if it does not already exist - FileUtils.mkdir_p(File.dirname(cache_filename)) - cache_filename - end - # if there is a matching cached command result, that then the specified timestamp, - # then return the cached result - # otherwise run the command and save the result to a cache file - # then return the result - def cache_run(command_request, timestamp) - # combine the command name and arguments into a single string - # and use it to create a cache filename, making sure it is a valid filename, - # by replacing all characters that are not valid in a filename with an underscore - # also replacing any path delimiters with an underscore - int_hash = command_request.to_hash - hash_string = "#{int_hash}"[0, 8] - cache_filename = Makit::Directories::PROJECT_ARTIFACTS + - "/commands/#{hash_string}.pb" - puts "cache_filename: #{cache_filename}" - - #cache_filename = Makit::Directories::PROJECT_ARTIFACTS + "/commands/#{command_request.name}.#{command_request.arguments.join("_")}.#{timestamp.seconds}.pb" - if File.exist?(cache_filename) - puts "cache file date: #{File.mtime(cache_filename)}" - if (File.mtime(cache_filename) > timestamp) - puts "cache_filename exists and is newer than #{timestamp}" - return Makit::Serializer.open(cache_filename, Makit::V1::Command) - else - puts "cache_filename exists, but is older than #{timestamp}" - end - end - - command = run(command_request) - # make sure the cache directory exists - FileUtils.mkdir_p(File.dirname(cache_filename)) - puts "saving command to cache_filename" - Makit::Serializer.save_as(cache_filename, command) - command - end - - # Run a command and return a Makit::V1::Command. - 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") - log_filename = get_cache_filename(command) - # 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 - - def log_rake_commands - dir = File.join(Makit::Directories::PROJECT_ARTIFACTS, "commands") - FileUtils.mkdir_p(dir) unless Dir.exist?(dir) - - # open a text file to write to - File.open(File.join(dir, "rake.commands.txt"), "w") do |file| - #rake_commands = commands.select { |command| command.name == "rake" } - commands.each do |command| - file.puts " " + Makit::CommandRunner.get_command_summary(command).strip_color_codes - file.puts " start time: #{command.started_at}" - file.puts command.output.strip_color_codes unless command.output.strip_color_codes.strip.length == 0 - file.puts command.error.strip_color_codes unless command.error.strip_color_codes.strip.length == 0 - file.puts " " - end - end - end - def log_slowest_commands - dir = File.join(Makit::Directories::PROJECT_ARTIFACTS, "commands") - FileUtils.mkdir_p(dir) unless Dir.exist?(dir) - - # open a text file to write to - File.open(File.join(dir, "slow.commands.txt"), "w") do |file| - Makit::RUNNER.commands.sort_by { |command| (command.duration.seconds + (command.duration.nanos / 1_000_000_000.0)) }.reverse.first(5).each do |command| - # Convert to float representation - duration_in_float = command.duration.seconds + (command.duration.nanos / 1_000_000_000.0) - #puts " #{command.name} took #{duration_in_float} seconds" - file.puts " " + Makit::CommandRunner.get_command_summary(command).strip_color_codes + " (#{command.duration.seconds} seconds)" - end - end - end - end # class CommandRunner -end # module Makit +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 = true + @commands = [] + end + + def get_cache_filename(command) + # test if the command_request is a Makit::V1::CommandRequest + #if command_request.is_a? Makit::V1::CommandRequest || command_request.is_a? Makit::V1::Command + # also replacing any path delimiters with an underscore + int_hash = Digest::SHA256.hexdigest("{command.name}.#{command.arguments.join(" ")}") + # int_hash + #int_hash = command_request.to_hash + hash_string = "#{int_hash}"[0, 8] + cache_filename = Makit::Directories::PROJECT_ARTIFACTS + + "/commands/cache/#{hash_string}.pb" + # create the directory if it does not already exist + FileUtils.mkdir_p(File.dirname(cache_filename)) + cache_filename + end + + # if there is a matching cached command result, that then the specified timestamp, + # then return the cached result + # otherwise run the command and save the result to a cache file + # then return the result + def cache_run(command_request, timestamp) + # combine the command name and arguments into a single string + # and use it to create a cache filename, making sure it is a valid filename, + # by replacing all characters that are not valid in a filename with an underscore + # also replacing any path delimiters with an underscore + int_hash = command_request.to_hash + hash_string = "#{int_hash}"[0, 8] + cache_filename = Makit::Directories::PROJECT_ARTIFACTS + + "/commands/#{hash_string}.pb" + #puts "cache_filename: #{cache_filename}" + + #cache_filename = Makit::Directories::PROJECT_ARTIFACTS + "/commands/#{command_request.name}.#{command_request.arguments.join("_")}.#{timestamp.seconds}.pb" + if File.exist?(cache_filename) + #puts "cache file date: #{File.mtime(cache_filename)}" + if (File.mtime(cache_filename) > timestamp) + #puts "cache_filename exists and is newer than #{timestamp}" + return Makit::Serializer.open(cache_filename, Makit::V1::Command) + else + #puts "cache_filename exists, but is older than #{timestamp}" + end + end + + command = run(command_request) + # make sure the cache directory exists + FileUtils.mkdir_p(File.dirname(cache_filename)) + #puts "saving command to cache_filename" + Makit::Serializer.save_as(cache_filename, command) + commands.push(command) + command + end + + # Run a command and return a Makit::V1::Command. + 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") + log_filename = get_cache_filename(command) + # 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 + + def log_rake_commands + dir = File.join(Makit::Directories::PROJECT_ARTIFACTS, "commands") + FileUtils.mkdir_p(dir) unless Dir.exist?(dir) + + # open a text file to write to + File.open(File.join(dir, "rake.commands.txt"), "w") do |file| + #rake_commands = commands.select { |command| command.name == "rake" } + commands.each do |command| + file.puts " " + Makit::CommandRunner.get_command_summary(command).strip_color_codes + file.puts " start time: #{command.started_at}" + file.puts command.output.strip_color_codes unless command.output.strip_color_codes.strip.length == 0 + file.puts command.error.strip_color_codes unless command.error.strip_color_codes.strip.length == 0 + file.puts " " + end + end + end + + def log_slowest_commands + dir = File.join(Makit::Directories::PROJECT_ARTIFACTS, "commands") + FileUtils.mkdir_p(dir) unless Dir.exist?(dir) + + # open a text file to write to + File.open(File.join(dir, "slow.commands.txt"), "w") do |file| + Makit::RUNNER.commands.sort_by { |command| (command.duration.seconds + (command.duration.nanos / 1_000_000_000.0)) }.reverse.first(5).each do |command| + # Convert to float representation + duration_in_float = command.duration.seconds + (command.duration.nanos / 1_000_000_000.0) + #puts " #{command.name} took #{duration_in_float} seconds" + file.puts " " + Makit::CommandRunner.get_command_summary(command).strip_color_codes + " (#{command.duration.seconds} seconds)" + end + end + end + end # class CommandRunner +end # module Makit