Class: Chef::ShellOut

Inherits:
Object
  • Object
show all
Defined in:
lib/chef/shell_out.rb

Overview

Chef::ShellOut

Provides a simplified interface to shelling out yet still collecting both standard out and standard error and providing full control over environment, working directory, uid, gid, etc.

No means for passing input to the subprocess is provided, nor is there any way to inspect the output of the command as it is being read. If you need to do that, you have to use popen4 (in Chef::Mixin::Command)

Platform Support

Chef::ShellOut uses Kernel.fork() and is therefore unsuitable for Windows or jruby.

Constant Summary collapse

READ_WAIT_TIME =
0.01
READ_SIZE =
4096
DEFAULT_READ_TIMEOUT =
60
DEFAULT_ENVIRONMENT =
{'LC_ALL' => 'C'}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*command_args) ⇒ ShellOut

Arguments:

Takes a single command, or a list of command fragments. These are used as arguments to Kernel.exec. See the Kernel.exec documentation for more explanation of how arguments are evaluated. The last argument can be an options Hash.

Options:

If the last argument is a Hash, it is removed from the list of args passed to exec and used as an options hash. The following options are available:

  • user: the user the commmand should run as. if an integer is given, it is used as a uid. A string is treated as a username and resolved to a uid with Etc.getpwnam

  • group: the group the command should run as. works similarly to user

  • cwd: the directory to chdir to before running the command

  • umask: a umask to set before running the command. If given as an Integer, be sure to use two leading zeros so it’s parsed as Octal. A string will be treated as an octal integer

  • returns: one or more Integer values to use as valid exit codes for the subprocess. This only has an effect if you call error! after run_command.

  • environment: a Hash of environment variables to set before the command is run. By default, the environment will always be set to ‘LC_ALL’ => ‘C’ to prevent issues with multibyte characters in Ruby 1.8. To avoid this, use :environment => nil for no extra environment settings, or :environment => … to set other environment settings without changing the locale.

  • timeout: a Numeric value for the number of seconds to wait on the child process before raising an Exception. This is calculated as the total amount of time that ShellOut waited on the child process without receiving any output (i.e., IO.select returned nil). Default is 60 seconds. Note: the stdlib Timeout library is not used.

Examples:

Invoke find(1) to search for .rb files:

find = Chef::ShellOut.new("find . -name '*.rb'")
find.run_command
# If all went well, the results are on +stdout+
puts find.stdout
# find(1) prints diagnostic info to STDERR:
puts "error messages" + find.stderr
# Raise an exception if it didn't exit with 0
find.error!

Run a command as the www user with no extra ENV settings from /tmp

cmd = Chef::ShellOut.new("apachectl", "start", :user => 'www', :env => nil, :cwd => '/tmp')
cmd.run_command # etc.


97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/chef/shell_out.rb', line 97

def initialize(*command_args)
  @stdout, @stderr = '', ''
  @environment = DEFAULT_ENVIRONMENT
  @cwd = nil
  @valid_exit_codes = [0]

  if command_args.last.is_a?(Hash)
    parse_options(command_args.pop)
  end

  @command = command_args.size == 1 ? command_args.first : command_args
end

Instance Attribute Details

#commandObject (readonly)

Returns the value of attribute command.



46
47
48
# File 'lib/chef/shell_out.rb', line 46

def command
  @command
end

#cwdObject

Returns the value of attribute cwd.



45
46
47
# File 'lib/chef/shell_out.rb', line 45

def cwd
  @cwd
end

#environmentObject (readonly)

Returns the value of attribute environment.



46
47
48
# File 'lib/chef/shell_out.rb', line 46

def environment
  @environment
end

#execution_timeObject (readonly)

Returns the value of attribute execution_time.



48
49
50
# File 'lib/chef/shell_out.rb', line 48

def execution_time
  @execution_time
end

#groupObject

Returns the value of attribute group.



45
46
47
# File 'lib/chef/shell_out.rb', line 45

def group
  @group
end

#process_status_pipeObject (readonly)

Returns the value of attribute process_status_pipe.



52
53
54
# File 'lib/chef/shell_out.rb', line 52

def process_status_pipe
  @process_status_pipe
end

#statusObject (readonly)

Returns the value of attribute status.



50
51
52
# File 'lib/chef/shell_out.rb', line 50

def status
  @status
end

#stderrObject (readonly)

Returns the value of attribute stderr.



50
51
52
# File 'lib/chef/shell_out.rb', line 50

def stderr
  @stderr
end

#stderr_pipeObject (readonly)

Returns the value of attribute stderr_pipe.



52
53
54
# File 'lib/chef/shell_out.rb', line 52

def stderr_pipe
  @stderr_pipe
end

#stdin_pipeObject (readonly)

Returns the value of attribute stdin_pipe.



52
53
54
# File 'lib/chef/shell_out.rb', line 52

def stdin_pipe
  @stdin_pipe
end

#stdoutObject (readonly)

Returns the value of attribute stdout.



50
51
52
# File 'lib/chef/shell_out.rb', line 50

def stdout
  @stdout
end

#stdout_pipeObject (readonly)

Returns the value of attribute stdout_pipe.



52
53
54
# File 'lib/chef/shell_out.rb', line 52

def stdout_pipe
  @stdout_pipe
end

#timeoutObject



124
125
126
# File 'lib/chef/shell_out.rb', line 124

def timeout
  @timeout || DEFAULT_READ_TIMEOUT
