diff options
Diffstat (limited to 'vendor/gix-tempfile/src/lib.rs')
-rw-r--r-- | vendor/gix-tempfile/src/lib.rs | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/vendor/gix-tempfile/src/lib.rs b/vendor/gix-tempfile/src/lib.rs new file mode 100644 index 000000000..8585c60b7 --- /dev/null +++ b/vendor/gix-tempfile/src/lib.rs @@ -0,0 +1,216 @@ +//! 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 `git-tempfiles` 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( + feature = "document-features", + cfg_attr(doc, doc = ::document_features::document_features!()) +)] +#![cfg_attr(docsrs, 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 parking_lot::Mutex; + use std::collections::HashMap; + + // 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>; + +mod fs; +pub use fs::{create_dir, remove_dir}; + +#[cfg(feature = "signals")] +/// signal setup and reusable handlers. +pub mod signal; + +mod forksafe; +use forksafe::ForksafeTempfile; + +pub mod handle; +use crate::handle::{Closed, Writable}; + +/// +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) +} + +/// 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) +} |