Module: CommandRunner
- Defined in:
- lib/command_runner.rb
Defined Under Namespace
Classes: CommandInstance
Constant Summary collapse
- MAX_TIME =
Time.new(2**63 - 1)
- DEFAULT_OPTIONS =
{:err=>[:child, :out]}
Class Method Summary collapse
-
.create(*args, timeout: nil, environment: {}, allowed_sub_commands: [], debug_log: nil, options: DEFAULT_OPTIONS) ⇒ Object
Create a helper instance to launch a command with a given configuration.
-
.run(*args, timeout: nil, environment: {}, debug_log: nil, options: DEFAULT_OPTIONS) ⇒ Object
Like IO.popen(), but block until the child completes.
-
.set_debug_log!(logger) ⇒ Object
Log all command line invocations to a logger object responding to :puts.
Class Method Details
.create(*args, timeout: nil, environment: {}, allowed_sub_commands: [], debug_log: nil, options: DEFAULT_OPTIONS) ⇒ Object
Create a helper instance to launch a command with a given configuration. Invoke the command with the run() method. The configuration given to create() can be overriden on each invocation of run().
The run() method of the helper instance must be invoked with a
Examples: git = CommandRunner.create([‘sudo’, ‘git’], timeout: 10, allowed_sub_commands: [:commit, :pull, :push]) git.run(:pull, ‘origin’, ‘master’) git.run(:pull, ‘origin’, ‘master’, timeout: 2) # override default timeout of 10 git.run(:status) # will raise an error because :status is not in list of allowed commands
160 161 162 |
# File 'lib/command_runner.rb', line 160 def self.create(*args, timeout: nil, environment: {}, allowed_sub_commands: [], debug_log: nil, options: DEFAULT_OPTIONS) CommandInstance.new(args, timeout, environment, allowed_sub_commands, debug_log, ) end |
.run(*args, timeout: nil, environment: {}, debug_log: nil, options: DEFAULT_OPTIONS) ⇒ Object
Like IO.popen(), but block until the child completes.
For convenience allows you to pass > 1 string args without a boxing array, to execute a command with arguments without a subshell. See examples below.
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) # Without subshell. Convenience API to avoid array boxing as below
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} geting SIGKILL in 3s"}
})
Takes an optional environment parameter (a Hash). The environment is populated with the keys/values of this parameter.
Returns a Hash with :out, :pid, and :status. :out is a string with stdout and stderr merged, and :status is a Process::Status. The :pid field is the the PID the child process had when it ran.
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.
By default stderr in the child is merged into its stdout. You can do any kind of advanced stream mapping by overriding the default options hash. The options are passed to Kernel.spawn. See ruby-doc.org/core-2.2.3/Kernel.html#method-i-spawn for details.
Fx. redirecting stderr to /dev/null would look like:
run('ls', 'nosuchfile', options: {:err => "/dev/null"})
All Kernel.spawn features, like setting umasks, process group, and are supported through the options hash.
Debugging: To help debugging your app you can set the debug_log parameter. It can be any old object responding to :puts. Fx. $stderr, $stdout, or the write end of an IO.pipe. CommandRunnerNG will put some info about all process start, stop, and timeouts here. To enable debug logging for all commands call CommandRunner.set_debug_log!($stderr) (or with some other object responding to :puts).
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 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/command_runner.rb', line 55 def self.run(*args, timeout: nil, environment: {}, debug_log: nil, options: DEFAULT_OPTIONS) if debug_log.nil? debug_log = @@global_debug_log end # If args is an array of strings, allow that as a shorthand for [arg1, arg2, arg3] if args.length > 1 && args.all? {|arg| arg.is_a? String} args = [args] end # 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(environment, *args, ) debug_log.puts("CommandRunnerNG spawn: args=#{args}, timeout=#{timeout}, options: #{}, PID: #{io.pid}") if debug_log.respond_to?(:puts) 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 => $?, pid: io.pid} debug_log.puts("CommandRunnerNG exit: PID: #{io.pid}, code: #{result[:status].exitstatus}") if debug_log.respond_to?(:puts) 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] debug_log.puts("CommandRunnerNG timeout: PID: #{io.pid}, action: #{action}") if debug_log.respond_to?(:puts) 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 => $?, pid: io.pid} debug_log.puts("CommandRunnerNG exit: PID: #{io.pid}, code: #{result[:status].exitstatus}") if debug_log.respond_to?(:puts) io.close result end |
.set_debug_log!(logger) ⇒ Object
Log all command line invocations to a logger object responding to :puts. Set to nil to disable. Setting the :debug_log keyword argument on individual invocations of CommandRunner.run() overrides this value.
166 167 168 169 170 171 172 |
# File 'lib/command_runner.rb', line 166 def self.set_debug_log!(logger) if logger.respond_to?(:puts) || logger.nil? @@global_debug_log = logger else raise ArgumentError.new("Logger '#{logger}' not responding to :puts") end end |