From 19cf4beef924916a5a18d07f3b21ef13f3dd2e56 Mon Sep 17 00:00:00 2001 From: Chris Price Date: Fri, 15 Aug 2014 12:41:33 -0700 Subject: [PATCH] (PUP-3030) Flesh out implementation of P::F::Tempfile This commit changes `Puppet::FileSystem::Tempfile` so that it no longer subclasses the built-in `Tempfile`. The goal here is to give us more control over the life cycle of the files when used on the server. The implementation is based on the Ruby 1.9.3 implementation of `Tempfile`, but strips out some things like the finalizer and methods that we don't need / use. Note that we had to also port over a few methods from `Dir` in order to support ruby 1.8.7. Relevant source code is here: https://github.com/ruby/ruby/blob/v1_9_3_547/lib/tempfile.rb https://github.com/ruby/ruby/blob/v1_9_3_547/lib/tmpdir.rb In its current incarnation the class is probably poorly named because it no longer makes any attempt to automagically unlink the file at some point in the future. I intend to submit a follow-up commit that renames the class to make this more obvious. --- lib/puppet/file_system/tempfile.rb | 181 +++++++++++++++++++++++++++-- 1 file changed, 171 insertions(+), 10 deletions(-) diff --git a/lib/puppet/file_system/tempfile.rb b/lib/puppet/file_system/tempfile.rb index 086d37f66d2..29ab6d80214 100644 --- a/lib/puppet/file_system/tempfile.rb +++ b/lib/puppet/file_system/tempfile.rb @@ -1,19 +1,180 @@ -require 'tempfile' require 'puppet/file_system' +require 'delegate' +require 'tmpdir' -class Puppet::FileSystem::Tempfile < ::Tempfile - - # Variation of Tempfile.open which ensures that the tempfile is closed and - # unlinked before returning - # +class Puppet::FileSystem::Tempfile < DelegateClass(File) # @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(identifier) - file = Puppet::FileSystem::Tempfile.new(identifier) - yield file + f = new(identifier) + yield f ensure - file.close! + f.close! + 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 -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