end

#umaskObject

Returns the value of attribute umask.



46
47
48
# File 'lib/chef/shell_out.rb', line 46

def umask
  @umask
end

#userObject

Returns the value of attribute user.



45
46
47
# File 'lib/chef/shell_out.rb', line 45

def user
  @user
end

#valid_exit_codesObject

Returns the value of attribute valid_exit_codes.



45
46
47
# File 'lib/chef/shell_out.rb', line 45

def valid_exit_codes
  @valid_exit_codes
end

Instance Method Details

#error!Object

Checks the exitstatus against the set of valid_exit_codes. If exitstatus is not in the list of valid_exit_codes, calls invalid!, which raises an Exception.

Returns

nil:

always returns nil when it does not raise

Raises

Chef::Exceptions::ShellCommandFailed:

via invalid!



216
217
218
219
220
# File 'lib/chef/shell_out.rb', line 216

def error!
  unless Array(valid_exit_codes).include?(exitstatus)
    invalid!("Expected process to exit 0, but it exited with #{exitstatus}")
  end
end

#exitstatusObject



141
142
143
# File 'lib/chef/shell_out.rb', line 141

def exitstatus
  @status && @status.exitstatus
end

#format_for_exceptionObject

Creates a String showing the output of the command, including a banner showing the exact command executed. Used by invalid! to show command results when the command exited with an unexpected status.



131
132
133
134
135
136
137
138
139
# File 'lib/chef/shell_out.rb', line 131

def format_for_exception
  msg = ""
  msg << "---- Begin output of #{command} ----\n"
  msg << "STDOUT: #{stdout.strip}\n"
  msg << "STDERR: #{stderr.strip}\n"
  msg << "---- End output of #{command} ----\n"
  msg << "Ran #{command} returned #{status.exitstatus}" if status
  msg
end

#gidObject



119
120
121
122
# File 'lib/chef/shell_out.rb', line 119

def gid
  return nil unless group
  group.kind_of?(Integer) ? group : Etc.getgrnam(group.to_s).gid
end

#inspectObject



235
236
237
238
239
# File 'lib/chef/shell_out.rb', line 235

def inspect
  "<#{self.class.name}##{object_id}: command: '#@command' process_status: #{@status.inspect} " +
  "stdout: '#{stdout.strip}' stderr: '#{stderr.strip}' child_pid: #{@child_pid.inspect} " +
  "environment: #{@environment.inspect} timeout: #{timeout} user: #@user group: #@group working_dir: #@cwd >"
end

#invalid!(msg = nil) ⇒ Object

Raises a Chef::Exceptions::ShellCommandFailed exception, appending the command’s stdout, stderr, and exitstatus to the exception message.

Arguments

msg: A String to use as the basis of the exception message. The default explanation is very generic, providing a more informative message is highly encouraged.

Raises

Chef::Exceptions::ShellCommandFailed always



230
231
232
233
# File 'lib/chef/shell_out.rb', line 230

def invalid!(msg=nil)
  msg ||= "Command produced unexpected results"
  raise Chef::Exceptions::ShellCommandFailed, msg + "\n" + format_for_exception
end

#run_commandObject

Run the command, writing the command’s standard out and standard error to stdout and stderr, and saving its exit status object to status

Returns

returns self; stdout, stderr, status, and exitstatus will be populated with results of the command

Raises

  • Errno::EACCES when you are not privileged to execute the command

  • Errno::ENOENT when the command is not available on the system (or not in the current $PATH)

  • Chef::Exceptions::CommandTimeout when the command does not complete within timeout seconds (default: 60s)



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/chef/shell_out.rb', line 156

def run_command
  Chef::Log.debug("sh(#{@command})")
  @child_pid = fork_subprocess

  configure_parent_process_file_descriptors
  propagate_pre_exec_failure

  @result = nil
  @execution_time = 0

  # Ruby 1.8.7 and 1.8.6 from mid 2009 try to allocate objects during GC
  # when calling IO.select and IO#read. Some OS Vendors are not interested
  # in updating their ruby packages (Apple, *cough*) and we *have to*
  # make it work. So I give you this epic hack:
  GC.disable
  until @status
    ready = IO.select(open_pipes, nil, nil, READ_WAIT_TIME)
    unless ready
      @execution_time += READ_WAIT_TIME
      if @execution_time >= timeout && !@result
        raise Chef::Exceptions::CommandTimeout, "command timed out:\n#{format_for_exception}"
      end
    end

    if ready && ready.first.include?(child_stdout)
      read_stdout_to_buffer
    end
    if ready && ready.first.include?(child_stderr)
      read_stderr_to_buffer
    end

    unless @status
      # make one more pass to get the last of the output after the
      # child process dies
      if results = Process.waitpid2(@child_pid, Process::WNOHANG)
        @status = results.last
        redo
      end
    end
  end
  self
rescue Exception
  # do our best to kill zombies
  Process.waitpid2(@child_pid, Process::WNOHANG) rescue nil
  raise
ensure
  # no matter what happens, turn the GC back on, and hope whatever busted
  # version of ruby we're on doesn't allocate some objects during the next
  # GC run.
  GC.enable
  close_all_pipes
end

#uidObject



114
115
116
117
# File 'lib/chef/shell_out.rb', line 114

def uid
  return nil unless user
  user.kind_of?(Integer) ? user : Etc.getpwnam(user.to_s).uid
end