Skip to content

Commit 3e2a4ed

Browse files
Liozouvtjnashstaticfloat
authored
Change delayed delete mechanism to prevent cross-drive mv failure for in-use DLL (#59635)
On Windows, during precompilation of package `A`, a DLL file is generated and may replace the existing one. If `A` is already loaded in the julia session however, the corresponding soon-to-be-obsolete DLL cannot be removed while julia is running. Currently, this problem is solved by moving the old DLL in a special "julia_delayed_deletes" folder, which will be cleaned in a later julia session by `Pkg.gc()`. However, moving an in-use DLL is only permitted on the same drive, and the "julia_delayed_deletes" is located in the `tempdir`, a.k.a. on a fixed drive, say `C:`. Thus, if the `DEPOT_PATH` points to a ".julia" in another drive, say `D:`, any precompilation occuring on an already loaded package will fail. This is what happens in #59589, actually resulting in an infinite loop that bloats up both memory and disk. @staticfloat had actually predicted that such an issue could occur in #53456 (comment). This PR fixes #59589 by changing the "delayed deleting" mechanism: instead of moving the old DLLs to a fixed folder, they are renamed in their initial folder and recorded in a list stored at a fixed location. Upon `Pkg.gc()`, the listed obsolete files will be deleted (JuliaLang/Pkg.jl#4392). It also prevents any similar infinite loops by cutting the `rm -> mv -> rm` cycle into `rm -> rename`. ~I also removed the private argument `allow_delayed_delete` from `rm`, which is only called in by [Pkg](https://github.com/JuliaLang/Pkg.jl/blob/7344e2656475261a83a6bd37d9d4cc1e7dcf5f0d/src/API.jl#L1127) but does not appear to be useful.~ EDIT: current state is #59635 (comment) --------- Co-authored-by: Jameson Nash <[email protected]> Co-authored-by: Elliot Saba <[email protected]>
1 parent f6dde2e commit 3e2a4ed

File tree

9 files changed

+132
-78
lines changed

9 files changed

+132
-78
lines changed

Compiler/test/special_loading.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
# Only run when testing Base compiler
44
if Base.identify_package("Compiler") === nothing
5-
mktempdir() do dir
5+
include(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "test", "tempdepot.jl"))
6+
mkdepottempdir() do dir
67
withenv("JULIA_DEPOT_PATH" => dir * (Sys.iswindows() ? ";" : ":"), "JULIA_LOAD_PATH" => nothing) do
78
cd(joinpath(@__DIR__, "CompilerLoadingTest")) do
89
@test success(pipeline(`$(Base.julia_cmd()[1]) --startup-file=no --project=. compiler_loading_test.jl`; stdout, stderr))

base/file.jl

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,9 @@ function mkpath(path::AbstractString; mode::Integer = 0o777)
253253
return path
254254
end
255255

256-
# Files that were requested to be deleted but can't be by the current process
257-
# i.e. loaded DLLs on Windows
258-
delayed_delete_dir() = joinpath(tempdir(), "julia_delayed_deletes")
256+
# Files that were requested to be deleted but can't be by the current process,
257+
# i.e. loaded DLLs on Windows, are listed in the directory below
258+
delayed_delete_ref() = joinpath(tempdir(), "julia_delayed_deletes_ref")
259259

