lib/raykit/command.rb



require 'open3'
require 'timeout'
require 'json'
require 'yaml'
require 'logger'
require 'securerandom'

BUFFER_SIZE=1024 if(!defined?(BUFFER_SIZE))
module Raykit
    # Functionality to support executing and logging system commands

    class Command
        @@commands=Array.new
        # The timeout in seconds, defaults to 0 if not specified

        attr_accessor :timeout
        # The working directory, defaults the current directory if not specified

        attr_accessor :directory
        # The start time

        attr_accessor :start_time
        # The execution time in seconds

        attr_accessor :elapsed
        attr_accessor :command,:output,:error,:exitstatus
        attr_accessor :user,:machine

        def init_defaults 
            @timeout=0
            @directory = Dir.pwd
            @output = ''
            @error = ''
            @exitstatus = 0
            @user = Environment.user
            @machine=Environment.machine
        end

        def initialize(command,timeout=0)
            @@commands=Array.new
            init_defaults
            @command=command
            @timeout=timeout
            if(@command.length > 0)
                run
            end
            self
        end

        def run() 
            @start_time = Time.now
            timer = Timer.new
            if(@timeout == 0)
                @output,@error,process_status = Open3.capture3(@command) 
                @exitstatus=process_status.exitstatus
            else
                Open3.popen3(@command, :chdir=>@directory) { |stdin,stdout,stderr,thread|
                    tick=2
                    pid = thread.pid
                    start = Time.now
                    elapsed = Time.now-start
                    while (elapsed) < @timeout and thread.alive?
                        Kernel.select([stdout,stderr], nil, nil, tick)
                        begin
                            @output << stdout.read_nonblock(BUFFER_SIZE)
                            @error << stderr.read_nonblock(BUFFER_SIZE)
                        rescue IO::WaitReadable
                        rescue EOFError
                            @exitstatus=thread.value.exitstatus
                            break
                        end
                        elapsed = Time.now-start
                    end
                    if thread.alive?
                        if(Gem.win_platform?)
                            `taskkill /f /pid #{pid}`
                        else
                            Process.kill("TERM", pid)
                        end
                        @exitstatus=5
                    else
                        @exitstatus=thread.value.exitstatus
                    end
                  }
            end
            @elapsed = timer.elapsed
            log
        end

        def log
            begin
                json=JSON.generate(to_hash)
                log_filename = Environment.get_dev_dir('log') + '/Raykit.Command/' + SecureRandom.uuid + '.json'
                log_dir = File.dirname(log_filename)
                FileUtils.mkdir_p(log_dir) if(!Dir.exist?(log_dir))
                File.open(log_filename, 'w') {|f| f.write(json) }
                if(@exitstatus == 0)
                    LOG.log('Raykit.Command',Logger::Severity::INFO,json)
                else
                    LOG.log('Raykit.Command',Logger::Severity::ERROR,json)
                end
            rescue JSON::GeneratorError => ge
                puts to_hash.to_s
                puts ge.to_s
                json=JSON.generate(to_hash)
            end
        end

        def elapsed_str()
            if(elapsed < 1.0)
                "%.0f" % (elapsed) + "s"
            else
                "%.0f" % (elapsed) + "s"
            end
        end

        def summary(show_directory=false)
            checkmark="\u2713"
            #warning="\u26A0"

            error="\u0058"
            symbol=Rainbow(checkmark.encode('utf-8')).green
            symbol=Rainbow(error.encode('utf-8')).red if(@exitstatus!=0)
            if(show_directory)
                puts symbol + "  " + elapsed_str.rjust(4) + " " + Rainbow(SECRETS.hide(@command)).yellow + " (#{@directory})"
            else
                puts symbol + "  " + elapsed_str.rjust(4) + " " + Rainbow(SECRETS.hide(@command)).yellow# + " (#{@directory})"

            end
            #puts symbol + "  " + elapsed_str.rjust(4) + " " + Rainbow(SECRETS.hide(@command)).yellow# + " (#{@directory})"

        end

        def details
            #summary

            puts @output
            puts @error
            puts 
        end

        def to_hash()
            hash = Hash.new
            hash[:command] = @command
            hash[:directory] = @directory
            hash[:timeout] = @timeout
            hash[:start_time] = @start_time
            hash[:elapsed] = @elapsed
            hash[:output] = @output.force_encoding("ISO-8859-1").encode("UTF-8")
            hash[:error] = @error.force_encoding("ISO-8859-1").encode("UTF-8")
            hash[:exitstatus] = @exitstatus
            hash[:user] = @user
            hash[:machine] = @machine
            hash[:context] = "Raykit.Command"
            hash
        end

        def from_hash(hash)
            @command = hash["command"]
            @directory=hash["directory"]
            @timeout=hash["timeout"]
            @start_time=hash["start_time"]
            @elapsed=hash["elapsed"]
            @output=hash["output"]
            @error=hash["error"]
            @exitstatus=hash["exitstatus"]
            @user=hash["user"] if(hash.include?("user"))
            @machine=hash["machine"] if(hash.include?("machine"))
        end

        def self.parse(json)
            cmd=Command.new('')
            cmd.from_hash(JSON.parse(json))
            cmd
        end

        def self.parse_yaml_commands(yaml)
            #commands=Array.new()

            data = YAML.load(yaml)
            commands = get_script_commands(data)
        end

        def self.get_script_commands(hash)
            commands=Array.new
            if(hash.key?('script'))
                hash['script'].each{|cmd|
                    commands << cmd
                }
            end
            hash.each{|k,v|
                if(v.is_a?(Hash))
                    subcommands=get_script_commands(v)
                    subcommands.each{|c|
                        commands << c
                    }
                end
            }
            commands
        end
    end
end