Windows: Iterative remove_dir_all

This will allow better strategies for use of memory and File handles. However, fully taking advantage of that is left to future work.
This commit is contained in:
Chris Denton 2022-04-26 00:13:24 +01:00
parent 7417110cef
commit 8b1f85caed
No known key found for this signature in database
GPG Key ID: 713472F2F45627DE

View File

@ -680,7 +680,7 @@ fn new(buffer: &'a DirBuff) -> Self {
} }
} }
impl<'a> Iterator for DirBuffIter<'a> { impl<'a> Iterator for DirBuffIter<'a> {
type Item = &'a [u16]; type Item = (&'a [u16], bool);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
use crate::mem::size_of; use crate::mem::size_of;
let buffer = &self.buffer?[self.cursor..]; let buffer = &self.buffer?[self.cursor..];
@ -689,14 +689,16 @@ fn next(&mut self) -> Option<Self::Item> {
// SAFETY: The buffer contains a `FILE_ID_BOTH_DIR_INFO` struct but the // SAFETY: The buffer contains a `FILE_ID_BOTH_DIR_INFO` struct but the
// last field (the file name) is unsized. So an offset has to be // last field (the file name) is unsized. So an offset has to be
// used to get the file name slice. // used to get the file name slice.
let (name, next_entry) = unsafe { let (name, is_directory, next_entry) = unsafe {
let info = buffer.as_ptr().cast::<c::FILE_ID_BOTH_DIR_INFO>(); let info = buffer.as_ptr().cast::<c::FILE_ID_BOTH_DIR_INFO>();
let next_entry = (*info).NextEntryOffset as usize; let next_entry = (*info).NextEntryOffset as usize;
let name = crate::slice::from_raw_parts( let name = crate::slice::from_raw_parts(
(*info).FileName.as_ptr().cast::<u16>(), (*info).FileName.as_ptr().cast::<u16>(),
(*info).FileNameLength as usize / size_of::<u16>(), (*info).FileNameLength as usize / size_of::<u16>(),
); );
(name, next_entry) let is_directory = ((*info).FileAttributes & c::FILE_ATTRIBUTE_DIRECTORY) != 0;
(name, is_directory, next_entry)
}; };
if next_entry == 0 { if next_entry == 0 {
@ -709,7 +711,7 @@ fn next(&mut self) -> Option<Self::Item> {
const DOT: u16 = b'.' as u16; const DOT: u16 = b'.' as u16;
match name { match name {
[DOT] | [DOT, DOT] => self.next(), [DOT] | [DOT, DOT] => self.next(),
_ => Some(name), _ => Some((name, is_directory)),
} }
} }
} }
@ -994,89 +996,77 @@ pub fn remove_dir_all(path: &Path) -> io::Result<()> {
if (file.basic_info()?.FileAttributes & c::FILE_ATTRIBUTE_DIRECTORY) == 0 { if (file.basic_info()?.FileAttributes & c::FILE_ATTRIBUTE_DIRECTORY) == 0 {
return Err(io::Error::from_raw_os_error(c::ERROR_DIRECTORY as _)); return Err(io::Error::from_raw_os_error(c::ERROR_DIRECTORY as _));
} }
let mut delete: fn(&File) -> io::Result<()> = File::posix_delete;
let result = match delete(&file) { match remove_dir_all_iterative(&file, File::posix_delete) {
Err(e) if e.kind() == io::ErrorKind::DirectoryNotEmpty => { Err(e) => {
match remove_dir_all_recursive(&file, delete) { if let Some(code) = e.raw_os_error() {
// Return unexpected errors. match code as u32 {
Err(e) if e.kind() != io::ErrorKind::DirectoryNotEmpty => return Err(e),
result => result,
}
}
// If POSIX delete is not supported for this filesystem then fallback to win32 delete. // If POSIX delete is not supported for this filesystem then fallback to win32 delete.
Err(e) c::ERROR_NOT_SUPPORTED
if e.raw_os_error() == Some(c::ERROR_NOT_SUPPORTED as i32) | c::ERROR_INVALID_FUNCTION
|| e.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as i32) => | c::ERROR_INVALID_PARAMETER => {
{ remove_dir_all_iterative(&file, File::win32_delete)
delete = File::win32_delete; }
Err(e) _ => Err(e),
} }
result => result,
};
if result.is_ok() {
Ok(())
} else { } else {
// This is a fallback to make sure the directory is actually deleted. Err(e)
// Otherwise this function is prone to failing with `DirectoryNotEmpty`
// due to possible delays between marking a file for deletion and the
// file actually being deleted from the filesystem.
//
// So we retry a few times before giving up.
for _ in 0..5 {
match remove_dir_all_recursive(&file, delete) {
Err(e) if e.kind() == io::ErrorKind::DirectoryNotEmpty => {}
result => return result,
} }
} }
// Try one last time. ok => ok,
delete(&file)
} }
} }
fn remove_dir_all_recursive(f: &File, delete: fn(&File) -> io::Result<()>) -> io::Result<()> { fn remove_dir_all_iterative(f: &File, delete: fn(&File) -> io::Result<()>) -> io::Result<()> {
let mut buffer = DirBuff::new(); let mut buffer = DirBuff::new();
let mut restart = true; let mut dirlist = vec![f.duplicate()?];
// FIXME: This is a hack so we can push to the dirlist vec after borrowing from it.
fn copy_handle(f: &File) -> mem::ManuallyDrop<File> {
unsafe { mem::ManuallyDrop::new(File::from_raw_handle(f.as_raw_handle())) }
}
while let Some(dir) = dirlist.last() {
let dir = copy_handle(dir);
// Fill the buffer and iterate the entries. // Fill the buffer and iterate the entries.
while f.fill_dir_buff(&mut buffer, restart)? { let more_data = dir.fill_dir_buff(&mut buffer, false)?;
for name in buffer.iter() { for (name, is_directory) in buffer.iter() {
// Open the file without following symlinks and try deleting it. if is_directory {
// We try opening will all needed permissions and if that is denied let child_dir = open_link_no_reparse(
// fallback to opening without `FILE_LIST_DIRECTORY` permission. &dir,
// Note `SYNCHRONIZE` permission is needed for synchronous access. name,
let mut result = c::SYNCHRONIZE | c::DELETE | c::FILE_LIST_DIRECTORY,
open_link_no_reparse(&f, name, c::SYNCHRONIZE | c::DELETE | c::FILE_LIST_DIRECTORY); )?;
if matches!(&result, Err(e) if e.kind() == io::ErrorKind::PermissionDenied) { dirlist.push(child_dir);
result = open_link_no_reparse(&f, name, c::SYNCHRONIZE | c::DELETE); } else {
} const MAX_RETRIES: u32 = 10;
for i in 1..=MAX_RETRIES {
let result = open_link_no_reparse(&dir, name, c::SYNCHRONIZE | c::DELETE);
match result { match result {
Ok(file) => match delete(&file) { Ok(f) => delete(&f)?,
Err(e) if e.kind() == io::ErrorKind::DirectoryNotEmpty => { // Already deleted, so skip.
// Iterate the directory's files. Err(e) if e.kind() == io::ErrorKind::NotFound => break,
// Ignore `DirectoryNotEmpty` errors here. They will be // Retry a few times if the file is locked or a delete is already in progress.
// caught when `remove_dir_all` tries to delete the top
// level directory. It can then decide if to retry or not.
match remove_dir_all_recursive(&file, delete) {
Err(e) if e.kind() == io::ErrorKind::DirectoryNotEmpty => {}
result => result?,
}
}
result => result?,
},
// Ignore error if a delete is already in progress or the file
// has already been deleted. It also ignores sharing violations
// (where a file is locked by another process) as these are
// usually temporary.
Err(e) Err(e)
if e.raw_os_error() == Some(c::ERROR_DELETE_PENDING as _) if i < MAX_RETRIES
|| e.kind() == io::ErrorKind::NotFound && (e.raw_os_error() == Some(c::ERROR_DELETE_PENDING as _)
|| e.raw_os_error() == Some(c::ERROR_SHARING_VIOLATION as _) => {} || e.raw_os_error()
== Some(c::ERROR_SHARING_VIOLATION as _)) => {}
// Otherwise return the error.
Err(e) => return Err(e), Err(e) => return Err(e),
} }
} }
// Continue reading directory entries without restarting from the beginning,
restart = false;
} }
delete(&f) }
// If there were no more files then delete the directory.
if !more_data {
if let Some(dir) = dirlist.pop() {
delete(&dir)?;
}
}
}
Ok(())
} }
pub fn readlink(path: &Path) -> io::Result<PathBuf> { pub fn readlink(path: &Path) -> io::Result<PathBuf> {