260260
"""
261261
rm(path::AbstractString; force::Bool=false, recursive::Bool=false)
@@ -288,13 +288,7 @@ function rm(path::AbstractString; force::Bool=false, recursive::Bool=false, allo
288288
force && err.code==Base.UV_ENOENT && return
289289
@static if Sys.iswindows()
290290
if allow_delayed_delete && err.code==Base.UV_EACCES && endswith(path, ".dll")
291-
# Loaded DLLs cannot be deleted on Windows, even with posix delete mode
292-
# but they can be moved. So move out to allow the dir to be deleted.
293-
# Pkg.gc() cleans up this dir when possible
294-
dir = mkpath(delayed_delete_dir())
295-
temp_path = tempname(dir, cleanup = false, suffix = string("_", basename(path)))
296-
@debug "Could not delete DLL most likely because it is loaded, moving to tempdir" path temp_path
297-
mv(path, temp_path)
291+
delayed_delete_dll(path)
298292
return
299293
end
300294
end
@@ -330,6 +324,22 @@ function rm(path::AbstractString; force::Bool=false, recursive::Bool=false, allo
330324
end
331325

332326

327+
# Loaded DLLs cannot be deleted on Windows, even with posix delete mode but they can be renamed.
328+
# delayed_delete_dll(path) does so temporarily, until later cleanup by Pkg.gc().
329+
function delayed_delete_dll(path)
330+
# in-use DLL must be kept on the same drive
331+
temp_path = tempname(abspath(dirname(path)); cleanup=false, suffix=string("_", basename(path)))
332+
@debug "Could not delete DLL most likely because it is loaded, moving to a temporary path" path temp_path
333+
mkpath(delayed_delete_ref())
334+
io = last(mktemp(delayed_delete_ref(); cleanup=false))
335+
try
336+
print(io, temp_path) # record the temporary path for Pkg.gc()
337+
finally
338+
close(io)
339+
end
340+
rename(path, temp_path) # do not call mv which could recursively call rm(path)
341+
end
342+
333343
# The following use Unix command line facilities
334344
function checkfor_mv_cp_cptree(src::AbstractString, dst::AbstractString, txt::AbstractString;
335345
force::Bool=false)
@@ -678,8 +688,35 @@ end
678688
# deprecated internal function used by some packages
679689
temp_cleanup_purge(; force=false) = force ? temp_cleanup_purge_all() : @lock TEMP_CLEANUP_LOCK temp_cleanup_purge_prelocked(false)
680690

691+
function temp_cleanup_postprocess(cleanup_dirs)
692+
if !isempty(cleanup_dirs)
693+
rmcmd = """
694+
cleanuplist = readlines(stdin) # This loop won't start running until stdin is closed, which is supposed to be sequenced after the process exits
695+
sleep(1) # Wait for the operating system to hopefully be ready, since the OS implementation is probably incorrect, given the history of buggy work-arounds like this that have existed for ages in dotNet and libuv
696+
for path in cleanuplist
697+
try
698+
rm(path, force=true, recursive=true)
699+
catch ex
700+
@warn "Failed to clean up temporary path \$(repr(path))\n\$ex" _group=:file
701+
end
702+
end
703+
"""
704+
cmd = Cmd(Base.cmd_gen(((Base.julia_cmd(),), ("--startup-file=no",), ("-e",), (rmcmd,))); ignorestatus = true, detach = true)
705+
pw = Base.PipeEndpoint()
706+
run(cmd, pw, devnull, stderr; wait=false)
707+
join(pw, cleanup_dirs, "\n")
708+
Base.dup(Base._fd(pw)) # intentionally leak a reference, until the process exits
709+
close(pw)
710+
end
711+
end
712+
713+
function temp_cleanup_atexit()
714+
temp_cleanup_purge_all()
715+
@lock TEMP_CLEANUP_LOCK temp_cleanup_postprocess(keys(TEMP_CLEANUP))
716+
end
717+
681718
function __postinit__()
682-
Base.atexit(temp_cleanup_purge_all)
719+
Base.atexit(temp_cleanup_atexit)
683720
end
684721

685722
const temp_prefix = "jl_"

test/compileall.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
# We make it a separate test target here, so that it can run in parallel
33
# with the rest of the tests.
44

5+
include("tempdepot.jl")
6+
57
function precompile_test_harness(@nospecialize(f))
6-
load_path = mktempdir()
8+
load_path = mkdepottempdir()
79
try
810
pushfirst!(LOAD_PATH, load_path)
911
pushfirst!(DEPOT_PATH, load_path)

test/core.jl

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const Bottom = Union{}
99
# For curmod_*
1010
include("testenv.jl")
1111

12+
include("tempdepot.jl")
13+
1214
## tests that `const` field declarations
1315

1416
# sanity tests that our built-in types are marked correctly for const fields
@@ -8443,7 +8445,7 @@ end
84438445

84448446
# precompilation
84458447
let load_path = mktempdir()
8446-
depot_path = mktempdir()
8448+
depot_path = mkdepottempdir()
84478449
try
84488450
pushfirst!(LOAD_PATH, load_path)
84498451
pushfirst!(DEPOT_PATH, depot_path)
@@ -8485,11 +8487,6 @@ let load_path = mktempdir()
84858487
filter!(()(load_path), LOAD_PATH)
84868488
filter!(()(depot_path), DEPOT_PATH)
84878489
rm(load_path, recursive=true, force=true)
8488-
try
8489-
rm(depot_path, force=true, recursive=true)
8490-
catch err
8491-
@show err
8492-
end
84938490
end
84948491
end
84958492

test/loading.jl

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ end
3434
@test @nested_LINE_expansion2() == ((@__LINE__() - 5, @__LINE__() - 9), @__LINE__())
3535

3636
original_depot_path = copy(Base.DEPOT_PATH)
37+
include("tempdepot.jl")
3738
include("precompile_utils.jl")
3839

3940
loaded_files = String[]
@@ -226,7 +227,6 @@ end
226227
end
227228
end
228229

229-
230230
## functional testing of package identification, location & loading ##
231231

232232
saved_load_path = copy(LOAD_PATH)
@@ -236,8 +236,9 @@ watcher_counter = Ref(0)
236236
push!(Base.active_project_callbacks, () -> watcher_counter[] += 1)
237237
push!(Base.active_project_callbacks, () -> error("broken"))
238238

239+
const testdefaultdepot = mkdepottempdir()
239240
push!(empty!(LOAD_PATH), joinpath(@__DIR__, "project"))
240-
append!(empty!(DEPOT_PATH), [mktempdir(), joinpath(@__DIR__, "depot")])
241+
append!(empty!(DEPOT_PATH), [testdefaultdepot, joinpath(@__DIR__, "depot")])
241242
@test watcher_counter[] == 0
242243
@test_logs (:error, r"active project callback .* failed") Base.set_active_project(nothing)
243244
@test watcher_counter[] == 1
@@ -461,7 +462,7 @@ function make_env(flat, root, roots, graph, paths, dummies)
461462
)
462463
end
463464

464-
const depots = [mktempdir() for _ = 1:3]
465+
const depots = [mkdepottempdir() for _ = 1:3]
465466
const envs = Dict{String,Any}()
466467

467468
append!(empty!(DEPOT_PATH), depots)
@@ -755,13 +756,6 @@ end
755756
for env in keys(envs)
756757
rm(env, force=true, recursive=true)
757758
end
758-
for depot in depots
759-
try
760-
rm(depot, force=true, recursive=true)
761-
catch err
762-
@show err
763-
end
764-
end
765759

766760
append!(empty!(LOAD_PATH), saved_load_path)
767761
append!(empty!(DEPOT_PATH), saved_depot_path)
@@ -1043,9 +1037,10 @@ end
10431037
_pkgversion == pkgversion(parent) || error("unexpected extension \$ext version: \$_pkgversion")
10441038
end
10451039
"""
1046-
depot_path = mktempdir()
1047-
try
1048-
proj = joinpath(@__DIR__, "project", "Extensions", "HasDepWithExtensions.jl")
1040+
depot_path = mkdepottempdir()
1041+
proj = joinpath(@__DIR__, "project", "Extensions", "HasDepWithExtensions.jl")
1042+
1043+
begin
10491044

