Skip to content

Commit dafabf9

Browse files
authored
Fix Tempfile#{dup,clone}
Instead of storing the delegate in @tmpfile, use __getobj__, since delegate library already handles dup/clone for that. Copy the unlinked, mode, and opts instance variables to the returned object when using dup/clone. Split the close/unlink finalizer into two finalizers. The close finalizer always closes when any Tempfile instance is GCed, since each Tempfile instance uses a separate file descriptor. The unlink finalizer unlinks only when the original and all duped/cloned Tempfiles are GCed, since all share the same path. For Tempfile#open, undefine the close finalizer after closing the current file, the redefine the close finalizer with the new file. Fixes [Bug #19441]
1 parent d6ddf78 commit dafabf9

File tree

2 files changed

+70
-18
lines changed

2 files changed

+70
-18
lines changed

lib/tempfile.rb

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -152,26 +152,49 @@ def initialize(basename="", tmpdir=nil, mode: 0, **options)
152152

153153
@unlinked = false
154154
@mode = mode|File::RDWR|File::CREAT|File::EXCL
155+
@finalizer_obj = Object.new
156+
tmpfile = nil
155157
::Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
156158
opts[:perm] = 0600
157-
@tmpfile = File.open(tmpname, @mode, **opts)
159+
tmpfile = File.open(tmpname, @mode, **opts)
158160
@opts = opts.freeze
159161
end
160-
ObjectSpace.define_finalizer(self, Remover.new(@tmpfile))
162+
ObjectSpace.define_finalizer(@finalizer_obj, Remover.new(tmpfile.path))
163+
ObjectSpace.define_finalizer(self, Closer.new(tmpfile))
161164

162-
super(@tmpfile)
165+
super(tmpfile)
166+
end
167+
168+
def initialize_dup(other)
169+
initialize_copy_iv(other)
170+
super(other)
171+
ObjectSpace.define_finalizer(self, Closer.new(__getobj__))
172+
end
173+
174+
def initialize_clone(other)
175+
initialize_copy_iv(other)
176+
super(other)
177+
ObjectSpace.define_finalizer(self, Closer.new(__getobj__))
178+
end
179+
180+
private def initialize_copy_iv(other)
181+
@unlinked = other.unlinked
182+
@mode = other.mode
183+
@opts = other.opts
184+
@finalizer_obj = other.finalizer_obj
163185
end
164186

165187
# Opens or reopens the file with mode "r+".
166188
def open
167189
_close
190+
ObjectSpace.undefine_finalizer(self)
168191
mode = @mode & ~(File::CREAT|File::EXCL)
169-
@tmpfile = File.open(@tmpfile.path, mode, **@opts)
170-
__setobj__(@tmpfile)
192+
__setobj__(File.open(__getobj__.path, mode, **@opts))
193+
ObjectSpace.define_finalizer(self, Closer.new(__getobj__))
171194
end
172195

173196
def _close # :nodoc:
174-
@tmpfile.close
197+
__getobj__.close
175198
end
176199
protected :_close
177200

@@ -228,57 +251,70 @@ def close!
228251
def unlink
229252
return if @unlinked
230253
begin
231-
File.unlink(@tmpfile.path)
254+
File.unlink(__getobj__.path)
232255
rescue Errno::ENOENT
233256
rescue Errno::EACCES
234257
# may not be able to unlink on Windows; just ignore
235258
return
236259
end
237-
ObjectSpace.undefine_finalizer(self)
260+
ObjectSpace.undefine_finalizer(@finalizer_obj)
238261
@unlinked = true
239262
end
240263
alias delete unlink
241264

242265
# Returns the full path name of the temporary file.
243266
# This will be nil if #unlink has been called.
244267
def path
245-
@unlinked ? nil : @tmpfile.path
268+
@unlinked ? nil : __getobj__.path
246269
end
247270

248271
# Returns the size of the temporary file. As a side effect, the IO
249272
# buffer is flushed before determining the size.
250273
def size
251-
if !@tmpfile.closed?
252-
@tmpfile.size # File#size calls rb_io_flush_raw()
274+
if !__getobj__.closed?
275+
__getobj__.size # File#size calls rb_io_flush_raw()
253276
else
254-
File.size(@tmpfile.path)
277+
File.size(__getobj__.path)
255278
end
256279
end
257280
alias length size
258281

259282
# :stopdoc:
260283
def inspect
261-
if @tmpfile.closed?
284+
if __getobj__.closed?
262285
"#<#{self.class}:#{path} (closed)>"
263286
else
264287
"#<#{self.class}:#{path}>"
265288
end
266289
end
267290

268-
class Remover # :nodoc:
291+
protected
292+
293+
attr_reader :unlinked, :mode, :opts, :finalizer_obj
294+
295+
class Closer # :nodoc:
269296
def initialize(tmpfile)
270-
@pid = Process.pid
271297
@tmpfile = tmpfile
272298
end
273299

300+
def call(*args)
301+
@tmpfile.close
302+
end
303+
end
304+
305+
class Remover # :nodoc:
306+
def initialize(path)
307+
@pid = Process.pid
308+
@path = path
309+
end
310+
274311
def call(*args)
275312
return if @pid != Process.pid
276313

277-
$stderr.puts "removing #{@tmpfile.path}..." if $DEBUG
314+
$stderr.puts "removing #{@path}..." if $DEBUG
278315

279-
@tmpfile.close
280316
begin
281-
File.unlink(@tmpfile.path)
317+
File.unlink(@path)
282318
rescue Errno::ENOENT
283319
end
284320

test/test_tempfile.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,22 @@ def test_basename_with_suffix
6363
assert_match(/\.txt$/, File.basename(t.path))
6464
end
6565

66+
def test_dup
67+
t = tempfile
68+
t2 = t.dup
69+
t2.close
70+
assert_equal true, t2.closed?
71+
assert_equal false, t.closed?
72+
end
73+
74+
def test_clone
75+
t = tempfile
76+
t2 = t.clone
77+
t2.close
78+
assert_equal true, t2.closed?
79+
assert_equal false, t.closed?
80+
end
81+
6682
def test_unlink
6783
t = tempfile("foo")
6884
path = t.path

0 commit comments

Comments
 (0)