diff options
author | Peter Zhu <[email protected]> | 2024-08-20 13:28:11 -0400 |
---|---|---|
committer | git <[email protected]> | 2024-08-20 18:07:42 +0000 |
commit | a68331e7036d7ab433778bf65eb854aabd5009c4 (patch) | |
tree | e64a0d781c4a332631a49f9fa6066e088b1c92a3 | |
parent | 41b427a2648ed2e049952450c698be917e0bb125 (diff) |
[ruby/tempfile] Add FinalizerManager to manage finalizers
As @jeremyevans pointed out for commit eb2d8b1:
> Each Tempfile instance has a separate File instance and file descriptor:
>
> t = Tempfile.new
> t.to_i # => 6
> t.dup.to_i => 7
FinalizerManager will keep track of the open File objects for the
particular file and will only unlink the file when all of the File objects
have been closed.
https://github.com/ruby/tempfile/commit/753ab16642
-rw-r--r-- | lib/tempfile.rb | 58 | ||||
-rw-r--r-- | test/test_tempfile.rb | 35 |
2 files changed, 58 insertions, 35 deletions
diff --git a/lib/tempfile.rb b/lib/tempfile.rb index 52a8d6cc9d..98706f7156 100644 --- a/lib/tempfile.rb +++ b/lib/tempfile.rb @@ -221,7 +221,6 @@ class Tempfile < DelegateClass(File) @unlinked = false @mode = mode|File::RDWR|File::CREAT|File::EXCL - @finalizer_obj = Object.new tmpfile = nil ::Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts| opts[:perm] = 0600 @@ -231,29 +230,27 @@ class Tempfile < DelegateClass(File) super(tmpfile) - define_finalizers - end - - private def define_finalizers - ObjectSpace.define_finalizer(@finalizer_obj, Closer.new(__getobj__)) - ObjectSpace.define_finalizer(@finalizer_obj, Remover.new(__getobj__.path)) + @finalizer_manager = FinalizerManager.new(__getobj__.path) + @finalizer_manager.register(self, __getobj__) end def initialize_dup(other) # :nodoc: initialize_copy_iv(other) super(other) + @finalizer_manager.register(self, __getobj__) end def initialize_clone(other) # :nodoc: initialize_copy_iv(other) super(other) + @finalizer_manager.register(self, __getobj__) end private def initialize_copy_iv(other) # :nodoc: @unlinked = other.unlinked @mode = other.mode @opts = other.opts - @finalizer_obj = other.finalizer_obj + @finalizer_manager = other.finalizer_manager end # Opens or reopens the file with mode "r+". @@ -263,8 +260,7 @@ class Tempfile < DelegateClass(File) mode = @mode & ~(File::CREAT|File::EXCL) __setobj__(File.open(__getobj__.path, mode, **@opts)) - ObjectSpace.undefine_finalizer(@finalizer_obj) - define_finalizers + @finalizer_manager.register(self, __getobj__) __getobj__ end @@ -334,9 +330,6 @@ class Tempfile < DelegateClass(File) return end - ObjectSpace.undefine_finalizer(@finalizer_obj) - ObjectSpace.define_finalizer(@finalizer_obj, Closer.new(__getobj__)) - @unlinked = true end alias delete unlink @@ -370,35 +363,32 @@ class Tempfile < DelegateClass(File) protected - attr_reader :unlinked, :mode, :opts, :finalizer_obj - - class Closer # :nodoc: - def initialize(tmpfile) - @tmpfile = tmpfile - end - - def call(*args) - @tmpfile.close - end - end + attr_reader :unlinked, :mode, :opts, :finalizer_manager - class Remover # :nodoc: + class FinalizerManager # :nodoc: def initialize(path) - @pid = Process.pid + @open_files = {} @path = path + @pid = Process.pid end - def call(*args) - return if @pid != Process.pid + def register(obj, file) + ObjectSpace.undefine_finalizer(obj) + ObjectSpace.define_finalizer(obj, self) + @open_files[obj.object_id] = file + end - $stderr.puts "removing #{@path}..." if $DEBUG + def call(object_id) + @open_files.delete(object_id).close - begin - File.unlink(@path) - rescue Errno::ENOENT + if @open_files.empty? && Process.pid == @pid + $stderr.puts "removing #{@path}..." if $DEBUG + begin + File.unlink(@path) + rescue Errno::ENOENT + end + $stderr.puts "done" if $DEBUG end - - $stderr.puts "done" if $DEBUG end end diff --git a/test/test_tempfile.rb b/test/test_tempfile.rb index ccebbf3c1a..1e40e4e480 100644 --- a/test/test_tempfile.rb +++ b/test/test_tempfile.rb @@ -3,6 +3,8 @@ require 'test/unit' require 'tempfile' class TestTempfile < Test::Unit::TestCase + LIB_TEMPFILE_RB_PATH = File.expand_path(__dir__ + "/../lib/tempfile.rb") + def initialize(*) super @tempfile = nil @@ -172,6 +174,38 @@ class TestTempfile < Test::Unit::TestCase end end unless /mswin|mingw/ =~ RUBY_PLATFORM + def test_finalizer_removes_file + assert_in_out_err(["-r#{LIB_TEMPFILE_RB_PATH}"], <<~RUBY) do |(filename,*), (error,*)| + file = Tempfile.new("foo") + puts file.path + RUBY + assert_file.not_exist?(filename) + assert_nil error + end + end + + def test_finalizer_removes_file_when_dup + assert_in_out_err(["-r#{LIB_TEMPFILE_RB_PATH}"], <<~RUBY) do |(filename,*), (error,*)| + file = Tempfile.new("foo") + file.dup + puts file.path + RUBY + assert_file.not_exist?(filename) + assert_nil error + end + end + + def test_finalizer_removes_file_when_clone + assert_in_out_err(["-r#{LIB_TEMPFILE_RB_PATH}"], <<~RUBY) do |(filename,*), (error,*)| + file = Tempfile.new("foo") + file.clone + puts file.path + RUBY + assert_file.not_exist?(filename) + assert_nil error + end + end + def test_finalizer_does_not_unlink_if_already_unlinked assert_in_out_err('-rtempfile', <<-'EOS') do |(filename,*), (error,*)| file = Tempfile.new('foo') @@ -474,5 +508,4 @@ puts Tempfile.new('foo').path assert_equal(true, t.autoclose?) } end - end |