Skip to content

Commit

Permalink
rm: move open DLLs to temp dir to allow dir to be deleted (#53456)
Browse files Browse the repository at this point in the history
  • Loading branch information
IanButterworth authored Feb 27, 2024
1 parent 98b3f72 commit 2e9b0bb
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 34 deletions.
40 changes: 26 additions & 14 deletions base/file.jl
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,10 @@ function mkpath(path::AbstractString; mode::Integer = 0o777)
path
end

# Files that were requested to be deleted but can't be by the current process
# i.e. loaded DLLs on Windows
delayed_delete_dir() = joinpath(tempdir(), "julia_delayed_deletes")

"""
rm(path::AbstractString; force::Bool=false, recursive::Bool=false)
Expand All @@ -270,33 +274,41 @@ Stacktrace:
[...]
```
"""
function rm(path::AbstractString; force::Bool=false, recursive::Bool=false)
function rm(path::AbstractString; force::Bool=false, recursive::Bool=false, allow_delayed_delete::Bool=true)
# allow_delayed_delete is used by Pkg.gc() but is otherwise not part of the public API
if islink(path) || !isdir(path)
try
@static if Sys.iswindows()
# is writable on windows actually means "is deletable"
st = lstat(path)
if ispath(st) && (filemode(st) & 0o222) == 0
chmod(path, 0o777)
end
end
unlink(path)
catch err
if force && isa(err, IOError) && err.code==Base.UV_ENOENT
return
if isa(err, IOError)
force && err.code==Base.UV_ENOENT && return
@static if Sys.iswindows()
if allow_delayed_delete && err.code==Base.UV_EACCES && endswith(path, ".dll")
# Loaded DLLs cannot be deleted on Windows, even with posix delete mode
# but they can be moved. So move out to allow the dir to be deleted
# TODO: Add a mechanism to delete these moved files after dlclose or process exit
dir = mkpath(delayed_delete_dir())
temp_path = tempname(dir, cleanup = false, suffix = string("_", basename(path)))
@debug "Could not delete DLL most likely because it is loaded, moving to tempdir" path temp_path
mv(path, temp_path)
return
end
end
end
rethrow()
end
else
if recursive
try
for p in readdir(path)
rm(joinpath(path, p), force=force, recursive=true)
try
rm(joinpath(path, p), force=force, recursive=true)
catch err
(isa(err, IOError) && err.code==Base.UV_EACCES) || rethrow()
end
end
catch err
if !(isa(err, IOError) && err.code==Base.UV_EACCES)
rethrow(err)
end
(isa(err, IOError) && err.code==Base.UV_EACCES) || rethrow()
end
end
req = Libc.malloc(_sizeof_uv_fs)
Expand Down
45 changes: 25 additions & 20 deletions base/loading.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2902,26 +2902,10 @@ function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, in
end

if cache_objects
try
rename(tmppath_so, ocachefile::String; force=true)
catch e
e isa IOError || rethrow()
isfile(ocachefile::String) || rethrow()
# Windows prevents renaming a file that is in use so if there is a Julia session started
# with a package image loaded, we cannot rename that file.
# The code belows append a `_i` to the name of the cache file where `i` is the smallest number such that
# that cache file does not exist.
ocachename, ocacheext = splitext(ocachefile::String)
old_cachefiles = Set(readdir(cachepath))
num = 1
while true
ocachefile = ocachename * "_$num" * ocacheext
in(basename(ocachefile), old_cachefiles) || break
num += 1
end
# TODO: Risk for a race here if some other process grabs this name before us
cachefile = cachefile_from_ocachefile(ocachefile)
rename(tmppath_so, ocachefile::String; force=true)
ocachefile_new = rename_unique_ocachefile(tmppath_so, ocachefile)
if ocachefile_new != ocachefile
cachefile = cachefile_from_ocachefile(ocachefile_new)
ocachefile = ocachefile_new
end
@static if Sys.isapple()
run(`$(Linking.dsymutil()) $ocachefile`, Base.DevNull(), Base.DevNull(), Base.DevNull())
Expand All @@ -2945,6 +2929,27 @@ function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, in
end
end

function rename_unique_ocachefile(tmppath_so::String, ocachefile_orig::String, ocachefile::String = ocachefile_orig, num = 0)
try
rename(tmppath_so, ocachefile; force=true)
catch e
e isa IOError || rethrow()
# If `rm` was called on a dir containing a loaded DLL, we moved it to temp for cleanup
# on restart. However the old path cannot be used (UV_EACCES) while the DLL is loaded
if !isfile(ocachefile) && e.code != Base.UV_EACCES
rethrow()
end
# Windows prevents renaming a file that is in use so if there is a Julia session started
# with a package image loaded, we cannot rename that file.
# The code belows append a `_i` to the name of the cache file where `i` is the smallest number such that
# that cache file does not exist.
ocachename, ocacheext = splitext(ocachefile_orig)
ocachefile_unique = ocachename * "_$num" * ocacheext
ocachefile = rename_unique_ocachefile(tmppath_so, ocachefile_orig, ocachefile_unique, num + 1)
end
return ocachefile
end

function module_build_id(m::Module)
hi, lo = ccall(:jl_module_build_id, NTuple{2,UInt64}, (Any,), m)
return (UInt128(hi) << 64) | lo
Expand Down

0 comments on commit 2e9b0bb

Please sign in to comment.