class MCollective::Shell

Wrapper around systemu that handles executing of system commands in a way that makes stdout, stderr and status available. Supports timeouts and sets a default sane environment.

s = Shell.new("date", opts)
s.runcommand
puts s.stdout
puts s.stderr
puts s.status.exitstatus

Options hash can have:

cwd         - the working directory the command will be run from
stdin       - a string that will be sent to stdin of the program
stdout      - a variable that will receive stdout, must support <<
stderr      - a variable that will receive stdin, must support <<
environment - the shell environment, defaults to include LC_ALL=C
              set to nil to clear the environment even of LC_ALL
timeout     - a timeout in seconds after which the subprocess is killed,
              the special value :on_thread_exit kills the subprocess
              when the invoking thread (typically the agent) has ended

Attributes

command[R]
cwd[R]
environment[R]
status[R]
stderr[R]
stdin[R]
stdout[R]
timeout[R]

Public Class Methods

new(command, options={}) click to toggle source
   # File lib/mcollective/shell.rb
27 def initialize(command, options={})
28   @environment = {"LC_ALL" => "C"}
29   @command = command
30   @status = nil
31   @stdout = ""
32   @stderr = ""
33   @stdin = nil
34   @cwd = Dir.tmpdir
35   @timeout = nil
36 
37   options.each do |opt, val|
38     case opt.to_s
39       when "stdout"
40         raise "stdout should support <<" unless val.respond_to?("<<")
41         @stdout = val
42 
43       when "stderr"
44         raise "stderr should support <<" unless val.respond_to?("<<")
45         @stderr = val
46 
47       when "stdin"
48         raise "stdin should be a String" unless val.is_a?(String)
49         @stdin = val
50 
51       when "cwd"
52         raise "Directory #{val} does not exist" unless File.directory?(val)
53         @cwd = val
54 
55       when "environment"
56         if val.nil?
57           @environment = {}
58         else
59           @environment.merge!(val.dup)
60           @environment = @environment.delete_if { |k,v| v.nil? }
61         end
62 
63       when "timeout"
64         raise "timeout should be a positive integer or the symbol :on_thread_exit symbol" unless val.eql?(:on_thread_exit) || ( val.is_a?(Integer) && val>0 )
65         @timeout = val
66     end
67   end
68 end

Public Instance Methods

runcommand() click to toggle source

Actually does the systemu call passing in the correct environment, stdout and stderr

    # File lib/mcollective/shell.rb
 71 def runcommand
 72   opts = {"env"    => @environment,
 73           "stdout" => @stdout,
 74           "stderr" => @stderr,
 75           "cwd"    => @cwd}
 76 
 77   opts["stdin"] = @stdin if @stdin
 78 
 79 
 80   thread = Thread.current
 81   # Start a double fork and exec with systemu which implies a guard thread.
 82   # If a valid timeout is configured the guard thread will terminate the
 83   # executing process and reap the pid.
 84   # If no timeout is specified the process will run to completion with the
 85   # guard thread reaping the pid on completion.
 86   @status = systemu(@command, opts) do |cid|
 87     begin
 88       if timeout.is_a?(Integer)
 89         # wait for the specified timeout
 90         sleep timeout
 91       else
 92         # sleep while the agent thread is still alive
 93         while(thread.alive?)
 94           sleep 0.1
 95         end
 96       end
 97 
 98       # if the process is still running
 99       if (Process.kill(0, cid))
100         # and a timeout was specified
101         if timeout
102           if Util.windows?
103             Process.kill('KILL', cid)
104           else
105             # Kill the process
106             Process.kill('TERM', cid)
107             sleep 2
108             Process.kill('KILL', cid) if (Process.kill(0, cid))
109           end
110         end
111         # only wait if the parent thread is dead
112         Process.waitpid(cid) unless thread.alive?
113       end
114     rescue SystemExit
115     rescue Errno::ESRCH
116     rescue Errno::ECHILD
117       Log.warn("Could not reap process '#{cid}'.")
118     rescue Exception => e
119       Log.info("Unexpected exception received while waiting for child process: #{e.class}: #{e}")
120     end
121   end
122   @status.thread.kill
123   @status
124 end