require "open3"
require "timeout"
require "json"
require "yaml"
require "logger"
require "securerandom"
BUFFER_SIZE = 1024 unless defined?(BUFFER_SIZE)
module Raykit
# Functionality to support executing and logging system commands
class Command
@@commands = []
# 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, :user, :machine, :logging_enabled, :success_log_level
def init_defaults
@timeout = 0
@directory = Dir.pwd
@output = +""
@error = +""
@exitstatus = 0
@user = Environment.user
@machine = Environment.machine
@logging_enabled = true
end
def initialize(command)
#def initialize(command, timeout = 0, success_log_level = Logger::DEBUG, logging_enabled = true)
timeout = 0
success_log_level = nil
logging_enabled = false
@@commands = []
init_defaults
@success_log_level = success_log_level
@logging_enabled = logging_enabled
@command = command
@timeout = timeout
#t = Time.now
@elapsed = 0
#run if @command.length.positive?
self
end
def set_timeout(timeout)
@timeout = timeout
self
end
def run
# puts '---running---'
@start_time = Time.now
@elapsed = 0
timer = Timer.new
if @timeout.zero?
@output, @error, process_status = Open3.capture3(@command)
@exitstatus = process_status.exitstatus
else
# =================================================
#puts "@timeout is #{@timeout}"
Open3.popen3(@command, chdir: @directory) do |_stdin, stdout, stderr, thread|
tick = 1
pid = thread.pid
start = Time.now
while ((Time.now - start) < @timeout) && 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
#puts "rescue block entered"
@exitstatus = thread.value.exitstatus
until stdout.eof?
@output << stdout.read_nonblock(BUFFER_SIZE)
end
until stderr.eof?
@error << stderr.read_nonblock(BUFFER_SIZE)
end
break
end
#begin
# @output << stdout.read_nonblock(BUFFER_SIZE)
# @error << stderr.read_nonblock(BUFFER_SIZE)
#rescue IO::WaitReadable
#rescue EOFError
# @exitstatus = thread.value.exitstatus
# until stdout.eof?
# @output << stdout.read_nonblock(BUFFER_SIZE)
# end
# until stderr.eof?
# @error << stderr.read_nonblock(BUFFER_SIZE)
# end
# break
#end
end
sleep 1
if thread.alive?
if Gem.win_platform?
`taskkill /f /pid #{pid}`
else
Process.kill("TERM", pid)
end
@exitstatus = 5
@error = +"timed out"
else
@exitstatus = thread.value.exitstatus
end
end
# =================================================
end
@elapsed = timer.elapsed
if @logging_enabled
log
if @exitstatus != 0
to_log_event.to_seq
else
# puts '---logging---'
unless @success_log_level.nil?
e = to_log_event
e["Level"] = "Verbose" if @success_log_level == "Verbose"
e["Level"] = "Debug" if @success_log_level == Logger::DEBUG
e["Level"] = "Information" if @success_log_level == Logger::INFO
e["Level"] = "Warning" if @elapsed > 60 * 2
e.to_seq
end
end
end
self
end
def to_log_event
secrets = Secrets.new
msg = secrets.hide(@command)
level = "Verbose"
level = "Warning" if @exitstatus != 0
output = @output
error = @error
output = @output[-1000..-1] if @output.length > 1200
error = @error[-1000..-1] if @error.length > 1200
Raykit::LogEvent.new(level, msg, {
"SourceContext" => "Raykit::Command",
"Category" => "Command",
"Timeout" => @timeout,
"Directory" => @directory,
"Output" => output,
"Error" => error,
"ExitStatus" => @exitstatus,
"Elapsed" => elapsed_str,
"ElapsedSeconds" => @elapsed,
})
end
def log
# --- Rolling File JSON -----
log = Logger.new("#{Raykit::Environment.log_dir}/Raykit.Commands.txt", "daily")
log.formatter = proc do |_severity, _datetime, _progname, msg|
"#{msg}\n"
end
secrets = Secrets.new
msg = secrets.hide(@command) # "?"# self.summary(false)
level = "Verbose"
level = "Warning" if @exitstatus != 0
event = Raykit::LogEvent.new(level, msg, {
"Timeout" => @timeout,
"Directory" => @directory,
"Output" => @output,
"Error" => @error,
"ExitStatus" => @exitstatus,
"Elapsed" => elapsed_str,
})
log.info event.to_json
# ---------------------------
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) unless Dir.exist?(log_dir)
File.open(log_filename, "w") { |f| f.write(json) }
if !@exitstatus.nil? && @exitstatus.zero?
LOG.log("Raykit.Command", Logger::Severity::INFO, json)
else
LOG.log("Raykit.Command", Logger::Severity::ERROR, json)
end
rescue JSON::GeneratorError => e
puts to_hash.to_s
puts e.to_s
json = JSON.generate(to_hash)
end
end
def elapsed_str
if elapsed < 1.0
end
"#{format("%.0f", elapsed)}s"
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
cmd = "#{Rainbow(SECRETS.hide(@command)).yellow}"
if show_directory
puts "#{symbol} #{cmd} " + Rainbow("#{elapsed_str}").cyan
puts Rainbow(" #{@directory}").white + " "
else
puts "#{symbol} #{Rainbow(SECRETS.hide(@command)).yellow} " + Rainbow("#{elapsed_str}").cyan
#puts "#{symbol} #{Rainbow(SECRETS.hide(@command)).yellow} (#{elapsed_str})"
end
self
end
def details_on_failure
if @exitstatus != 0
details
end
self
end
def details
puts " exit_code: " + @exitstatus.to_s
if @output.length > 0
@output.lines.each do |line|
puts " " + line
end
end
if @error.length > 0
@error.lines.each do |line|
puts " " + line
end
end
self
end
def to_markdown
checkmark = "\u2713"
error = "\u0058"
symbol = checkmark.encode("utf-8")
symbol = error.encode("utf-8") if @exitstatus != 0
cmd = "#{SECRETS.hide(@command)}"
md = "#{symbol} #{SECRETS.hide(@command)} (#{elapsed_str})"
md
end
def save
filename = "#{Environment.get_dev_dir("log")}/Commands/#{SecureRandom.uuid}"
log_dir = File.dirname(filename)
FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir)
File.open(filename, "w") do |f|
f.write(JSON.pretty_generate(to_hash))
end
self
end
def save_as(filename)
File.open(filename, "w") do |f|
f.write(JSON.pretty_generate(to_hash))
end
self
end
def log_to_file(filename)
File.delete(filename) if File.exists?(filename)
File.open(filename, "w") { |f|
f.puts output
f.puts error
}
self
end
def to_hash
hash = {}
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.safe_load(yaml)
commands = get_script_commands(data)
end
def self.get_script_commands(hash)
commands = []
if hash.key?("script")
hash["script"].each do |cmd|
commands << cmd
end
end
hash.each do |_k, v|
next unless v.is_a?(Hash)
subcommands = get_script_commands(v)
subcommands.each do |c|
commands << c
end
end
commands
end
end
end