Skip to main content

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//! ## Examples
30//!
31//! ```
32//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
33//! use std::io::Write;
34//!
35//! # let dir = tempfile::tempdir()?;
36//! # let destination = dir.path().join("config");
37//! let mut tempfile = gix_tempfile::writable_at(
38//!     dir.path().join("config.lock"),
39//!     gix_tempfile::ContainingDirectory::Exists,
40//!     gix_tempfile::AutoRemove::Tempfile,
41//! )?;
42//! tempfile.write_all(b"new = value\n")?;
43//! tempfile.persist(&destination)?;
44//!
45//! assert_eq!(std::fs::read_to_string(&destination)?, "new = value\n");
46//! # Ok(()) }
47//! ```
48//!
49//! ## Feature Flags
50#![cfg_attr(
51    all(doc, feature = "document-features"),
52    doc = ::document_features::document_features!()
53)]
54#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))]
55#![deny(missing_docs, rust_2018_idioms, unsafe_code)]
56
57use std::{
58    io,
59    marker::PhantomData,
60    path::{Path, PathBuf},
61    sync::atomic::AtomicUsize,
62};
63
64use std::sync::LazyLock;
65
66#[cfg(feature = "hp-hashmap")]
67type HashMap<K, V> = dashmap::DashMap<K, V>;
68
69#[cfg(not(feature = "hp-hashmap"))]
70mod hashmap {
71    use std::collections::HashMap;
72
73    use parking_lot::Mutex;
74
75    // TODO(performance): use the `gix-hashtable` slot-map once available. It seems quite fast already though, so experiment.
76    pub struct Concurrent<K, V> {
77        inner: Mutex<HashMap<K, V>>,
78    }
79
80    impl<K, V> Default for Concurrent<K, V>
81    where
82        K: Eq + std::hash::Hash,
83    {
84        fn default() -> Self {
85            Concurrent {
86                inner: Default::default(),
87            }
88        }
89    }
90
91    impl<K, V> Concurrent<K, V>
92    where
93        K: Eq + std::hash::Hash + Clone,
94    {
95        pub fn insert(&self, key: K, value: V) -> Option<V> {
96            self.inner.lock().insert(key, value)
97        }
98
99        pub fn remove(&self, key: &K) -> Option<(K, V)> {
100            self.inner.lock().remove(key).map(|v| (key.clone(), v))
101        }
102
103        pub fn for_each<F>(&self, cb: F)
104        where
105            Self: Sized,
106            F: FnMut(&mut V),
107        {
108            if let Some(mut guard) = self.inner.try_lock() {
109                guard.values_mut().for_each(cb);
110            }
111        }
112    }
113}
114
115#[cfg(not(feature = "hp-hashmap"))]
116type HashMap<K, V> = hashmap::Concurrent<K, V>;
117
118pub use gix_fs::dir::{create as create_dir, remove as remove_dir};
119
120/// signal setup and reusable handlers.
121#[cfg(feature = "signals")]
122pub mod signal;
123
124mod forksafe;
125use forksafe::ForksafeTempfile;
126
127pub mod handle;
128use crate::handle::{Closed, Writable};
129
130///
131pub mod registry;
132
133static NEXT_MAP_INDEX: AtomicUsize = AtomicUsize::new(0);
134static REGISTRY: LazyLock<HashMap<usize, Option<ForksafeTempfile>>> = LazyLock::new(|| {
135    #[cfg(feature = "signals")]
136    if signal::handler::MODE.load(std::sync::atomic::Ordering::SeqCst) != signal::handler::Mode::None as usize {
137        for sig in signal_hook::consts::TERM_SIGNALS {
138            // SAFETY: handlers are considered unsafe because a lot can go wrong. See `cleanup_tempfiles()` for details on safety.
139            #[allow(unsafe_code)]
140            unsafe {
141                #[cfg(not(windows))]
142                {
143                    signal_hook_registry::register_sigaction(*sig, signal::handler::cleanup_tempfiles_nix)
144                }
145                #[cfg(windows)]
146                {
147                    signal_hook::low_level::register(*sig, signal::handler::cleanup_tempfiles_windows)
148                }
149            }
150            .expect("signals can always be installed");
151        }
152    }
153    HashMap::default()
154});
155
156/// A type expressing the ways we can deal with directories containing a tempfile.
157#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)]
158pub enum ContainingDirectory {
159    /// Assume the directory for the tempfile exists and cause failure if it doesn't
160    Exists,
161    /// Create the directory recursively with the given number of retries in a way that is somewhat race resistant
162    /// depending on the amount of retries.
163    CreateAllRaceProof(create_dir::Retries),
164}
165
166/// A type expressing the ways we cleanup after ourselves to remove resources we created.
167/// Note that cleanup has no effect if the tempfile is persisted.
168#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
169pub enum AutoRemove {
170    /// Remove the temporary file after usage if it wasn't persisted.
171    Tempfile,
172    /// Remove the temporary file as well the containing directories if they are empty until the given `directory`.
173    TempfileAndEmptyParentDirectoriesUntil {
174        /// The directory which shall not be removed even if it is empty.
175        boundary_directory: PathBuf,
176    },
177}
178
179impl AutoRemove {
180    fn execute_best_effort(self, directory_to_potentially_delete: &Path) -> Option<PathBuf> {
181        match self {
182            AutoRemove::Tempfile => None,
183            AutoRemove::TempfileAndEmptyParentDirectoriesUntil { boundary_directory } => {
184                remove_dir::empty_upward_until_boundary(directory_to_potentially_delete, &boundary_directory).ok();
185                Some(boundary_directory)
186            }
187        }
188    }
189}
190
191/// A registered temporary file which will delete itself on drop or if the program is receiving signals that
192/// should cause it to terminate.
193///
194/// # Note
195///
196/// Signals interrupting the calling thread right after taking ownership of the registered tempfile
197/// will cause all but this tempfile to be removed automatically. In the common case it will persist on disk as destructors
198/// were not called or didn't get to remove the file.
199///
200/// In the best case the file is a true temporary with a non-clashing name that 'only' fills up the disk,
201/// in the worst case the temporary file is used as a lock file which may leave the repository in a locked
202/// state forever.
203///
204/// This kind of raciness exists whenever [`take()`][Handle::take()] is used and can't be circumvented.
205#[derive(Debug)]
206#[must_use = "A handle that is immediately dropped doesn't lock a resource meaningfully"]
207pub struct Handle<Marker: std::fmt::Debug> {
208    id: usize,
209    _marker: PhantomData<Marker>,
210}
211
212/// A shortcut to [`Handle::<Writable>::new()`], creating a writable temporary file with non-clashing name in a directory.
213pub fn new(
214    containing_directory: impl AsRef<Path>,
215    directory: ContainingDirectory,
216    cleanup: AutoRemove,
217) -> io::Result<Handle<Writable>> {
218    Handle::<Writable>::new(containing_directory, directory, cleanup)
219}
220
221/// A shortcut to [`Handle::<Writable>::at()`] providing a writable temporary file at the given path.
222pub fn writable_at(
223    path: impl AsRef<Path>,
224    directory: ContainingDirectory,
225    cleanup: AutoRemove,
226) -> io::Result<Handle<Writable>> {
227    Handle::<Writable>::at(path, directory, cleanup)
228}
229
230/// Like [`writable_at`], but allows to set the given filesystem `permissions`.
231pub fn writable_at_with_permissions(
232    path: impl AsRef<Path>,
233    directory: ContainingDirectory,
234    cleanup: AutoRemove,
235    permissions: std::fs::Permissions,
236) -> io::Result<Handle<Writable>> {
237    Handle::<Writable>::at_with_permissions(path, directory, cleanup, permissions)
238}
239
240/// A shortcut to [`Handle::<Closed>::at()`] providing a closed temporary file to mark the presence of something.
241pub fn mark_at(
242    path: impl AsRef<Path>,
243    directory: ContainingDirectory,
244    cleanup: AutoRemove,
245) -> io::Result<Handle<Closed>> {
246    Handle::<Closed>::at(path, directory, cleanup)
247}
248
249/// Like [`mark_at`], but allows to set the given filesystem `permissions`.
250pub fn mark_at_with_permissions(
251    path: impl AsRef<Path>,
252    directory: ContainingDirectory,
253    cleanup: AutoRemove,
254    permissions: std::fs::Permissions,
255) -> io::Result<Handle<Closed>> {
256    Handle::<Closed>::at_with_permissions(path, directory, cleanup, permissions)
257}