| //! git-style registered tempfiles that are removed upon typical termination signals. |
| //! |
| //! To register signal handlers in a typical application that doesn't have its own, call |
| //! [`gix_tempfile::signal::setup(Default::default())`][signal::setup()] before creating the first tempfile. |
| //! |
| //! Signal handlers are powered by [`signal-hook`] to get notified when the application is told to shut down |
| //! to assure tempfiles are deleted. The deletion is filtered by process id to allow forks to have their own |
| //! set of tempfiles that won't get deleted when the parent process exits. |
| //! |
| //! ### Initial Setup |
| //! |
| //! As no handlers for `TERMination` are installed, it is required to call [`signal::setup()`] before creating |
| //! the first tempfile. This also allows to control how this crate integrates with |
| //! other handlers under application control. |
| //! |
| //! As a general rule of thumb, use `Default::default()` as argument to emulate the default behaviour and |
| //! abort the process after cleaning temporary files. Read more about options in [`signal::handler::Mode`]. |
| //! |
| //! # Limitations |
| //! |
| //! ## Tempfiles might remain on disk |
| //! |
| //! * Uninterruptible signals are received like `SIGKILL` |
| //! * The application is performing a write operation on the tempfile when a signal arrives, preventing this tempfile to be removed, |
| //! but not others. Any other operation dealing with the tempfile suffers from the same issue. |
| //! |
| //! [`signal-hook`]: https://docs.rs/signal-hook |
| //! |
| //! ## Feature Flags |
| #![cfg_attr( |
| all(doc, feature = "document-features"), |
| doc = ::document_features::document_features!() |
| )] |
| #![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg, doc_auto_cfg))] |
| #![deny(missing_docs, rust_2018_idioms, unsafe_code)] |
| |
| use std::{ |
| io, |
| marker::PhantomData, |
| path::{Path, PathBuf}, |
| sync::atomic::AtomicUsize, |
| }; |
| |
| use once_cell::sync::Lazy; |
| |
| #[cfg(feature = "hp-hashmap")] |
| type HashMap<K, V> = dashmap::DashMap<K, V>; |
| |
| #[cfg(not(feature = "hp-hashmap"))] |
| mod hashmap { |
| use std::collections::HashMap; |
| |
| use parking_lot::Mutex; |
| |
| // TODO(performance): use the `gix-hashtable` slot-map once available. It seems quite fast already though, so experiment. |
| pub struct Concurrent<K, V> { |
| inner: Mutex<HashMap<K, V>>, |
| } |
| |
| impl<K, V> Default for Concurrent<K, V> |
| where |
| K: Eq + std::hash::Hash, |
| { |
| fn default() -> Self { |
| Concurrent { |
| inner: Default::default(), |
| } |
| } |
| } |
| |
| impl<K, V> Concurrent<K, V> |
| where |
| K: Eq + std::hash::Hash + Clone, |
| { |
| pub fn insert(&self, key: K, value: V) -> Option<V> { |
| self.inner.lock().insert(key, value) |
| } |
| |
| pub fn remove(&self, key: &K) -> Option<(K, V)> { |
| self.inner.lock().remove(key).map(|v| (key.clone(), v)) |
| } |
| |
| pub fn for_each<F>(&self, cb: F) |
| where |
| Self: Sized, |
| F: FnMut(&mut V), |
| { |
| if let Some(mut guard) = self.inner.try_lock() { |
| guard.values_mut().for_each(cb); |
| } |
| } |
| } |
| } |
| |
| #[cfg(not(feature = "hp-hashmap"))] |
| type HashMap<K, V> = hashmap::Concurrent<K, V>; |
| |
| pub use gix_fs::dir::{create as create_dir, remove as remove_dir}; |
| |
| /// signal setup and reusable handlers. |
| #[cfg(feature = "signals")] |
| pub mod signal; |
| |
| mod forksafe; |
| use forksafe::ForksafeTempfile; |
| |
| pub mod handle; |
| use crate::handle::{Closed, Writable}; |
| |
| /// |
| #[allow(clippy::empty_docs)] |
| pub mod registry; |
| |
| static NEXT_MAP_INDEX: AtomicUsize = AtomicUsize::new(0); |
| static REGISTRY: Lazy<HashMap<usize, Option<ForksafeTempfile>>> = Lazy::new(|| { |
| #[cfg(feature = "signals")] |
| if signal::handler::MODE.load(std::sync::atomic::Ordering::SeqCst) != signal::handler::Mode::None as usize { |
| for sig in signal_hook::consts::TERM_SIGNALS { |
| // SAFETY: handlers are considered unsafe because a lot can go wrong. See `cleanup_tempfiles()` for details on safety. |
| #[allow(unsafe_code)] |
| unsafe { |
| #[cfg(not(windows))] |
| { |
| signal_hook_registry::register_sigaction(*sig, signal::handler::cleanup_tempfiles_nix) |
| } |
| #[cfg(windows)] |
| { |
| signal_hook::low_level::register(*sig, signal::handler::cleanup_tempfiles_windows) |
| } |
| } |
| .expect("signals can always be installed"); |
| } |
| } |
| HashMap::default() |
| }); |
| |
| /// A type expressing the ways we can deal with directories containing a tempfile. |
| #[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)] |
| pub enum ContainingDirectory { |
| /// Assume the directory for the tempfile exists and cause failure if it doesn't |
| Exists, |
| /// Create the directory recursively with the given amount of retries in a way that is somewhat race resistant |
| /// depending on the amount of retries. |
| CreateAllRaceProof(create_dir::Retries), |
| } |
| |
| /// A type expressing the ways we cleanup after ourselves to remove resources we created. |
| /// Note that cleanup has no effect if the tempfile is persisted. |
| #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] |
| pub enum AutoRemove { |
| /// Remove the temporary file after usage if it wasn't persisted. |
| Tempfile, |
| /// Remove the temporary file as well the containing directories if they are empty until the given `directory`. |
| TempfileAndEmptyParentDirectoriesUntil { |
| /// The directory which shall not be removed even if it is empty. |
| boundary_directory: PathBuf, |
| }, |
| } |
| |
| impl AutoRemove { |
| fn execute_best_effort(self, directory_to_potentially_delete: &Path) -> Option<PathBuf> { |
| match self { |
| AutoRemove::Tempfile => None, |
| AutoRemove::TempfileAndEmptyParentDirectoriesUntil { boundary_directory } => { |
| remove_dir::empty_upward_until_boundary(directory_to_potentially_delete, &boundary_directory).ok(); |
| Some(boundary_directory) |
| } |
| } |
| } |
| } |
| |
| /// A registered temporary file which will delete itself on drop or if the program is receiving signals that |
| /// should cause it to terminate. |
| /// |
| /// # Note |
| /// |
| /// Signals interrupting the calling thread right after taking ownership of the registered tempfile |
| /// will cause all but this tempfile to be removed automatically. In the common case it will persist on disk as destructors |
| /// were not called or didn't get to remove the file. |
| /// |
| /// In the best case the file is a true temporary with a non-clashing name that 'only' fills up the disk, |
| /// in the worst case the temporary file is used as a lock file which may leave the repository in a locked |
| /// state forever. |
| /// |
| /// This kind of raciness exists whenever [`take()`][Handle::take()] is used and can't be circumvented. |
| #[derive(Debug)] |
| #[must_use = "A handle that is immediately dropped doesn't lock a resource meaningfully"] |
| pub struct Handle<Marker: std::fmt::Debug> { |
| id: usize, |
| _marker: PhantomData<Marker>, |
| } |
| |
| /// A shortcut to [`Handle::<Writable>::new()`], creating a writable temporary file with non-clashing name in a directory. |
| pub fn new( |
| containing_directory: impl AsRef<Path>, |
| directory: ContainingDirectory, |
| cleanup: AutoRemove, |
| ) -> io::Result<Handle<Writable>> { |
| Handle::<Writable>::new(containing_directory, directory, cleanup) |
| } |
| |
| /// A shortcut to [`Handle::<Writable>::at()`] providing a writable temporary file at the given path. |
| pub fn writable_at( |
| path: impl AsRef<Path>, |
| directory: ContainingDirectory, |
| cleanup: AutoRemove, |
| ) -> io::Result<Handle<Writable>> { |
| Handle::<Writable>::at(path, directory, cleanup) |
| } |
| |
| /// Like [`writable_at`], but allows to set the given filesystem `permissions`. |
| pub fn writable_at_with_permissions( |
| path: impl AsRef<Path>, |
| directory: ContainingDirectory, |
| cleanup: AutoRemove, |
| permissions: std::fs::Permissions, |
| ) -> io::Result<Handle<Writable>> { |
| Handle::<Writable>::at_with_permissions(path, directory, cleanup, permissions) |
| } |
| |
| /// A shortcut to [`Handle::<Closed>::at()`] providing a closed temporary file to mark the presence of something. |
| pub fn mark_at( |
| path: impl AsRef<Path>, |
| directory: ContainingDirectory, |
| cleanup: AutoRemove, |
| ) -> io::Result<Handle<Closed>> { |
| Handle::<Closed>::at(path, directory, cleanup) |
| } |
| |
| /// Like [`mark_at`], but allows to set the given filesystem `permissions`. |
| pub fn mark_at_with_permissions( |
| path: impl AsRef<Path>, |
| directory: ContainingDirectory, |
| cleanup: AutoRemove, |
| permissions: std::fs::Permissions, |
| ) -> io::Result<Handle<Closed>> { |
| Handle::<Closed>::at_with_permissions(path, directory, cleanup, permissions) |
| } |