gix_tempfile/
lib.rs

1//! git-style registered tempfiles that are removed upon typical termination signals.
2//!
3//! To register signal handlers in a typical application that doesn't have its own, call
4//! [`gix_tempfile::signal::setup(Default::default())`][signal::setup()] before creating the first tempfile.
5//!
6//! Signal handlers are powered by [`signal-hook`] to get notified when the application is told to shut down
7//! to assure tempfiles are deleted. The deletion is filtered by process id to allow forks to have their own
8//! set of tempfiles that won't get deleted when the parent process exits.
9//!
10//! ### Initial Setup
11//!
12//! As no handlers for `TERMination` are installed, it is required to call [`signal::setup()`] before creating
13//! the first tempfile. This also allows to control how this crate integrates with
14//! other handlers under application control.
15//!
16//! As a general rule of thumb, use `Default::default()` as argument to emulate the default behaviour and
17//! abort the process after cleaning temporary files. Read more about options in [`signal::handler::Mode`].
18//!
19//! # Limitations
20//!
21//! ## Tempfiles might remain on disk
22//!
23//! * Uninterruptible signals are received like `SIGKILL`
24//! * The application is performing a write operation on the tempfile when a signal arrives, preventing this tempfile to be removed,
25//!   but not others. Any other operation dealing with the tempfile suffers from the same issue.
26//!
27//! [`signal-hook`]: https://docs.rs/signal-hook
28//!
29//! ## Feature Flags
30#![cfg_attr(
31    all(doc, feature = "document-features"),
32    doc = ::document_features::document_features!()
33)]
34#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg, doc_auto_cfg))]
35#![deny(missing_docs, rust_2018_idioms, unsafe_code)]
36
37use std::{
38    io,
39    marker::PhantomData,
40    path::{Path, PathBuf},
41    sync::atomic::AtomicUsize,
42};
43
44use once_cell::sync::Lazy;
45
46#[cfg(feature = "hp-hashmap")]
47type HashMap<K, V> = dashmap::DashMap<K, V>;
48
49#[cfg(not(feature = "hp-hashmap"))]
50mod hashmap {
51    use std::collections::HashMap;
52
53    use parking_lot::Mutex;
54
55    // TODO(performance): use the `gix-hashtable` slot-map once available. It seems quite fast already though, so experiment.
56    pub struct Concurrent<K, V> {
57        inner: Mutex<HashMap<K, V>>,
58    }
59
60    impl<K, V> Default for Concurrent<K, V>
61    where
62        K: Eq + std::hash::Hash,
63    {
64        fn default() -> Self {
65            Concurrent {
66                inner: Default::default(),
67            }
68        }
69    }
70
71    impl<K, V> Concurrent<K, V>
72    where
73        K: Eq + std::hash::Hash + Clone,
74    {
75        pub fn insert(&self, key: K, value: V) -> Option<V> {
76            self.inner.lock().insert(key, value)
77        }
78
79        pub fn remove(&self, key: &K) -> Option<(K, V)> {
80            self.inner.lock().remove(key).map(|v| (key.clone(), v))
81        }
82
83        pub fn for_each<F>(&self, cb: F)
84        where
85            Self: Sized,
86            F: FnMut(&mut V),
87        {
88            if let Some(mut guard) = self.inner.try_lock() {
89                guard.values_mut().for_each(cb);
90            }
91        }
92    }
93}
94
95#[cfg(not(feature = "hp-hashmap"))]
96type HashMap<K, V> = hashmap::Concurrent<K, V>;
97
98pub use gix_fs::dir::{create as create_dir, remove as remove_dir};
99
100/// signal setup and reusable handlers.
101#[cfg(feature = "signals")]
102pub mod signal;
103
104mod forksafe;
105use forksafe::ForksafeTempfile;
106
107pub mod handle;
108use crate::handle::{Closed, Writable};
109
110///
111pub mod registry;
112
113static NEXT_MAP_INDEX: AtomicUsize = AtomicUsize::new(0);
114static REGISTRY: Lazy<HashMap<usize, Option<ForksafeTempfile>>> = Lazy::new(|| {
115    #[cfg(feature = "signals")]
116    if signal::handler::MODE.load(std::sync::atomic::Ordering::SeqCst) != signal::handler::Mode::None as usize {
117        for sig in signal_hook::consts::TERM_SIGNALS {
118            // SAFETY: handlers are considered unsafe because a lot can go wrong. See `cleanup_tempfiles()` for details on safety.
119            #[allow(unsafe_code)]
120            unsafe {
121                #[cfg(not(windows))]
122                {
123                    signal_hook_registry::register_sigaction(*sig, signal::handler::cleanup_tempfiles_nix)
124                }
125                #[cfg(windows)]
126                {
127                    signal_hook::low_level::register(*sig, signal::handler::cleanup_tempfiles_windows)
128                }
129            }
130            .expect("signals can always be installed");
131        }
132    }
133    HashMap::default()
134});
135
136/// A type expressing the ways we can deal with directories containing a tempfile.
137#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)]
138pub enum ContainingDirectory {
139    /// Assume the directory for the tempfile exists and cause failure if it doesn't
140    Exists,
141    /// Create the directory recursively with the given amount of retries in a way that is somewhat race resistant
142    /// depending on the amount of retries.
143    CreateAllRaceProof(create_dir::Retries),
144}
145
146/// A type expressing the ways we cleanup after ourselves to remove resources we created.
147/// Note that cleanup has no effect if the tempfile is persisted.
148#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
149pub enum AutoRemove {
150    /// Remove the temporary file after usage if it wasn't persisted.
151    Tempfile,
152    /// Remove the temporary file as well the containing directories if they are empty until the given `directory`.
153    TempfileAndEmptyParentDirectoriesUntil {
154        /// The directory which shall not be removed even if it is empty.
155        boundary_directory: PathBuf,
156    },
157}
158
159impl AutoRemove {
160    fn execute_best_effort(self, directory_to_potentially_delete: &Path) -> Option<PathBuf> {
161        match self {
162            AutoRemove::Tempfile => None,
163            AutoRemove::TempfileAndEmptyParentDirectoriesUntil { boundary_directory } => {
164                remove_dir::empty_upward_until_boundary(directory_to_potentially_delete, &boundary_directory).ok();
165                Some(boundary_directory)
166            }
167        }
168    }
169}
170
171/// A registered temporary file which will delete itself on drop or if the program is receiving signals that
172/// should cause it to terminate.
173///
174/// # Note
175///
176/// Signals interrupting the calling thread right after taking ownership of the registered tempfile
177/// will cause all but this tempfile to be removed automatically. In the common case it will persist on disk as destructors
178/// were not called or didn't get to remove the file.
179///
180/// In the best case the file is a true temporary with a non-clashing name that 'only' fills up the disk,
181/// in the worst case the temporary file is used as a lock file which may leave the repository in a locked
182/// state forever.
183///
184/// This kind of raciness exists whenever [`take()`][Handle::take()] is used and can't be circumvented.
185#[derive(Debug)]
186#[must_use = "A handle that is immediately dropped doesn't lock a resource meaningfully"]
187pub struct Handle<Marker: std::fmt::Debug> {
188    id: usize,
189    _marker: PhantomData<Marker>,
190}
191
192/// A shortcut to [`Handle::<Writable>::new()`], creating a writable temporary file with non-clashing name in a directory.
193pub fn new(
194    containing_directory: impl AsRef<Path>,
195    directory: ContainingDirectory,
196    cleanup: AutoRemove,
197) -> io::Result<Handle<Writable>> {
198    Handle::<Writable>::new(containing_directory, directory, cleanup)
199}
200
201/// A shortcut to [`Handle::<Writable>::at()`] providing a writable temporary file at the given path.
202pub fn writable_at(
203    path: impl AsRef<Path>,
204    directory: ContainingDirectory,
205    cleanup: AutoRemove,
206) -> io::Result<Handle<Writable>> {
207    Handle::<Writable>::at(path, directory, cleanup)
208}
209
210/// Like [`writable_at`], but allows to set the given filesystem `permissions`.
211pub fn writable_at_with_permissions(
212    path: impl AsRef<Path>,
213    directory: ContainingDirectory,
214    cleanup: AutoRemove,
215    permissions: std::fs::Permissions,
216) -> io::Result<Handle<Writable>> {
217    Handle::<Writable>::at_with_permissions(path, directory, cleanup, permissions)
218}
219
220/// A shortcut to [`Handle::<Closed>::at()`] providing a closed temporary file to mark the presence of something.
221pub fn mark_at(
222    path: impl AsRef<Path>,
223    directory: ContainingDirectory,
224    cleanup: AutoRemove,
225) -> io::Result<Handle<Closed>> {
226    Handle::<Closed>::at(path, directory, cleanup)
227}
228
229/// Like [`mark_at`], but allows to set the given filesystem `permissions`.
230pub fn mark_at_with_permissions(
231    path: impl AsRef<Path>,
232    directory: ContainingDirectory,
233    cleanup: AutoRemove,
234    permissions: std::fs::Permissions,
235) -> io::Result<Handle<Closed>> {
236    Handle::<Closed>::at_with_permissions(path, directory, cleanup, permissions)
237}