Class: MCollective::Shell

Inherits:
Object
  • Object
show all
Defined in:
lib/mcollective/shell.rb

Overview

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

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(command, options = {}) ⇒ Shell

Returns a new instance of Shell.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/mcollective/shell.rb', line 27

def initialize(command, options={})
  @environment = {"LC_ALL" => "C"}
  @command = command
  @status = nil
  @stdout = ""
  @stderr = ""
  @stdin = nil
  @cwd = Dir.tmpdir
  @timeout = nil

  options.each do |opt, val|
    case opt.to_s
    when "stdout"
      raise "stdout should support <<" unless val.respond_to?("<<")

      @stdout = val

    when "stderr"
      raise "stderr should support <<" unless val.respond_to?("<<")

      @stderr = val

    when "stdin"
      raise "stdin should be a String" unless val.is_a?(String)

      @stdin = val

    when "cwd"
      raise "Directory #{val} does not exist" unless File.directory?(val)

      @cwd = val

    when "environment"
      if val.nil?
        @environment = {}
      else
        @environment.merge!(val.dup)
        @environment = @environment.delete_if { |_k, v| v.nil? }
      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?(Integer) && val > 0)

      @timeout = val
    end
  end
end

Instance Attribute Details

#commandObject (readonly)

Returns the value of attribute command.



25
26
27
# File 'lib/mcollective/shell.rb', line 25

def command
  @command
end

#cwdObject (readonly)

Returns the value of attribute cwd.



25
26
27
# File 'lib/mcollective/shell.rb', line 25

def cwd
  @cwd
end

#environmentObject (readonly)

Returns the value of attribute environment.



25
26
27
# File 'lib/mcollective/shell.rb', line 25

def environment
  @environment
end

#statusObject (readonly)

Returns the value of attribute status.



25
26
27
# File 'lib/mcollective/shell.rb', line 25

def status
  @status
end

#stderrObject (readonly)

Returns the value of attribute stderr.



25
26
27
# File 'lib/mcollective/shell.rb', line 25

def stderr
  @stderr
end

#stdinObject (readonly)

Returns the value of attribute stdin.



25
26
27
# File 'lib/mcollective/shell.rb', line 25

def stdin
  @stdin
end

#stdoutObject (readonly)

Returns the value of attribute stdout.



25
26
27
# File 'lib/mcollective/shell.rb', line 25

def stdout
  @stdout
end

#timeoutObject (readonly)

Returns the value of attribute timeout.



25
26
27
# File 'lib/mcollective/shell.rb', line 25

def timeout
  @timeout
end

Instance Method Details

#runcommandObject

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



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/mcollective/shell.rb', line 76

def runcommand
  opts = {"env" => @environment,
          "stdout" => @stdout,
          "stderr" => @stderr,
          "cwd" => @cwd}

  opts["stdin"] = @stdin if @stdin

  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
      if timeout.is_a?(Integer)
        # wait for the specified timeout
        sleep timeout
      else
        # sleep while the agent thread is still alive
        sleep 0.1 while thread.alive?
      end

      # 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) # rubocop:disable Metrics/BlockNesting
          end
        end
        # only wait if the parent thread is dead
        Process.waitpid(cid) unless thread.alive?
      end
    rescue SystemExit # rubocop:disable Lint/SuppressedException
    rescue Errno::ESRCH # rubocop:disable Lint/SuppressedException
    rescue Errno::ECHILD
      Log.warn("Could not reap process '#{cid}'.")
    rescue Exception => e # rubocop:disable Lint/RescueException
      Log.info("Unexpected exception received while waiting for child process: #{e.class}: #{e}")
    end
  end
  @status.thread.kill
  @status
end