Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rm: move open DLLs to temp dir to allow dir to be deleted #53456

Merged
merged 8 commits into from
Feb 27, 2024
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
IanButterworth marked this conversation as resolved.
Show resolved Hide resolved
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