10501045
function gen_extension_cmd(compile, distr=false)
10511046
load_distr = distr ? "using Distributed; addprocs(1)" : ""
@@ -1155,7 +1150,7 @@ end
11551150

11561151
# Extension-to-extension dependencies
11571152

1158-
mktempdir() do depot # Parallel pre-compilation
1153+
mkdepottempdir() do depot # Parallel pre-compilation
11591154
code = """
11601155
Base.disable_parallel_precompile = false
11611156
using ExtToExtDependency
@@ -1171,7 +1166,7 @@ end
11711166
)
11721167
@test occursin("Hello ext-to-ext!", String(read(cmd)))
11731168
end
1174-
mktempdir() do depot # Serial pre-compilation
1169+
mkdepottempdir() do depot # Serial pre-compilation
11751170
code = """
11761171
Base.disable_parallel_precompile = true
11771172
using ExtToExtDependency
@@ -1188,7 +1183,7 @@ end
11881183
@test occursin("Hello ext-to-ext!", String(read(cmd)))
11891184
end
11901185

1191-
mktempdir() do depot # Parallel pre-compilation
1186+
mkdepottempdir() do depot # Parallel pre-compilation
11921187
code = """
11931188
Base.disable_parallel_precompile = false
11941189
using CrossPackageExtToExtDependency
@@ -1204,7 +1199,7 @@ end
12041199
)
12051200
@test occursin("Hello x-package ext-to-ext!", String(read(cmd)))
12061201
end
1207-
mktempdir() do depot # Serial pre-compilation
1202+
mkdepottempdir() do depot # Serial pre-compilation
12081203
code = """
12091204
Base.disable_parallel_precompile = true
12101205
using CrossPackageExtToExtDependency
@@ -1224,7 +1219,7 @@ end
12241219
# Extensions for "parent" dependencies
12251220
# (i.e. an `ExtAB` where A depends on / loads B, but B provides the extension)
12261221

