Update version according to OSCI-883
[packages/precise/mcollective.git] / lib / mcollective / shell.rb
index 0f113d51976ba97c71ca7dd0ce3ba8f01d336725..60f44179179c07514dc10495c4069734682c1e22 100644 (file)
@@ -17,9 +17,12 @@ module MCollective
   #   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
   #
   class Shell
-    attr_reader :environment, :command, :status, :stdout, :stderr, :stdin, :cwd
+    attr_reader :environment, :command, :status, :stdout, :stderr, :stdin, :cwd, :timeout
 
     def initialize(command, options={})
       @environment = {"LC_ALL" => "C"}
@@ -29,6 +32,7 @@ module MCollective
       @stderr = ""
       @stdin = nil
       @cwd = Dir.tmpdir
+      @timeout = nil
 
       options.each do |opt, val|
         case opt.to_s
@@ -54,6 +58,9 @@ module MCollective
             else
               @environment.merge!(val.dup)
             end
+          when "timeout"
+            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 )
+            @timeout = val
         end
       end
     end
@@ -67,25 +74,51 @@ module MCollective
 
       opts["stdin"] = @stdin if @stdin
 
-      # Check if the parent thread is alive. If it should die,
-      # and the process spawned by systemu is still alive,
-      # fire off a blocking waitpid and wait for the process to
-      # finish so that we can avoid zombies.
+
       thread = Thread.current
+      # Start a double fork and exec with systemu which implies a guard thread.
+      # If a valid timeout is configured the guard thread will terminate the
+      # executing process and reap the pid.
+      # If no timeout is specified the process will run to completion with the
+      # guard thread reaping the pid on completion.
       @status = systemu(@command, opts) do |cid|
         begin
-          while(thread.alive?)
-            sleep 0.1
+          if timeout.is_a?(Fixnum)
+            # wait for the specified timeout
+            sleep timeout
+          else
+            # sleep while the agent thread is still alive
+            while(thread.alive?)
+              sleep 0.1
+            end
           end
 
-          Process.waitpid(cid) if Process.getpgid(cid)
+          # if the process is still running
+          if (Process.kill(0, cid))
+            # and a timeout was specified
+            if timeout
+              if Util.windows?
+                Process.kill('KILL', cid)
+              else
+                # Kill the process
+                Process.kill('TERM', cid)
+                sleep 2
+                Process.kill('KILL', cid) if (Process.kill(0, cid))
+              end
+            end
+            # only wait if the parent thread is dead
+            Process.waitpid(cid) unless thread.alive?
+          end
         rescue SystemExit
         rescue Errno::ESRCH
         rescue Errno::ECHILD
+          Log.warn("Could not reap process '#{cid}'.")
         rescue Exception => e
           Log.info("Unexpected exception received while waiting for child process: #{e.class}: #{e}")
         end
       end
+      @status.thread.kill
+      @status
     end
   end
 end