[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.

753ab16642
This commit is contained in:
Peter Zhu 2024-08-20 13:28:11 -04:00 committed by git
parent 41b427a264
commit a68331e703
2 changed files with 58 additions and 35 deletions

View file

@ -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
attr_reader :unlinked, :mode, :opts, :finalizer_manager
class Closer # :nodoc:
def initialize(tmpfile)
@tmpfile = tmpfile
end
def call(*args)
@tmpfile.close
end
end
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

View file

@ -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