1227-
mktempdir() do depot # Parallel pre-compilation
1222+
mkdepottempdir() do depot # Parallel pre-compilation
12281223
code = """
12291224
Base.disable_parallel_precompile = false
12301225
using Parent
@@ -1239,7 +1234,7 @@ end
12391234
)
12401235
@test occursin("Hello parent!", String(read(cmd)))
12411236
end
1242-
mktempdir() do depot # Serial pre-compilation
1237+
mkdepottempdir() do depot # Serial pre-compilation
12431238
code = """
12441239
Base.disable_parallel_precompile = true
12451240
using Parent
@@ -1254,13 +1249,6 @@ end
12541249
)
12551250
@test occursin("Hello parent!", String(read(cmd)))
12561251
end
1257-
1258-
finally
1259-
try
1260-
rm(depot_path, force=true, recursive=true)
1261-
catch err
1262-
@show err
1263-
end
12641252
end
12651253
end
12661254

@@ -1364,7 +1352,7 @@ end
13641352
end
13651353

13661354
@testset "relocatable upgrades #51989" begin
1367-
mktempdir() do depot
1355+
mkdepottempdir() do depot
13681356
# realpath is needed because Pkg is used for one of the precompile paths below, and Pkg calls realpath on the
13691357
# project path so the cache file slug will be different if the tempdir is given as a symlink
13701358
# (which it often is on MacOS) which would break the test.
@@ -1446,7 +1434,7 @@ end
14461434
end
14471435

14481436
@testset "code coverage disabled during precompilation" begin
1449-
mktempdir() do depot
1437+
mkdepottempdir() do depot
14501438
cov_test_dir = joinpath(@__DIR__, "project", "deps", "CovTest.jl")
14511439
cov_cache_dir = joinpath(depot, "compiled", "v$(VERSION.major).$(VERSION.minor)", "CovTest")
14521440
function rm_cov_files()
@@ -1490,7 +1478,7 @@ end
14901478
end
14911479

