Skip to content

Commit 57a022f

Browse files
mktemp/dir: delete temp files on exit by default (#32851)
The main change here is that the non-higher-order versions of mktemp/dir will now by default put the temp files/dirs that they create into a temp cleanup list which will be deleted upon process exit. Instead of putting lots of atexit handlers in the list, this uses a single handler and keeps a list of temporary paths to cleanup. If the list gets long (> 1024) it will be scanned for already-deleted paths when adding a new path to the cleanup list. To avoid running too frequently, it doubles the "too many paths" threshold after this. If cleanup fails for higher order `mktemp() do ... end` forms, they will also put their temp files/dirs in the cleanup list but marked for "asap" deletion. Every time the above mechanism for cleaning out the temp cleanup list scans for already-deleted temp files, this will try again to delete these temp files.
1 parent a6275b6 commit 57a022f

File tree

5 files changed

+244
-19
lines changed

5 files changed

+244
-19
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Standard library changes
6363
* `nothing` can now be `print`ed, and interplated into strings etc. as the string `"nothing"`. It is still not permitted to be interplated into Cmds (i.e. ``echo `$(nothing)` `` will still error without running anything.) ([#32148])
6464
* When `open` is called with a function, command, and keyword argument (e.g. ```open(`ls`, read=true) do f ...```)
6565
it now correctly throws a `ProcessFailedException` like other similar calls ([#32193]).
66+
* `mktemp` and `mktempdir` now try, by default, to remove temporary paths they create before the process exits ([#32851]).
6667

6768
#### Libdl
6869

base/file.jl

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,34 @@ function tempdir()
448448
end
449449
end
450450

451+
const TEMP_CLEANUP_MIN = Ref(1024)
452+
const TEMP_CLEANUP_MAX = Ref(1024)
453+
const TEMP_CLEANUP = Dict{String,Bool}()
454+
const TEMP_CLEANUP_LOCK = ReentrantLock()
455+
456+
function temp_cleanup_later(path::AbstractString; asap::Bool=false)
457+
lock(TEMP_CLEANUP_LOCK)
458+
TEMP_CLEANUP[path] = asap
459+
if length(TEMP_CLEANUP) > TEMP_CLEANUP_MAX[]
460+
temp_cleanup_purge(false)
461+
TEMP_CLEANUP_MAX[] = max(TEMP_CLEANUP_MIN[], 2*length(TEMP_CLEANUP))
462+
end
463+
unlock(TEMP_CLEANUP_LOCK)
464+
return nothing
465+
end
466+
467+
function temp_cleanup_purge(all::Bool=true)
468+
need_gc = Sys.iswindows()
469+
for (path, asap) in TEMP_CLEANUP
470+
if (all || asap) && ispath(path)
471+
need_gc && GC.gc(true)
472+
need_gc = false
473+
rm(path, recursive=true, force=true)
474+
end
475+
!ispath(path) && delete!(TEMP_CLEANUP, path)
476+
end
477+
end
478+
451479
const temp_prefix = "jl_"
452480

453481
if Sys.iswindows()
@@ -466,8 +494,9 @@ function _win_tempname(temppath::AbstractString, uunique::UInt32)
466494
return transcode(String, tname)
467495
end
468496

469-
function mktemp(parent=tempdir())
497+
function mktemp(parent::AbstractString=tempdir(); cleanup::Bool=true)
470498
filename = _win_tempname(parent, UInt32(0))
499+
cleanup && temp_cleanup_later(filename)
471500
return (filename, Base.open(filename, "r+"))
472501
end
473502

@@ -498,10 +527,11 @@ function tempname()
498527
end
499528

500529
# Create and return the name of a temporary file along with an IOStream
501-
function mktemp(parent=tempdir())
530+
function mktemp(parent::AbstractString=tempdir(); cleanup::Bool=true)
502531
b = joinpath(parent, temp_prefix * "XXXXXX")
503532
p = ccall(:mkstemp, Int32, (Cstring,), b) # modifies b
504533
systemerror(:mktemp, p == -1)
534+
cleanup && temp_cleanup_later(b)
505535
return (b, fdio(p, true))
506536
end
507537

@@ -524,22 +554,25 @@ created. The path is likely to be unique, but this cannot be guaranteed.
524554
tempname()
525555

526556
"""
527-
mktemp(parent=tempdir())
557+
mktemp(parent=tempdir(); cleanup=true) -> (path, io)
528558
529-
Return `(path, io)`, where `path` is the path of a new temporary file in `parent` and `io`
530-
is an open file object for this path.
559+
Return `(path, io)`, where `path` is the path of a new temporary file in `parent`
560+
and `io` is an open file object for this path. The `cleanup` option controls whether
561+
the temporary file is automatically deleted when the process exits.
531562
"""
532563
mktemp(parent)
533564

534565
"""
535-
mktempdir(parent=tempdir(); prefix=$(repr(temp_prefix)))
566+
mktempdir(parent=tempdir(); prefix=$(repr(temp_prefix)), cleanup=true) -> path
536567
537568
Create a temporary directory in the `parent` directory with a name
538569
constructed from the given prefix and a random suffix, and return its path.
539570
Additionally, any trailing `X` characters may be replaced with random characters.
540-
If `parent` does not exist, throw an error.
571+
If `parent` does not exist, throw an error. The `cleanup` option controls whether
572+
the temporary directory is automatically deleted when the process exits.
541573
"""
542-
function mktempdir(parent=tempdir(); prefix=temp_prefix)
574+
function mktempdir(parent::AbstractString=tempdir();
575+
prefix::AbstractString=temp_prefix, cleanup::Bool=true)
543576
if isempty(parent) || occursin(path_separator_re, parent[end:end])
544577
# append a path_separator only if parent didn't already have one
545578
tpath = "$(parent)$(prefix)XXXXXX"
@@ -558,6 +591,7 @@ function mktempdir(parent=tempdir(); prefix=temp_prefix)
558591
end
559592
path = unsafe_string(ccall(:jl_uv_fs_t_path, Cstring, (Ptr{Cvoid},), req))
560593
ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{Cvoid},), req)
594+
cleanup && temp_cleanup_later(path)
561595
return path
562596
finally
563597
Libc.free(req)
@@ -571,17 +605,18 @@ end
571605
Apply the function `f` to the result of [`mktemp(parent)`](@ref) and remove the
572606
temporary file upon completion.
573607
"""
574-
function mktemp(fn::Function, parent=tempdir())
575-
(tmp_path, tmp_io) = mktemp(parent)
608+
function mktemp(fn::Function, parent::AbstractString=tempdir())
609+
(tmp_path, tmp_io) = mktemp(parent, cleanup=false)
576610
try
577611
fn(tmp_path, tmp_io)
578612
finally
579-
# TODO: should we call GC.gc() first on error, to make it much more likely that `rm` succeeds?
580613
try
581614
close(tmp_io)
582615
rm(tmp_path)
583616
catch ex
584617
@error "mktemp cleanup" _group=:file exception=(ex, catch_backtrace())
618+
# might be possible to remove later
619+
temp_cleanup_later(tmp_path, asap=true)
585620
end
586621
end
587622
end
@@ -592,16 +627,18 @@ end
592627
Apply the function `f` to the result of [`mktempdir(parent; prefix)`](@ref) and remove the
593628
temporary directory all of its contents upon completion.
594629
"""
595-
function mktempdir(fn::Function, parent=tempdir(); prefix=temp_prefix)
596-
tmpdir = mktempdir(parent; prefix=prefix)
630+
function mktempdir(fn::Function, parent::AbstractString=tempdir();
631+
prefix::AbstractString=temp_prefix)
632+
tmpdir = mktempdir(parent; prefix=prefix, cleanup=false)
597633
try
598634
fn(tmpdir)
599635
finally
600-
# TODO: should we call GC.gc() first on error, to make it much more likely that `rm` succeeds?
601636
try
602637
rm(tmpdir, recursive=true)
603638
catch ex
604639
@error "mktempdir cleanup" _group=:file exception=(ex, catch_backtrace())
640+
# might be possible to remove later
641+
temp_cleanup_later(tmpdir, asap=true)
605642
end
606643
end
607644
end

base/initdefs.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ end
293293

294294
## atexit: register exit hooks ##
295295

296-
const atexit_hooks = []
296+
const atexit_hooks = Callable[Filesystem.temp_cleanup_purge]
297297

298298
"""
299299
atexit(f)

doc/src/base/file.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ Base.Filesystem.rm
2929
Base.Filesystem.touch
3030
Base.Filesystem.tempname
3131
Base.Filesystem.tempdir
32-
Base.Filesystem.mktemp(::Any)
33-
Base.Filesystem.mktemp(::Function, ::Any)
34-
Base.Filesystem.mktempdir(::Any)
35-
Base.Filesystem.mktempdir(::Function, ::Any)
32+
Base.Filesystem.mktemp(::AbstractString)
33+
Base.Filesystem.mktemp(::Function, ::AbstractString)
34+
Base.Filesystem.mktempdir(::AbstractString)
35+
Base.Filesystem.mktempdir(::Function, ::AbstractString)
3636
Base.Filesystem.isblockdev
3737
Base.Filesystem.ischardev
3838
Base.Filesystem.isdir

test/file.jl

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,193 @@ if !Sys.iswindows()
4848
cd(pwd_)
4949
end
5050

51+
child_eval(code::String) = eval(Meta.parse(readchomp(`$(Base.julia_cmd()) -E $code`)))
52+
53+
@testset "mktemp/dir basic cleanup" begin
54+
# mktemp without cleanup
55+
t = child_eval("t = mktemp(cleanup=false)[1]; @assert isfile(t); t")
56+
@test isfile(t)
57+
rm(t, force=true)
58+
@test !ispath(t)
59+
# mktemp with cleanup
60+
t = child_eval("t = mktemp()[1]; @assert isfile(t); t")
61+
@test !ispath(t)
62+
# mktempdir without cleanup
63+
t = child_eval("t = mktempdir(cleanup=false); touch(joinpath(t, \"file.txt\")); t")
64+
@test isfile("$t/file.txt")
65+
rm(t, recursive=true, force=true)
66+
@test !ispath(t)
67+
# mktempdir with cleanup
68+
t = child_eval("t = mktempdir(); touch(joinpath(t, \"file.txt\")); t")
69+
@test !ispath(t)
70+
end
71+
72+
import Base.Filesystem: TEMP_CLEANUP_MIN, TEMP_CLEANUP_MAX, TEMP_CLEANUP
73+
74+
function with_temp_cleanup(f::Function, n::Int)
75+
SAVE_TEMP_CLEANUP_MIN = TEMP_CLEANUP_MIN[]
76+
SAVE_TEMP_CLEANUP_MAX = TEMP_CLEANUP_MAX[]
77+
SAVE_TEMP_CLEANUP = copy(TEMP_CLEANUP)
78+
empty!(TEMP_CLEANUP)
79+
TEMP_CLEANUP_MIN[] = n
80+
TEMP_CLEANUP_MAX[] = n
81+
try f()
82+
finally
83+
Sys.iswindows() && GC.gc(true)
84+
for t in keys(TEMP_CLEANUP)
85+
rm(t, recursive=true, force=true)
86+
end
87+
copy!(TEMP_CLEANUP, SAVE_TEMP_CLEANUP)
88+
TEMP_CLEANUP_MAX[] = SAVE_TEMP_CLEANUP_MAX
89+
TEMP_CLEANUP_MIN[] = SAVE_TEMP_CLEANUP_MIN
90+
end
91+
end
92+
93+
function mktempfile(; cleanup=true)
94+
(file, io) = mktemp(cleanup=cleanup)
95+
Sys.iswindows() && close(io)
96+
return file
97+
end
98+
99+
@testset "mktemp/dir cleanup list purging" begin
100+
n = 12 # cleanup min & max
101+
@assert n % 2 == n % 3 == 0 # otherwise tests won't work
102+
with_temp_cleanup(n) do
103+
@test length(TEMP_CLEANUP) == 0
104+
@test TEMP_CLEANUP_MAX[] == n
105+
# for n mktemps, no purging is triggered
106+
temps = String[]
107+
for i = 1:n
108+
t = i % 2 == 0 ? mktempfile() : mktempdir()
109+
push!(temps, t)
110+
@test ispath(t)
111+
@test length(TEMP_CLEANUP) ==
112+
@test TEMP_CLEANUP_MAX[] == n
113+
# delete 1/3 of the temp paths
114+
i % 3 == 0 && rm(t, recursive=true, force=true)
115+
end
116+
# without cleanup no purge is triggered
117+
t = mktempdir(cleanup=false)
118+
@test isdir(t)
119+
@test length(TEMP_CLEANUP) == n
120+
@test TEMP_CLEANUP_MAX[] == n
121+
rm(t, recursive=true, force=true)
122+
# purge triggered by next mktemp with cleanup
123+
t = mktempfile()
124+
push!(temps, t)
125+
n′ = 2n÷3 + 1
126+
@test 2n′ > n
127+
@test isfile(t)
128+
@test length(TEMP_CLEANUP) == n′
129+
@test TEMP_CLEANUP_MAX[] == 2n′
130+
# remove all temp files
131+
for t in temps
132+
rm(t, recursive=true, force=true)
133+
end
134+
# for n′ mktemps, no purging is triggered
135+
for i = 1:n′
136+
t = i % 2 == 0 ? mktempfile() : mktempdir()
137+
push!(temps, t)
138+
@test ispath(t)
139+
@test length(TEMP_CLEANUP) == n′ + i
140+
@test TEMP_CLEANUP_MAX[] == 2n′
141+
# delete 2/3 of the temp paths
142+
i % 3 != 0 && rm(t, recursive=true, force=true)
143+
end
144+
# without cleanup no purge is triggered
145+
t = mktempfile(cleanup=false)
146+
@test isfile(t)
147+
@test length(TEMP_CLEANUP) == 2n′
148+
@test TEMP_CLEANUP_MAX[] == 2n′
149+
rm(t, force=true)
150+
# purge triggered by next mktemp
151+
t = mktempdir()
152+
push!(temps, t)
153+
n′′ = n′÷3 + 1
154+
@test 2n′′ < n
155+
@test isdir(t)
156+
@test length(TEMP_CLEANUP) == n′′
157+
@test TEMP_CLEANUP_MAX[] == n
158+
end
159+
end
160+
161+
no_error_logging(f::Function) =
162+
Base.CoreLogging.with_logger(f, Base.CoreLogging.NullLogger())
163+
164+
@testset "hof mktemp/dir when cleanup is prevented" begin
165+
d = mktempdir()
166+
with_temp_cleanup(3) do
167+
@test length(TEMP_CLEANUP) == 0
168+
@test TEMP_CLEANUP_MAX[] == 3
169+
local t, f
170+
temps = String[]
171+
# mktemp is normally cleaned up on completion
172+
mktemp(d) do path, _
173+
@test isfile(path)
174+
t = path
175+
end
176+
@test !ispath(t)
177+
@test length(TEMP_CLEANUP) == 0
178+
@test TEMP_CLEANUP_MAX[] == 3
179+
# mktemp when cleanup is prevented
180+
no_error_logging() do
181+
mktemp(d) do path, _
182+
@test isfile(path)
183+
f = open(path) # make undeletable on Windows
184+
chmod(d, 0o400) # make undeletable on UNIX
185+
t = path
186+
end
187+
end
188+
chmod(d, 0o700)
189+
close(f)
190+
@test isfile(t)
191+
@test length(TEMP_CLEANUP) == 1
192+
@test TEMP_CLEANUP_MAX[] == 3
193+
push!(temps, t)
194+
# mktempdir is normally cleaned up on completion
195+
mktempdir(d) do path
196+
@test isdir(path)
197+
t = path
198+
end
199+
@test !ispath(t)
200+
@test length(TEMP_CLEANUP) == 1
201+
@test TEMP_CLEANUP_MAX[] == 3
202+
# mktempdir when cleanup is prevented
203+
no_error_logging() do
204+
mktempdir(d) do path
205+
@test isdir(path)
206+
# make undeletable on Windows:
207+
f = open(joinpath(path, "file.txt"), "w+")
208+
chmod(d, 0o400) # make undeletable on UNIX
209+
t = path
210+
end
211+
end
212+
chmod(d, 0o700)
213+
close(f)
214+
@test isdir(t)
215+
@test length(TEMP_CLEANUP) == 2
216+
@test TEMP_CLEANUP_MAX[] == 3
217+
push!(temps, t)
218+
# make one more temp file
219+
t = mktemp()[1]
220+
@test isfile(t)
221+
@test length(TEMP_CLEANUP) == 3
222+
@test TEMP_CLEANUP_MAX[] == 3
223+
# nothing has been deleted yet
224+
for t in temps
225+
@test ispath(t)
226+
end
227+
# another temp file triggers purge
228+
t = mktempdir()
229+
@test isdir(t)
230+
@test length(TEMP_CLEANUP) == 2
231+
@test TEMP_CLEANUP_MAX[] == 4
232+
# now all the temps are gone
233+
for t in temps
234+
@test !ispath(t)
235+
end
236+
end
237+
end
51238

52239
#######################################################################
53240
# This section tests some of the features of the stat-based file info #

0 commit comments

Comments
 (0)