Update version according to OSCI-856
[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   #
21   class Shell
22     attr_reader :environment, :command, :status, :stdout, :stderr, :stdin, :cwd
23
24     def initialize(command, options={})
25       @environment = {"LC_ALL" => "C"}
26       @command = command
27       @status = nil
28       @stdout = ""
29       @stderr = ""
30       @stdin = nil
31       @cwd = Dir.tmpdir
32
33       options.each do |opt, val|
34         case opt.to_s
35           when "stdout"
36             raise "stdout should support <<" unless val.respond_to?("<<")
37             @stdout = val
38
39           when "stderr"
40             raise "stderr should support <<" unless val.respond_to?("<<")
41             @stderr = val
42
43           when "stdin"
44             raise "stdin should be a String" unless val.is_a?(String)
45             @stdin = val
46
47           when "cwd"
48             raise "Directory #{val} does not exist" unless File.directory?(val)
49             @cwd = val
50
51           when "environment"
52             if val.nil?
53               @environment = {}
54             else
55               @environment.merge!(val.dup)
56             end
57         end
58       end
59     end
60
61     # Actually does the systemu call passing in the correct environment, stdout and stderr
62     def runcommand
63       opts = {"env"    => @environment,
64               "stdout" => @stdout,
65               "stderr" => @stderr,
66               "cwd"    => @cwd}
67
68       opts["stdin"] = @stdin if @stdin
69
70       # Check if the parent thread is alive. If it should die,
71       # and the process spawned by systemu is still alive,
72       # fire off a blocking waitpid and wait for the process to
73       # finish so that we can avoid zombies.
74       thread = Thread.current
75       @status = systemu(@command, opts) do |cid|
76         begin
77           while(thread.alive?)
78             sleep 0.1
79           end
80
81           Process.waitpid(cid) if Process.getpgid(cid)
82         rescue SystemExit
83         rescue Errno::ESRCH
84         rescue Errno::ECHILD
85         rescue Exception => e
86           Log.info("Unexpected exception received while waiting for child process: #{e.class}: #{e}")
87         end
88       end
89     end
90   end
91 end