Module: CommandRunner
- Defined in:
- lib/command_runner.rb
Constant Summary collapse
- MAX_TIME =
Time.new(2**63 -1)
Class Method Summary collapse
-
.run(*args, timeout: nil) ⇒ Object
Like IO.popen(), but block until the child completes.
Class Method Details
.run(*args, timeout: nil) ⇒ Object
Like IO.popen(), but block until the child completes. Takes an optional timeout parameter. If timeout is a number the child will be killed after that many seconds if it haven’t completed. Alternatively it can be a Hash of timeouts to actions. Each action can be a string or integer specifying the signal to send, or a Proc to execute. The Proc will be called with the child PID as argument. These examples are equivalent:
run('sleep 10', timeout: 5) # With a subshell
run(['sleep', '10'], timeout: 5) # No subshell in this one and the rest
run(['sleep', '10'], timeout: {5 => 'KILL'})
run(['sleep', '10'], timeout: {5 => Proc.new { |pid| Process.kill('KILL', pid)}})
run(['sleep', '10'], timeout: {
5 => 'KILL',
2 => Proc.new {|pid| puts "PID #{pid} getting SIGKILL in 3s"}
})
Returns a Hash with :out and :status. :out is a string with stdout and stderr merged, and :status is a Process::Status.
As a special case - if an action Proc raises an exception, the child will be killed with SIGKILL, cleaned up, and the exception rethrown to the caller of run.
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/command_runner.rb', line 29 def self.run(*args, timeout: nil) # This could be tweakable through vararg opts tick = 0.1 now = Time.now # Build deadline_sequence. A list of deadlines and corresponding actions to take if timeout if timeout.is_a? Numeric deadline_sequence = [{deadline: now + timeout, action: 'KILL'}] elsif timeout.is_a? Hash deadline_sequence = timeout.collect do |t, action| unless action.is_a? Integer or action.is_a? String or action.is_a? Proc raise "Unsupported action type '#{action.class}'. Must be Integer, String, or Proc" end unless t.is_a? Numeric raise "Unsupported timeout value '#{t}'. Must be a Numeric" end {deadline: now + t, action: action} end.sort! { |a, b| a[:deadline] <=> b[:deadline]} else raise "Unsupported type for timeout paramter: #{timeout.class}" end else deadline_sequence = [{deadline: MAX_TIME, action: 0}] end # Spawn child, merging stderr into stdout io = IO.popen(*args, :err=>[:child, :out]) data = "" # Run through all deadlines until command completes. # We could merge this block into the selecting block above, # but splitting like this saves us a Process.wait syscall per iteration. eof = false deadline_sequence.each do |point| while Time.now < point[:deadline] if Process.wait(io.pid, Process::WNOHANG) read_nonblock_safe!(io, data, tick) result = {out: data, status: $?} io.close return result elsif !eof eof = read_nonblock_safe!(io, data, tick) end end # Deadline for this point reached. Fire the action. action = point[:action] if action.is_a? String or action.is_a? Integer Process.kill(action, io.pid) elsif action.is_a? Proc begin action.call(io.pid) rescue => e # If the action block throws and error, clean up and rethrow begin Process.kill('KILL', io.pid) rescue # process already dead end Process.wait(io.pid) io.close raise e end else # Given the assertions when building the deadline_sequence this should never be reached raise "Internal error in CommandRunnerNG. Child may be left unattended!" end end # Either we didn't have a deadline, or none of the deadlines killed off the child. Process.wait(io.pid) read_nonblock_safe!(io, data, tick) result = {out: data, status: $?} io.close result end |