-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Support creating lock files on ExFAT on MacOS #17115
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
Changes from 2 commits
25cada0
7c90004
7660f9d
3f1c106
55110c2
ae184f8
b2cb800
d4272a1
d904d8c
f6d86da
7ae2762
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,33 @@ static LOCK_TIMEOUT: LazyLock<Duration> = LazyLock::new(|| { | |
| } | ||
| }); | ||
|
|
||
| #[derive(Debug, Error)] | ||
| pub enum CreateLockedFileError { | ||
| #[error("Could not create temporary file")] | ||
| CreateTemporary(#[source] io::Error), | ||
| #[error("Could not persist temporary file `{}`", path.user_display())] | ||
| PersistTemporary { | ||
| path: PathBuf, | ||
| #[source] | ||
| source: io::Error, | ||
| }, | ||
| #[error("Could not open existing file")] | ||
| OpenExisting(#[source] io::Error), | ||
| #[error("Could not create or open the file")] | ||
|
||
| CreateOrOpen(#[source] io::Error), | ||
| } | ||
|
|
||
| impl CreateLockedFileError { | ||
| pub fn as_io_error(&self) -> &io::Error { | ||
| match self { | ||
| Self::CreateTemporary(err) => err, | ||
| Self::PersistTemporary { source, .. } => source, | ||
| Self::OpenExisting(err) => err, | ||
| Self::CreateOrOpen(err) => err, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Debug, Error)] | ||
| pub enum LockedFileError { | ||
| #[error( | ||
|
|
@@ -58,8 +85,12 @@ pub enum LockedFileError { | |
| #[source] | ||
| source: io::Error, | ||
| }, | ||
| #[error(transparent)] | ||
| Io(#[from] io::Error), | ||
| #[error("Could not open or create lock file at `{}`", path.user_display())] | ||
| Create { | ||
| path: PathBuf, | ||
| #[source] | ||
| source: CreateLockedFileError, | ||
| }, | ||
| #[error(transparent)] | ||
| #[cfg(feature = "tokio")] | ||
| JoinError(#[from] tokio::task::JoinError), | ||
|
|
@@ -72,7 +103,7 @@ impl LockedFileError { | |
| #[cfg(feature = "tokio")] | ||
| Self::JoinError(_) => None, | ||
| Self::Lock { source, .. } => Some(source), | ||
| Self::Io(err) => Some(err), | ||
| Self::Create { source, .. } => Some(source.as_io_error()), | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -201,7 +232,10 @@ impl LockedFile { | |
| mode: LockedFileMode, | ||
| resource: impl Display, | ||
| ) -> Result<Self, LockedFileError> { | ||
| let file = Self::create(path)?; | ||
| let file = Self::create(&path).map_err(|source| LockedFileError::Create { | ||
| path: path.as_ref().to_path_buf(), | ||
| source, | ||
| })?; | ||
| let resource = resource.to_string(); | ||
| Self::lock_file(file, mode, &resource).await | ||
| } | ||
|
|
@@ -222,10 +256,19 @@ impl LockedFile { | |
| } | ||
|
|
||
| #[cfg(unix)] | ||
| fn create(path: impl AsRef<Path>) -> Result<fs_err::File, std::io::Error> { | ||
| use std::os::unix::fs::PermissionsExt; | ||
| fn create(path: impl AsRef<Path>) -> Result<fs_err::File, CreateLockedFileError> { | ||
| use rustix::io::Errno; | ||
| #[allow(clippy::disallowed_types)] | ||
| use std::{fs::File, os::unix::fs::PermissionsExt}; | ||
| use tempfile::NamedTempFile; | ||
|
|
||
| #[allow(clippy::disallowed_types)] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, we can't easily.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, I had only looked at the second callsite |
||
| fn try_set_permissions(file: &File) { | ||
| if let Err(err) = file.set_permissions(std::fs::Permissions::from_mode(0o666)) { | ||
| warn!("Failed to set permissions on temporary file: {err}"); | ||
| } | ||
| } | ||
|
|
||
| // If path already exists, return it. | ||
| if let Ok(file) = fs_err::OpenOptions::new() | ||
| .read(true) | ||
|
|
@@ -238,16 +281,12 @@ impl LockedFile { | |
| // Otherwise, create a temporary file with 666 permissions. We must set | ||
| // permissions _after_ creating the file, to override the `umask`. | ||
| let file = if let Some(parent) = path.as_ref().parent() { | ||
| NamedTempFile::new_in(parent)? | ||
| NamedTempFile::new_in(parent) | ||
| } else { | ||
| NamedTempFile::new()? | ||
| }; | ||
| if let Err(err) = file | ||
| .as_file() | ||
| .set_permissions(std::fs::Permissions::from_mode(0o666)) | ||
| { | ||
| warn!("Failed to set permissions on temporary file: {err}"); | ||
| NamedTempFile::new() | ||
| } | ||
| .map_err(CreateLockedFileError::CreateTemporary)?; | ||
| try_set_permissions(file.as_file()); | ||
|
|
||
| // Try to move the file to path, but if path exists now, just open path | ||
| match file.persist_noclobber(path.as_ref()) { | ||
|
|
@@ -258,20 +297,59 @@ impl LockedFile { | |
| .read(true) | ||
| .write(true) | ||
| .open(path.as_ref()) | ||
| .map_err(CreateLockedFileError::OpenExisting) | ||
| } else if matches!( | ||
| Errno::from_io_error(&err.error), | ||
| Some(Errno::NOTSUP | Errno::INVAL) | ||
|
Comment on lines
+284
to
+286
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mentioned something about
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will add the comment, I was on the fence but you persuaded me :) Regarding upstream, the problem is complicated and this topic seems to have been discussed at length already: rust-lang/rust#78880 The fundamental issues are:
So I am not certain there is anything meaningful we can add for upstream, the situation isn't great and there aren't many good solutions which don't involve going further upstream, and breaking userspace APIs is taboo. I think we are kind of stuck here. Footnotes
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for write-up! |
||
| ) { | ||
| // Fallback in case `persist_noclobber`, which uses `renameat2` or | ||
| // `renameatx_np` under the hood, is not supported by the FS. Linux reports this | ||
| // with `EINVAL` and MacOS with `ENOTSUP`. | ||
|
|
||
| // There is a race here where another process has just created the file, and we | ||
| // try to open it and get permission errors because the other process hasn't set | ||
| // the permission bits yet. This will lead to a transient failure, but unlike | ||
| // alternative approaches it won't ever lead to a situation where two processes | ||
| // are locking two different files. Also, since `persist_noclobber` is more | ||
| // likely to not be supported on special filesystems which don't have permission | ||
| // bits, it's less likely to ever matter. | ||
| let file = fs_err::OpenOptions::new() | ||
| .read(true) | ||
| .write(true) | ||
| .create(true) | ||
| .open(path.as_ref()) | ||
| .map_err(CreateLockedFileError::CreateOrOpen)?; | ||
|
|
||
| // We don't want to `try_set_permissions` in cases where another user's process | ||
| // has already created the lockfile and changed its permissions because we might | ||
| // not have permission to change the permissions which would produce a confusing | ||
| // warning. | ||
| if file | ||
| .metadata() | ||
| .is_ok_and(|metadata| metadata.permissions().mode() != 0o666) | ||
|
||
| { | ||
| try_set_permissions(file.file()); | ||
| } | ||
| Ok(file) | ||
| } else { | ||
| Err(err.error) | ||
| let temp_path = err.file.into_temp_path(); | ||
| Err(CreateLockedFileError::PersistTemporary { | ||
| path: <tempfile::TempPath as AsRef<Path>>::as_ref(&temp_path).to_path_buf(), | ||
| source: err.error, | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(not(unix))] | ||
| fn create(path: impl AsRef<Path>) -> std::io::Result<fs_err::File> { | ||
| fn create(path: impl AsRef<Path>) -> Result<fs_err::File, CreateLockedFileError> { | ||
| fs_err::OpenOptions::new() | ||
| .read(true) | ||
| .write(true) | ||
| .create(true) | ||
| .open(path.as_ref()) | ||
| .map_err(CreateLockedFileError::CreateOrOpen) | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: We usually have one top level error enum per module or crate. It's a developer convenience that allows the functions and methods in the module or crate to all return the same error type, where it's easier to add new variants to a single type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I agree with the justification, but I will adjust to fit the style for the time being.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't a rule or anything fwiw, it's more a pattern that seems common, and there's definitely exceptions.