14921480
@testset "command-line flags" begin
1493-
mktempdir() do depot_path mktempdir() do dir
1481+
mkdepottempdir() do depot_path mktempdir() do dir
14941482
# generate a Parent.jl and Child.jl package, with Parent depending on Child
14951483
open(joinpath(dir, "Child.jl"), "w") do io
14961484
println(io, """
@@ -1573,7 +1561,7 @@ end
15731561
end
15741562

15751563
@testset "including non-existent file throws proper error #52462" begin
1576-
mktempdir() do depot
1564+
mkdepottempdir() do depot
15771565
project_path = joinpath(depot, "project")
15781566
mkpath(project_path)
15791567

@@ -1715,7 +1703,7 @@ end
17151703
end
17161704

17171705
@testset "require_stdlib loading duplication" begin
1718-
depot_path = mktempdir()
1706+
depot_path = mkdepottempdir()
17191707
oldBase64 = nothing
17201708
try
17211709
push!(empty!(DEPOT_PATH), depot_path)
@@ -1739,7 +1727,6 @@ end
17391727
finally
17401728
oldBase64 === nothing || Base.register_root_module(oldBase64)
17411729
copy!(DEPOT_PATH, original_depot_path)
1742-
rm(depot_path, force=true, recursive=true)
17431730
end
17441731
end
17451732

test/precompile.jl

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ using Test, Distributed, Random, Logging, Libdl
44
using REPL # testing the doc lookup function should be outside of the scope of this file, but is currently tested here
55

66
include("precompile_utils.jl")
7+
include("tempdepot.jl")
78

89
Foo_module = :Foo4b3a94a1a081a8cb
910
foo_incl_dep = :foo4b3a94a1a081a8cb
@@ -1524,11 +1525,11 @@ end
15241525
test_workers = addprocs(1)
15251526
push!(test_workers, myid())
15261527
save_cwd = pwd()
1527-
temp_path = mktempdir()
1528+
temp_path = mkdepottempdir()
15281529
try
15291530
cd(temp_path)
15301531
load_path = mktempdir(temp_path)
1531-
load_cache_path = mktempdir(temp_path)
1532+
load_cache_path = mkdepottempdir(temp_path)
15321533

15331534
ModuleA = :Issue19960A
15341535
ModuleB = :Issue19960B
@@ -1576,11 +1577,6 @@ end
15761577
end
15771578
finally
15781579
cd(save_cwd)
1579-
try
1580-
rm(temp_path, recursive=true)
1581-
catch err
1582-
@show err
1583-
end
15841580
pop!(test_workers) # remove myid
15851581
rmprocs(test_workers)
15861582
end

test/precompile_utils.jl

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,18 @@
11
# This file is a part of Julia. License is MIT: https://julialang.org/license
22

3+
include("tempdepot.jl")
4+
35
function precompile_test_harness(@nospecialize(f), testset::String)
46
@testset "$testset" precompile_test_harness(f, true)
57
end
68
function precompile_test_harness(@nospecialize(f), separate::Bool=true)
7-
load_path = mktempdir()
8-
load_cache_path = separate ? mktempdir() : load_path
9+
load_path = mkdepottempdir()
10+
load_cache_path = separate ? mkdepottempdir() : load_path
911
try
1012
pushfirst!(LOAD_PATH, load_path)
1113
pushfirst!(DEPOT_PATH, load_cache_path)
1214
f(load_path)
1315
finally
14-
try
15-
rm(load_path, force=true, recursive=true)
16-
catch err
17-
@show err
18-
end
19-
if separate
20-
try
21-
rm(load_cache_path, force=true, recursive=true)
22-
catch err
23-
@show err
24-
end
25-
end
2616
filter!(()(load_path), LOAD_PATH)
2717
separate && filter!(()(load_cache_path), DEPOT_PATH)
2818
end

0 commit comments

Comments
 (0)