blob: 9a5d52405b60f4f23903e3865605d41b51ed5206 [file] [log] [blame] [edit]
//! 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)
}