class CommandProposal::Services::Runner
Attributes
session[RW]
Public Class Methods
execute(friendly_id)
click to toggle source
Add expiration and things like that…
# File lib/command_proposal/services/runner.rb, line 7 def self.execute(friendly_id) task = ::CommandProposal::Task.find_by!(friendly_id: friendly_id) new.execute(task.primary_iteration) end
new()
click to toggle source
# File lib/command_proposal/services/runner.rb, line 13 def initialize @session = session end
Public Instance Methods
execute(iteration)
click to toggle source
# File lib/command_proposal/services/runner.rb, line 17 def execute(iteration) @iteration = iteration prepare run complete proposal = ::CommandProposal::Service::ProposalPresenter.new(@iteration) @iteration = nil proposal end
quick_run(friendly_id)
click to toggle source
# File lib/command_proposal/services/runner.rb, line 29 def quick_run(friendly_id) task = ::CommandProposal::Task.module.find_by!(friendly_id: friendly_id) iteration = task&.primary_iteration raise CommandProposal::Error, ":#{friendly_id} does not have approval to run." unless iteration&.approved? @session.eval(iteration.code) end
Private Instance Methods
bring_function()
click to toggle source
# File lib/command_proposal/services/runner.rb, line 101 def bring_function "def bring(*func_names); func_names.each { |f| self.quick_run(f) }; end" end
complete()
click to toggle source
# File lib/command_proposal/services/runner.rb, line 105 def complete @iteration.completed_at = Time.current if @iteration.cancelling? || @iteration.cancelled? @iteration.result += "\n\n~~~~~ CANCELLED ~~~~~" @iteration.status = :cancelled elsif @iteration.status&.to_sym == :failed # No-op else @iteration.status = :success end @iteration.save! return if @iteration.task.console? # Don't notify for every console entry proposal = ::CommandProposal::Service::ProposalPresenter.new(@iteration) if @iteration.success? ::CommandProposal.configuration.success_callback&.call(proposal) else ::CommandProposal.configuration.failed_callback&.call(proposal) end end
full_trace_from_exception(exception)
click to toggle source
# File lib/command_proposal/services/runner.rb, line 161 def full_trace_from_exception(exception) trace = exception.try(:backtrace).presence return trace if trace.present? trace = @session.send(:caller).dup return trace if trace.present? trace = caller.dup trace end
gather_exception_info(exception)
click to toggle source
# File lib/command_proposal/services/runner.rb, line 139 def gather_exception_info(exception) error_info = [] backtrace = full_trace_from_exception(exception) eval_trace = backtrace.select { |row| row.include?("(eval)") }.presence || [] eval_trace = eval_trace.map do |row| eval_row_number = row[/\(eval\)\:\d+/].to_s[7..-1] next if eval_row_number.blank? error_line = @iteration.code.split("\n")[eval_row_number.to_i - 1] "#{eval_row_number}: #{error_line}" if error_line.present? end.compact error_info += ["\n>> Command Trace"] + eval_trace if eval_trace.any? app_trace = backtrace.select { |row| row.include?("/app/") && !row.match?(/command_proposal\/(lib|app)/) }.presence || [] error_info += ["\n>> App Trace"] + app_trace if app_trace.any? error_info.uniq.join("\n") end
prepare()
click to toggle source
# File lib/command_proposal/services/runner.rb, line 44 def prepare raise CommandProposal::Error, "Cannot run task without approval" unless @iteration.approved? raise CommandProposal::Error, "Modules cannot be run independently" if @iteration.task.module? @iteration.task.update(last_executed_at: Time.current) @iteration.update(started_at: Time.current, status: :started) end
results_from_exception(exc)
click to toggle source
# File lib/command_proposal/services/runner.rb, line 126 def results_from_exception(exc) klass = exc.class msg = exc.try(:message) || exc.try(:body) || exc.to_s # Remove proposal context msg.gsub!(/ for \#\<CommandProposal.*/, "") msg.gsub!(/(::)?CommandProposal::Services::Runner(::)?/, "") # Remove gem lines msg.gsub!(/\/?((\w|(\\ ))*\/)*command_proposal\/services(\/(\w|(\\ ))*)*\.\w+\:\d+\: /, "") info = gather_exception_info(exc) ["#{klass}: #{msg}", info.presence].compact.join("\n") end
run()
click to toggle source
# File lib/command_proposal/services/runner.rb, line 52 def run begin @session.eval("#{bring_function};params = #{@iteration.args || {}}.with_indifferent_access") rescue Exception => e # rubocop:disable Lint/RescueException - Yes, rescue full Exception so that we can catch typos in evals as well return @iteration.result = results_from_exception(e) end stored_stdout = $stdout $stdout = StringIO.new result = nil # Init var for scope status = nil running_thread = Thread.new do begin # Run bring functions in here so we can capture any string outputs # OR! Run the full runner and instead of saving to an iteration, return the string for prepending here result = @session.eval("_ = (#{@iteration.code})").inspect # rubocop:disable Security/Eval - Eval is scary, but in this case it's exactly what we need. status = :success rescue Exception => e # rubocop:disable Lint/RescueException - Yes, rescue full Exception so that we can catch typos in evals as well status = :failed result = results_from_exception(e) end end while running_thread.status.present? @iteration.reload if $stdout.try(:string) != @iteration.result @iteration.update(result: $stdout.try(:string).dup) end if @iteration.cancelling? running_thread.exit status = :cancelled end sleep 0.4 end output = $stdout.try(:string) output = nil if output == "" # Not using presence because we want to maintain other empty objects such as [] and {} $stdout = stored_stdout @iteration.status = status @iteration.result = [output, "#{result || 'nil'}"].compact.join("\n") end