diff --git a/lib/puppet/file_system/uniquefile.rb b/lib/puppet/file_system/uniquefile.rb index 8e87a4efc..2b1311509 100644 --- a/lib/puppet/file_system/uniquefile.rb +++ b/lib/puppet/file_system/uniquefile.rb @@ -1,188 +1,190 @@ require 'puppet/file_system' require 'delegate' require 'tmpdir' # A class that provides `Tempfile`-like capabilities, but does not attempt to # manage the deletion of the file for you. API is identical to the # normal `Tempfile` class. # # @api public class Puppet::FileSystem::Uniquefile < DelegateClass(File) # Convenience method which ensures that the file is closed and # unlinked before returning # # @param identifier [String] additional part of generated pathname # @yieldparam file [File] the temporary file object # @return result of the passed block # @api private def self.open_tmp(identifier) f = new(identifier) yield f ensure - f.close! + if f + f.close! + end end def initialize(basename, *rest) create_tmpname(basename, *rest) do |tmpname, n, opts| mode = File::RDWR|File::CREAT|File::EXCL perm = 0600 if opts mode |= opts.delete(:mode) || 0 opts[:perm] = perm perm = nil else opts = perm end self.class.locking(tmpname) do @tmpfile = File.open(tmpname, mode, opts) @tmpname = tmpname end @mode = mode & ~(File::CREAT|File::EXCL) perm or opts.freeze @opts = opts end super(@tmpfile) end # Opens or reopens the file with mode "r+". def open @tmpfile.close if @tmpfile @tmpfile = File.open(@tmpname, @mode, @opts) __setobj__(@tmpfile) end def _close begin @tmpfile.close if @tmpfile ensure @tmpfile = nil end end protected :_close def close(unlink_now=false) if unlink_now close! else _close end end def close! _close unlink end def unlink return unless @tmpname begin File.unlink(@tmpname) rescue Errno::ENOENT rescue Errno::EACCES # may not be able to unlink on Windows; just ignore return end @tmpname = nil end alias delete unlink # Returns the full path name of the temporary file. # This will be nil if #unlink has been called. def path @tmpname end private def make_tmpname(prefix_suffix, n) case prefix_suffix when String prefix = prefix_suffix suffix = "" when Array prefix = prefix_suffix[0] suffix = prefix_suffix[1] else raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}" end t = Time.now.strftime("%Y%m%d") path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}" path << "-#{n}" if n path << suffix end def create_tmpname(basename, *rest) if opts = try_convert_to_hash(rest[-1]) opts = opts.dup if rest.pop.equal?(opts) max_try = opts.delete(:max_try) opts = [opts] else opts = [] end tmpdir, = *rest if $SAFE > 0 and tmpdir.tainted? tmpdir = '/tmp' else tmpdir ||= tmpdir() end n = nil begin path = File.expand_path(make_tmpname(basename, n), tmpdir) yield(path, n, *opts) rescue Errno::EEXIST n ||= 0 n += 1 retry if !max_try or n < max_try raise "cannot generate temporary name using `#{basename}' under `#{tmpdir}'" end path end def try_convert_to_hash(h) begin h.to_hash rescue NoMethodError => e nil end end @@systmpdir ||= defined?(Etc.systmpdir) ? Etc.systmpdir : '/tmp' def tmpdir tmp = '.' if $SAFE > 0 tmp = @@systmpdir else for dir in [ENV['TMPDIR'], ENV['TMP'], ENV['TEMP'], @@systmpdir, '/tmp'] if dir and stat = File.stat(dir) and stat.directory? and stat.writable? tmp = dir break end rescue nil end File.expand_path(tmp) end end class << self # yields with locking for +tmpname+ and returns the result of the # block. def locking(tmpname) lock = tmpname + '.lock' mkdir(lock) yield ensure rmdir(lock) if lock end def mkdir(*args) Dir.mkdir(*args) end def rmdir(*args) Dir.rmdir(*args) end end end \ No newline at end of file diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index ea0c4b186..e05339517 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,552 +1,554 @@ # A module to collect utility functions. require 'English' require 'puppet/error' require 'puppet/util/execution_stub' require 'uri' require 'pathname' require 'ostruct' require 'puppet/util/platform' require 'puppet/util/symbolic_file_mode' require 'puppet/file_system/uniquefile' require 'securerandom' module Puppet module Util require 'puppet/util/monkey_patches' require 'benchmark' # These are all for backward compatibility -- these are methods that used # to be in Puppet::Util but have been moved into external modules. require 'puppet/util/posix' extend Puppet::Util::POSIX extend Puppet::Util::SymbolicFileMode def self.activerecord_version if (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR)) ([::ActiveRecord::VERSION::MAJOR, ::ActiveRecord::VERSION::MINOR].join('.').to_f) else 0 end end # Run some code with a specific environment. Resets the environment back to # what it was at the end of the code. def self.withenv(hash) saved = ENV.to_hash hash.each do |name, val| ENV[name.to_s] = val end yield ensure ENV.clear saved.each do |name, val| ENV[name] = val end end # Execute a given chunk of code with a new umask. def self.withumask(mask) cur = File.umask(mask) begin yield ensure File.umask(cur) end end # Change the process to a different user def self.chuser if group = Puppet[:group] begin Puppet::Util::SUIDManager.change_group(group, true) rescue => detail Puppet.warning "could not change to group #{group.inspect}: #{detail}" $stderr.puts "could not change to group #{group.inspect}" # Don't exit on failed group changes, since it's # not fatal #exit(74) end end if user = Puppet[:user] begin Puppet::Util::SUIDManager.change_user(user, true) rescue => detail $stderr.puts "Could not change to user #{user}: #{detail}" exit(74) end end end # Create instance methods for each of the log levels. This allows # the messages to be a little richer. Most classes will be calling this # method. def self.logmethods(klass, useself = true) Puppet::Util::Log.eachlevel { |level| klass.send(:define_method, level, proc { |args| args = args.join(" ") if args.is_a?(Array) if useself Puppet::Util::Log.create( :level => level, :source => self, :message => args ) else Puppet::Util::Log.create( :level => level, :message => args ) end }) } end # Proxy a bunch of methods to another object. def self.classproxy(klass, objmethod, *methods) classobj = class << klass; self; end methods.each do |method| classobj.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # Proxy a bunch of methods to another object. def self.proxy(klass, objmethod, *methods) methods.each do |method| klass.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end def benchmark(*args) msg = args.pop level = args.pop object = nil if args.empty? if respond_to?(level) object = self else object = Puppet end else object = args.pop end raise Puppet::DevError, "Failed to provide level to :benchmark" unless level unless level == :none or object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to #{level}" end # Only benchmark if our log level is high enough if level != :none and Puppet::Util::Log.sendlevel?(level) seconds = Benchmark.realtime { yield } object.send(level, msg + (" in %0.2f seconds" % seconds)) return seconds else yield end end module_function :benchmark # Resolve a path for an executable to the absolute path. This tries to behave # in the same manner as the unix `which` command and uses the `PATH` # environment variable. # # @api public # @param bin [String] the name of the executable to find. # @return [String] the absolute path to the found executable. def which(bin) if absolute_path?(bin) return bin if FileTest.file? bin and FileTest.executable? bin else ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir| begin dest = File.expand_path(File.join(dir, bin)) rescue ArgumentError => e # if the user's PATH contains a literal tilde (~) character and HOME is not set, we may get # an ArgumentError here. Let's check to see if that is the case; if not, re-raise whatever error # was thrown. if e.to_s =~ /HOME/ and (ENV['HOME'].nil? || ENV['HOME'] == "") # if we get here they have a tilde in their PATH. We'll issue a single warning about this and then # ignore this path element and carry on with our lives. Puppet::Util::Warnings.warnonce("PATH contains a ~ character, and HOME is not set; ignoring PATH element '#{dir}'.") elsif e.to_s =~ /doesn't exist|can't find user/ # ...otherwise, we just skip the non-existent entry, and do nothing. Puppet::Util::Warnings.warnonce("Couldn't expand PATH containing a ~ character; ignoring PATH element '#{dir}'.") else raise end else if Puppet.features.microsoft_windows? && File.extname(dest).empty? exts = ENV['PATHEXT'] exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD] exts.each do |ext| destext = File.expand_path(dest + ext) return destext if FileTest.file? destext and FileTest.executable? destext end end return dest if FileTest.file? dest and FileTest.executable? dest end end end nil end module_function :which # Determine in a platform-specific way whether a path is absolute. This # defaults to the local platform if none is specified. # # Escape once for the string literal, and once for the regex. slash = '[\\\\/]' label = '[^\\\\/]+' AbsolutePathWindows = %r!^(?:(?:[A-Z]:#{slash})|(?:#{slash}#{slash}#{label}#{slash}#{label})|(?:#{slash}#{slash}\?#{slash}#{label}))!io AbsolutePathPosix = %r!^/! def absolute_path?(path, platform=nil) # Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard # library uses that to test what platform it's on. Normally in Puppet we # would use Puppet.features.microsoft_windows?, but this method needs to # be called during the initialization of features so it can't depend on # that. platform ||= Puppet::Util::Platform.windows? ? :windows : :posix regex = case platform when :windows AbsolutePathWindows when :posix AbsolutePathPosix else raise Puppet::DevError, "unknown platform #{platform} in absolute_path" end !! (path =~ regex) end module_function :absolute_path? # Convert a path to a file URI def path_to_uri(path) return unless path params = { :scheme => 'file' } if Puppet.features.microsoft_windows? path = path.gsub(/\\/, '/') if unc = /^\/\/([^\/]+)(\/.+)/.match(path) params[:host] = unc[1] path = unc[2] elsif path =~ /^[a-z]:\//i path = '/' + path end end params[:path] = URI.escape(path) begin URI::Generic.build(params) rescue => detail raise Puppet::Error, "Failed to convert '#{path}' to URI: #{detail}", detail.backtrace end end module_function :path_to_uri # Get the path component of a URI def uri_to_path(uri) return unless uri.is_a?(URI) path = URI.unescape(uri.path) if Puppet.features.microsoft_windows? and uri.scheme == 'file' if uri.host path = "//#{uri.host}" + path # UNC else path.sub!(/^\//, '') end end path end module_function :uri_to_path def safe_posix_fork(stdin=$stdin, stdout=$stdout, stderr=$stderr, &block) child_pid = Kernel.fork do $stdin.reopen(stdin) $stdout.reopen(stdout) $stderr.reopen(stderr) 3.upto(256){|fd| IO::new(fd).close rescue nil} block.call if block end child_pid end module_function :safe_posix_fork def memory unless defined?(@pmap) @pmap = which('pmap') end if @pmap %x{#{@pmap} #{Process.pid}| grep total}.chomp.sub(/^\s*total\s+/, '').sub(/K$/, '').to_i else 0 end end def symbolizehash(hash) newhash = {} hash.each do |name, val| name = name.intern if name.respond_to? :intern newhash[name] = val end newhash end module_function :symbolizehash # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } seconds end module_function :memory, :thinmark # Because IO#binread is only available in 1.9 def binread(file) Puppet.deprecation_warning("Puppet::Util.binread is deprecated. Read the file without this method as it will be removed in a future version.") File.open(file, 'rb') { |f| f.read } end module_function :binread # utility method to get the current call stack and format it to a human-readable string (which some IDEs/editors # will recognize as links to the line numbers in the trace) def self.pretty_backtrace(backtrace = caller(1)) backtrace.collect do |line| _, path, rest = /^(.*):(\d+.*)$/.match(line).to_a # If the path doesn't exist - like in one test, and like could happen in # the world - we should just tolerate it and carry on. --daniel 2012-09-05 # Also, if we don't match, just include the whole line. if path path = Pathname(path).realpath rescue path "#{path}:#{rest}" else line end end.join("\n") end # Replace a file, securely. This takes a block, and passes it the file # handle of a file open for writing. Write the replacement content inside # the block and it will safely replace the target file. # # This method will make no changes to the target file until the content is # successfully written and the block returns without raising an error. # # As far as possible the state of the existing file, such as mode, is # preserved. This works hard to avoid loss of any metadata, but will result # in an inode change for the file. # # Arguments: `filename`, `default_mode` # # The filename is the file we are going to replace. # # The default_mode is the mode to use when the target file doesn't already # exist; if the file is present we copy the existing mode/owner/group values # across. The default_mode can be expressed as an octal integer, a numeric string (ie '0664') # or a symbolic file mode. DEFAULT_POSIX_MODE = 0644 DEFAULT_WINDOWS_MODE = nil def replace_file(file, default_mode, &block) raise Puppet::DevError, "replace_file requires a block" unless block_given? if default_mode unless valid_symbolic_mode?(default_mode) raise Puppet::DevError, "replace_file default_mode: #{default_mode} is invalid" end mode = symbolic_mode_to_int(normalize_symbolic_mode(default_mode)) else if Puppet.features.microsoft_windows? mode = DEFAULT_WINDOWS_MODE else mode = DEFAULT_POSIX_MODE end end begin file = Puppet::FileSystem.pathname(file) tempfile = Puppet::FileSystem::Uniquefile.new(Puppet::FileSystem.basename_string(file), Puppet::FileSystem.dir_string(file)) # Set properties of the temporary file before we write the content, because # Tempfile doesn't promise to be safe from reading by other people, just # that it avoids races around creating the file. # # Our Windows emulation is pretty limited, and so we have to carefully # and specifically handle the platform, which has all sorts of magic. # So, unlike Unix, we don't pre-prep security; we use the default "quite # secure" tempfile permissions instead. Magic happens later. if !Puppet.features.microsoft_windows? # Grab the current file mode, and fall back to the defaults. effective_mode = if Puppet::FileSystem.exist?(file) stat = Puppet::FileSystem.lstat(file) tempfile.chown(stat.uid, stat.gid) stat.mode else mode end if effective_mode # We only care about the bottom four slots, which make the real mode, # and not the rest of the platform stat call fluff and stuff. tempfile.chmod(effective_mode & 07777) end end # OK, now allow the caller to write the content of the file. yield tempfile # Now, make sure the data (which includes the mode) is safe on disk. tempfile.flush begin tempfile.fsync rescue NotImplementedError # fsync may not be implemented by Ruby on all platforms, but # there is absolutely no recovery path if we detect that. So, we just # ignore the return code. # # However, don't be fooled: that is accepting that we are running in # an unsafe fashion. If you are porting to a new platform don't stub # that out. end tempfile.close if Puppet.features.microsoft_windows? # Windows ReplaceFile needs a file to exist, so touch handles this if !Puppet::FileSystem.exist?(file) Puppet::FileSystem.touch(file) if mode Puppet::Util::Windows::Security.set_mode(mode, Puppet::FileSystem.path_string(file)) end end # Yes, the arguments are reversed compared to the rename in the rest # of the world. Puppet::Util::Windows::File.replace_file(FileSystem.path_string(file), tempfile.path) else File.rename(tempfile.path, Puppet::FileSystem.path_string(file)) end ensure # in case an error occurred before we renamed the temp file, make sure it # gets deleted - tempfile.close! + if tempfile + tempfile.close! + end end # Ideally, we would now fsync the directory as well, but Ruby doesn't # have support for that, and it doesn't matter /that/ much... # Return something true, and possibly useful. file end module_function :replace_file # Executes a block of code, wrapped with some special exception handling. Causes the ruby interpreter to # exit if the block throws an exception. # # @api public # @param [String] message a message to log if the block fails # @param [Integer] code the exit code that the ruby interpreter should return if the block fails # @yield def exit_on_fail(message, code = 1) yield # First, we need to check and see if we are catching a SystemExit error. These will be raised # when we daemonize/fork, and they do not necessarily indicate a failure case. rescue SystemExit => err raise err # Now we need to catch *any* other kind of exception, because we may be calling third-party # code (e.g. webrick), and we have no idea what they might throw. rescue Exception => err ## NOTE: when debugging spec failures, these two lines can be very useful #puts err.inspect #puts Puppet::Util.pretty_backtrace(err.backtrace) Puppet.log_exception(err, "Could not #{message}: #{err}") Puppet::Util::Log.force_flushqueue() exit(code) end module_function :exit_on_fail def deterministic_rand(seed,max) if defined?(Random) == 'constant' && Random.class == Class Random.new(seed).rand(max).to_s else srand(seed) result = rand(max).to_s srand() result end end module_function :deterministic_rand ####################################################################################################### # Deprecated methods relating to process execution; these have been moved to Puppet::Util::Execution ####################################################################################################### def execpipe(command, failonfail = true, &block) Puppet.deprecation_warning("Puppet::Util.execpipe is deprecated; please use Puppet::Util::Execution.execpipe") Puppet::Util::Execution.execpipe(command, failonfail, &block) end module_function :execpipe def execfail(command, exception) Puppet.deprecation_warning("Puppet::Util.execfail is deprecated; please use Puppet::Util::Execution.execfail") Puppet::Util::Execution.execfail(command, exception) end module_function :execfail def execute(*args) Puppet.deprecation_warning("Puppet::Util.execute is deprecated; please use Puppet::Util::Execution.execute") Puppet::Util::Execution.execute(*args) end module_function :execute end end require 'puppet/util/errors' require 'puppet/util/methodhelper' require 'puppet/util/metaid' require 'puppet/util/classgen' require 'puppet/util/docs' require 'puppet/util/execution' require 'puppet/util/logging' require 'puppet/util/package' require 'puppet/util/warnings' diff --git a/lib/puppet/util/execution.rb b/lib/puppet/util/execution.rb index 22d6ba08a..556aba50e 100644 --- a/lib/puppet/util/execution.rb +++ b/lib/puppet/util/execution.rb @@ -1,327 +1,327 @@ require 'puppet/file_system/uniquefile' module Puppet require 'rbconfig' require 'puppet/error' # A command failed to execute. # @api public class ExecutionFailure < Puppet::Error end end # This module defines methods for execution of system commands. It is intented for inclusion # in classes that needs to execute system commands. # @api public module Puppet::Util::Execution # This is the full output from a process. The object itself (a String) is the # stdout of the process. # # @api public class ProcessOutput < String # @return [Integer] The exit status of the process # @api public attr_reader :exitstatus # @api private def initialize(value,exitstatus) super(value) @exitstatus = exitstatus end end # 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. # # @param command [String, Array] the command to execute as one string, # or as parts in an array. The parts of the array are joined with one # separating space between each entry when converting to the command line # string to execute. # @param failonfail [Boolean] (true) if the execution should fail with # Exception on failure or not. # @yield [pipe] to a block executing a subprocess # @yieldparam pipe [IO] the opened pipe # @yieldreturn [String] the output to return # @raise [Puppet::ExecutionFailure] if the executed chiled process did not # exit with status == 0 and `failonfail` is `true`. # @return [String] a string with the output from the subprocess executed by # the given block # # @see Kernel#open for `mode` values # @api public 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 open("| #{command_str} 2>&1") do |pipe| yield pipe end end if failonfail && exitstatus != 0 raise Puppet::ExecutionFailure, output end output end def self.exitstatus $CHILD_STATUS.exitstatus end private_class_method :exitstatus # Wraps execution of {execute} with mapping of exception to given exception (and output as argument). # @raise [exception] under same conditions as {execute}, but raises the given `exception` with the output as argument # @return (see execute) # @api public def self.execfail(command, exception) output = execute(command) return output rescue Puppet::ExecutionFailure raise exception, output, exception.backtrace end # Default empty options for {execute} NoOptionsSpecified = {} # Executes the desired command, and return the status and output. # def execute(command, options) # @param command [Array, String] the command to execute. If it is # an Array the first element should be the executable and the rest of the # elements should be the individual arguments to that executable. # @param options [Hash] a Hash of options # @option options [Boolean] :failonfail if this value is set to true, then this method will raise an error if the # command is not executed successfully. # @option options [Integer, String] :uid (nil) the user id of the user that the process should be run as # @option options [Integer, String] :gid (nil) the group id of the group that the process should be run as # @option options [Boolean] :combine sets whether or not to combine stdout/stderr in the output # @option options [String] :stdinfile (nil) sets a file that can be used for stdin. Passing a string for stdin is not currently # supported. # @option options [Boolean] :squelch (true) if true, ignore stdout / stderr completely. # @option options [Boolean] :override_locale (true) by default (and if this option is set to true), we will temporarily override # the user/system locale to "C" (via environment variables LANG and LC_*) while we are executing the command. # This ensures that the output of the command will be formatted consistently, making it predictable for parsing. # Passing in a value of false for this option will allow the command to be executed using the user/system locale. # @option options [Hash<{String => String}>] :custom_environment ({}) a hash of key/value pairs to set as environment variables for the duration # of the command. # @return [Puppet::Util::Execution::ProcessOutput] output as specified by options # @raise [Puppet::ExecutionFailure] if the executed chiled process did not exit with status == 0 and `failonfail` is # `true`. # @note 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. # @comment See commits efe9a833c and d32d7f30 # @api public # def self.execute(command, options = 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 default_options = { :failonfail => NoOptionsSpecified.equal?(options), :uid => nil, :gid => nil, :combine => NoOptionsSpecified.equal?(options), :stdinfile => nil, :squelch => false, :override_locale => true, :custom_environment => {}, } options = default_options.merge(options) if command.is_a?(Array) command = command.flatten.map(&:to_s) str = command.join(" ") elsif command.is_a?(String) str = command end if respond_to? :debug debug "Executing '#{str}'" else Puppet.debug "Executing '#{str}'" end null_file = Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' begin stdin = File.open(options[:stdinfile] || null_file, 'r') stdout = options[:squelch] ? File.open(null_file, 'w') : Puppet::FileSystem::Uniquefile.new('puppet') stderr = options[:combine] ? stdout : File.open(null_file, 'w') exec_args = [command, options, stdin, stdout, stderr] if execution_stub = Puppet::Util::ExecutionStub.current_value return execution_stub.call(*exec_args) elsif Puppet.features.posix? child_pid = execute_posix(*exec_args) exit_status = Process.waitpid2(child_pid).last.exitstatus elsif Puppet.features.microsoft_windows? process_info = execute_windows(*exec_args) begin exit_status = Puppet::Util::Windows::Process.wait_process(process_info.process_handle) ensure FFI::WIN32.CloseHandle(process_info.process_handle) FFI::WIN32.CloseHandle(process_info.thread_handle) end end [stdin, stdout, stderr].each {|io| io.close rescue nil} # read output in if required unless options[:squelch] output = wait_for_output(stdout) Puppet.warning "Could not get output" unless output end if options[:failonfail] and exit_status != 0 raise Puppet::ExecutionFailure, "Execution of '#{str}' returned #{exit_status}: #{output.strip}" end ensure - if !options[:squelch] + if !options[:squelch] && stdout # if we opened a temp file for stdout, we need to clean it up. stdout.close! end end Puppet::Util::Execution::ProcessOutput.new(output || '', exit_status) end # 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) # @return [String] the path to the Ruby executable # @api private # def self.ruby_path() File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT']). sub(/.*\s.*/m, '"\&"') end # Because some modules provide their own version of this method. class << self alias util_execute execute end # This is private method. # @comment see call to private_class_method after method definition # @api private # def self.execute_posix(command, options, stdin, stdout, stderr) child_pid = Puppet::Util.safe_posix_fork(stdin, stdout, stderr) do # We can't just call Array(command), and rely on it returning # things like ['foo'], when passed ['foo'], because # Array(command) will call command.to_a internally, which when # given a string can end up doing Very Bad Things(TM), such as # turning "/tmp/foo;\r\n /bin/echo" into ["/tmp/foo;\r\n", " /bin/echo"] command = [command].flatten Process.setsid begin Puppet::Util::SUIDManager.change_privileges(options[:uid], options[:gid], true) # if the caller has requested that we override locale environment variables, if (options[:override_locale]) then # loop over them and clear them Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |name| ENV.delete(name) } # set LANG and LC_ALL to 'C' so that the command will have consistent, predictable output # it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in # a forked process. ENV['LANG'] = 'C' ENV['LC_ALL'] = 'C' end # unset all of the user-related environment variables so that different methods of starting puppet # (automatic start during boot, via 'service', via /etc/init.d, etc.) won't have unexpected side # effects relating to user / home dir environment vars. # it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in # a forked process. Puppet::Util::POSIX::USER_ENV_VARS.each { |name| ENV.delete(name) } options[:custom_environment] ||= {} Puppet::Util.withenv(options[:custom_environment]) do Kernel.exec(*command) end rescue => detail Puppet.log_exception(detail, "Could not execute posix command: #{detail}") exit!(1) end end child_pid end private_class_method :execute_posix # This is private method. # @comment see call to private_class_method after method definition # @api private # def self.execute_windows(command, options, stdin, stdout, stderr) command = command.map do |part| part.include?(' ') ? %Q["#{part.gsub(/"/, '\"')}"] : part end.join(" ") if command.is_a?(Array) options[:custom_environment] ||= {} Puppet::Util.withenv(options[:custom_environment]) do Puppet::Util::Windows::Process.execute(command, options, stdin, stdout, stderr) end end private_class_method :execute_windows # This is private method. # @comment see call to private_class_method after method definition # @api private # def self.wait_for_output(stdout) # Make sure the file's actually been written. This is basically a race # condition, and is probably a horrible way to handle it, but, well, oh # well. # (If this method were treated as private / inaccessible from outside of this file, we shouldn't have to worry # about a race condition because all of the places that we call this from are preceded by a call to "waitpid2", # meaning that the processes responsible for writing the file have completed before we get here.) 2.times do |try| if Puppet::FileSystem.exist?(stdout.path) stdout.open begin return stdout.read ensure stdout.close stdout.unlink end else time_to_sleep = try / 2.0 Puppet.warning "Waiting for output; will sleep #{time_to_sleep} seconds" sleep(time_to_sleep) end end nil end private_class_method :wait_for_output end