2 # Wrapper around systemu that handles executing of system commands
3 # in a way that makes stdout, stderr and status available. Supports
4 # timeouts and sets a default sane environment.
6 # s = Shell.new("date", opts)
10 # puts s.status.exitstatus
12 # Options hash can have:
14 # cwd - the working directory the command will be run from
15 # stdin - a string that will be sent to stdin of the program
16 # stdout - a variable that will receive stdout, must support <<
17 # stderr - a variable that will receive stdin, must support <<
18 # environment - the shell environment, defaults to include LC_ALL=C
19 # set to nil to clear the environment even of LC_ALL
20 # timeout - a timeout in seconds after which the subprocess is killed,
21 # the special value :on_thread_exit kills the subprocess
22 # when the invoking thread (typically the agent) has ended
25 attr_reader :environment, :command, :status, :stdout, :stderr, :stdin, :cwd, :timeout
27 def initialize(command, options={})
28 @environment = {"LC_ALL" => "C"}
37 options.each do |opt, val|
40 raise "stdout should support <<" unless val.respond_to?("<<")
44 raise "stderr should support <<" unless val.respond_to?("<<")
48 raise "stdin should be a String" unless val.is_a?(String)
52 raise "Directory #{val} does not exist" unless File.directory?(val)
59 @environment.merge!(val.dup)
62 raise "timeout should be a positive integer or the symbol :on_thread_exit symbol" unless val.eql?(:on_thread_exit) || ( val.is_a?(Fixnum) && val>0 )
68 # Actually does the systemu call passing in the correct environment, stdout and stderr
70 opts = {"env" => @environment,
75 opts["stdin"] = @stdin if @stdin
78 thread = Thread.current
79 # Start a double fork and exec with systemu which implies a guard thread.
80 # If a valid timeout is configured the guard thread will terminate the
81 # executing process and reap the pid.
82 # If no timeout is specified the process will run to completion with the
83 # guard thread reaping the pid on completion.
84 @status = systemu(@command, opts) do |cid|
86 if timeout.is_a?(Fixnum)
87 # wait for the specified timeout
90 # sleep while the agent thread is still alive
96 # if the process is still running
97 if (Process.kill(0, cid))
98 # and a timeout was specified
101 Process.kill('KILL', cid)
104 Process.kill('TERM', cid)
106 Process.kill('KILL', cid) if (Process.kill(0, cid))
109 # only wait if the parent thread is dead
110 Process.waitpid(cid) unless thread.alive?
115 Log.warn("Could not reap process '#{cid}'.")
116 rescue Exception => e
117 Log.info("Unexpected exception received while waiting for child process: #{e.class}: #{e}")