Update version according to OSCI-883
[packages/precise/mcollective.git] / lib / mcollective / shell.rb
1 module MCollective
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.
5   #
6   #   s = Shell.new("date", opts)
7   #   s.runcommand
8   #   puts s.stdout
9   #   puts s.stderr
10   #   puts s.status.exitstatus
11   #
12   # Options hash can have:
13   #
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
23   #
24   class Shell
25     attr_reader :environment, :command, :status, :stdout, :stderr, :stdin, :cwd, :timeout
26
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             end
61           when "timeout"
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 )
63             @timeout = val
64         end
65       end
66     end
67
68     # Actually does the systemu call passing in the correct environment, stdout and stderr
69     def runcommand
70       opts = {"env"    => @environment,
71               "stdout" => @stdout,
72               "stderr" => @stderr,
73               "cwd"    => @cwd}
74
75       opts["stdin"] = @stdin if @stdin
76
77
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|
85         begin
86           if timeout.is_a?(Fixnum)
87             # wait for the specified timeout
88             sleep timeout
89           else
90             # sleep while the agent thread is still alive
91             while(thread.alive?)
92               sleep 0.1
93             end
94           end
95
96           # if the process is still running
97           if (Process.kill(0, cid))
98             # and a timeout was specified
99             if timeout
100               if Util.windows?
101                 Process.kill('KILL', cid)
102               else
103                 # Kill the process
104                 Process.kill('TERM', cid)
105                 sleep 2
106                 Process.kill('KILL', cid) if (Process.kill(0, cid))
107               end
108             end
109             # only wait if the parent thread is dead
110             Process.waitpid(cid) unless thread.alive?
111           end
112         rescue SystemExit
113         rescue Errno::ESRCH
114         rescue Errno::ECHILD
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}")
118         end
119       end
120       @status.thread.kill
121       @status
122     end
123   end
124 end