Module: Puppet::Util::Execution
- Included in:
- Provider::Exec, Diff
- Defined in:
- lib/puppet/util/execution.rb
Overview
This module defines methods for execution of system commands. It is intended for inclusion in classes that needs to execute system commands.
Defined Under Namespace
Classes: ProcessOutput
Constant Summary collapse
- NoOptionsSpecified =
Default empty options for execute
{}
Class Method Summary collapse
-
.execpipe(command, failonfail = true) {|pipe| ... } ⇒ String
The command can be a simple string, which is executed as-is, or an Array, which is treated as a set of command arguments to pass through.
-
.execute(command, options = NoOptionsSpecified) ⇒ Puppet::Util::Execution::ProcessOutput
(also: util_execute)
Executes the desired command, and return the status and output.
-
.ruby_path ⇒ String
private
Returns the path to the ruby executable (available via Config object, even if it’s not in the PATH… so this is slightly safer than just using Puppet::Util.which).
Class Method Details
.execpipe(command, failonfail = true) {|pipe| ... } ⇒ String
The command can be a simple string, which is executed as-is, or an Array, which is treated as a set of command arguments to pass through.
In either case, the command is passed directly to the shell, STDOUT and STDERR are connected together, and STDOUT will be streamed to the yielded pipe.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/puppet/util/execution.rb', line 59 def self.execpipe(command, failonfail = true) # Paste together an array with spaces. We used to paste directly # together, no spaces, which made for odd invocations; the user had to # include whitespace between arguments. # # Having two spaces is really not a big drama, since this passes to the # shell anyhow, while no spaces makes for a small developer cost every # time this is invoked. --daniel 2012-02-13 command_str = command.respond_to?(:join) ? command.join(' ') : command if respond_to? :debug debug "Executing '#{command_str}'" else Puppet.debug { "Executing '#{command_str}'" } end # force the run of the command with # the user/system locale to "C" (via environment variables LANG and LC_*) # it enables to have non localized output for some commands and therefore # a predictable output english_env = ENV.to_hash.merge({ 'LANG' => 'C', 'LC_ALL' => 'C' }) output = Puppet::Util.withenv(english_env) do # We are intentionally using 'pipe' with open to launch a process open("| #{command_str} 2>&1") do |pipe| # rubocop:disable Security/Open yield pipe end end if failonfail && exitstatus != 0 raise Puppet::ExecutionFailure, output.to_s end output end |
.execute(command, options = NoOptionsSpecified) ⇒ Puppet::Util::Execution::ProcessOutput Also known as: util_execute
Unfortunately, the default behavior for failonfail and combine (since 0.22.4 and 0.24.7, respectively) depend on whether options are specified or not. If specified, then failonfail and combine default to false (even when the options specified are neither failonfail nor combine). If no options are specified, then failonfail and combine default to true.
Executes the desired command, and return the status and output. def execute(command, options)
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 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 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 |
# File 'lib/puppet/util/execution.rb', line 138 def self.execute(command, = NoOptionsSpecified) # specifying these here rather than in the method signature to allow callers to pass in a partial # set of overrides without affecting the default values for options that they don't pass in = { :failonfail => NoOptionsSpecified.equal?(), :uid => nil, :gid => nil, :combine => NoOptionsSpecified.equal?(), :stdinfile => nil, :squelch => false, :override_locale => true, :custom_environment => {}, :sensitive => false, :suppress_window => false, } = .merge() case command when Array command = command.flatten.map(&:to_s) command_str = command.join(" ") when String command_str = command end # do this after processing 'command' array or string command_str = '[redacted]' if [:sensitive] user_log_s = ''.dup if [:uid] user_log_s << " uid=#{[:uid]}" end if [:gid] user_log_s << " gid=#{[:gid]}" end if user_log_s != '' user_log_s.prepend(' with') end if respond_to? :debug debug "Executing#{user_log_s}: '#{command_str}'" else Puppet.debug { "Executing#{user_log_s}: '#{command_str}'" } end null_file = Puppet::Util::Platform.windows? ? 'NUL' : '/dev/null' cwd = [:cwd] if cwd && !Puppet::FileSystem.directory?(cwd) raise ArgumentError, _("Working directory %{cwd} does not exist!") % { cwd: cwd } end begin stdin = Puppet::FileSystem.open([:stdinfile] || null_file, nil, 'r') # On Windows, continue to use the file-based approach to avoid breaking people's existing # manifests. If they use a script that doesn't background cleanly, such as # `start /b ping 127.0.0.1`, we couldn't handle it with pipes as there's no non-blocking # read available. if [:squelch] stdout = Puppet::FileSystem.open(null_file, nil, 'w') elsif Puppet.features.posix? reader, stdout = IO.pipe else stdout = Puppet::FileSystem::Uniquefile.new('puppet') end stderr = [:combine] ? stdout : Puppet::FileSystem.open(null_file, nil, 'w') exec_args = [command, , stdin, stdout, stderr] output = ''.dup # We close stdin/stdout/stderr immediately after fork/exec as they're no longer needed by # this process. In most cases they could be closed later, but when `stdout` is the "writer" # pipe we must close it or we'll never reach eof on the `reader` pipe. execution_stub = Puppet::Util::ExecutionStub.current_value if execution_stub child_pid = execution_stub.call(*exec_args) [stdin, stdout, stderr].each { |io| begin io.close rescue nil end } return child_pid elsif Puppet.features.posix? child_pid = nil begin child_pid = execute_posix(*exec_args) [stdin, stdout, stderr].each { |io| begin io.close rescue nil end } if [:squelch] exit_status = Process.waitpid2(child_pid).last.exitstatus else # Use non-blocking read to check for data. After each attempt, # check whether the child is done. This is done in case the child # forks and inherits stdout, as happens in `foo &`. # If we encounter EOF, though, then switch to a blocking wait for # the child; after EOF, IO.select will never block and the loop # below will use maximum CPU available. wait_flags = Process::WNOHANG until results = Process.waitpid2(child_pid, wait_flags) # rubocop:disable Lint/AssignmentInCondition # If not done, wait for data to read with a timeout # This timeout is selected to keep activity low while waiting on # a long process, while not waiting too long for the pathological # case where stdout is never closed. ready = IO.select([reader], [], [], 0.1) begin output << reader.read_nonblock(4096) if ready rescue Errno::EAGAIN rescue EOFError wait_flags = 0 end end # Read any remaining data. Allow for but don't expect EOF. begin loop do output << reader.read_nonblock(4096) end rescue Errno::EAGAIN rescue EOFError end # Force to external encoding to preserve prior behavior when reading a file. # Wait until after reading all data so we don't encounter corruption when # reading part of a multi-byte unicode character if default_external is UTF-8. output.force_encoding(Encoding.default_external) exit_status = results.last.exitstatus end child_pid = nil rescue Timeout::Error => e # NOTE: For Ruby 2.1+, an explicit Timeout::Error class has to be # passed to Timeout.timeout in order for there to be something for # this block to rescue. unless child_pid.nil? Process.kill(:TERM, child_pid) # Spawn a thread to reap the process if it dies. Thread.new { Process.waitpid(child_pid) } end raise e end elsif Puppet::Util::Platform.windows? process_info = execute_windows(*exec_args) begin [stdin, stderr].each { |io| begin io.close rescue nil end } exit_status = Puppet::Util::Windows::Process.wait_process(process_info.process_handle) # read output in if required unless [:squelch] output = wait_for_output(stdout) Puppet.warning _("Could not get output") unless output end ensure FFI::WIN32.CloseHandle(process_info.process_handle) FFI::WIN32.CloseHandle(process_info.thread_handle) end end if [:failonfail] and exit_status != 0 raise Puppet::ExecutionFailure, _("Execution of '%{str}' returned %{exit_status}: %{output}") % { str: command_str, exit_status: exit_status, output: output.strip } end ensure # Make sure all handles are closed in case an exception was thrown attempting to execute. [stdin, stdout, stderr].each { |io| begin io.close rescue nil end } unless [:squelch] # if we opened a pipe, we need to clean it up. reader.close if reader stdout.close! if stdout && Puppet::Util::Platform.windows? end end Puppet::Util::Execution::ProcessOutput.new(output || '', exit_status) end |
.ruby_path ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns the path to the ruby executable (available via Config object, even if it’s not in the PATH… so this is slightly safer than just using Puppet::Util.which)
338 339 340 341 342 |
# File 'lib/puppet/util/execution.rb', line 338 def self.ruby_path File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT']) .sub(/.*\s.*/m, '"\&"') end |