git_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//! [`git_tempfile::setup(Default::default())`][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 [`setup()`] before creating the first tempfile.
13//! This also allows to control how `git-tempfiles` integrates with other handlers under application control.
14//!
15//! As a general rule of thumb, use `Default::default()` as argument to emulate the default behaviour and
16//! abort the process after cleaning temporary files. Read more about options in [SignalHandlerMode].
17//!
18//! # Limitations
19//!
20//! ## Tempfiles might remain on disk
21//!
22//! * Uninterruptible signals are received like `SIGKILL`
23//! * The application is performing a write operation on the tempfile when a signal arrives, preventing this tempfile to be removed,
24//! but not others. Any other operation dealing with the tempfile suffers from the same issue.
25//!
26//! [signal-hook]: https://docs.rs/signal-hook
27#![deny(missing_docs, rust_2018_idioms, unsafe_code)]
28
29use std::{
30 io,
31 marker::PhantomData,
32 path::{Path, PathBuf},
33 sync::atomic::AtomicUsize,
34};
35
36use dashmap::DashMap;
37use once_cell::sync::Lazy;
38
39mod fs;
40pub use fs::{create_dir, remove_dir};
41
42pub mod handler;
43
44mod forksafe;
45use forksafe::ForksafeTempfile;
46
47pub mod handle;
48use crate::handle::{Closed, Writable};
49
50static SIGNAL_HANDLER_MODE: AtomicUsize = AtomicUsize::new(SignalHandlerMode::None as usize);
51static NEXT_MAP_INDEX: AtomicUsize = AtomicUsize::new(0);
52static REGISTER: Lazy<DashMap<usize, Option<ForksafeTempfile>>> = Lazy::new(|| {
53 let mode = SIGNAL_HANDLER_MODE.load(std::sync::atomic::Ordering::SeqCst);
54 if mode != SignalHandlerMode::None as usize {
55 for sig in signal_hook::consts::TERM_SIGNALS {
56 // SAFETY: handlers are considered unsafe because a lot can go wrong. See `cleanup_tempfiles()` for details on safety.
57 #[allow(unsafe_code)]
58 unsafe {
59 #[cfg(not(windows))]
60 {
61 signal_hook_registry::register_sigaction(*sig, handler::cleanup_tempfiles_nix)
62 }
63 #[cfg(windows)]
64 {
65 signal_hook::low_level::register(*sig, handler::cleanup_tempfiles_windows)
66 }
67 }
68 .expect("signals can always be installed");
69 }
70 }
71 DashMap::new()
72});
73
74/// Define how our signal handlers act
75#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)]
76pub enum SignalHandlerMode {
77 /// Do not install a signal handler at all, but have somebody else call our handler directly.
78 None = 0,
79 /// Delete all remaining registered tempfiles on termination.
80 DeleteTempfilesOnTermination = 1,
81 /// Delete all remaining registered tempfiles on termination and emulate the default handler behaviour.
82 ///
83 /// This typically leads to the process being aborted.
84 DeleteTempfilesOnTerminationAndRestoreDefaultBehaviour = 2,
85}
86
87impl Default for SignalHandlerMode {
88 /// By default we will emulate the default behaviour and abort the process.
89 ///
90 /// While testing, we will not abort the process.
91 fn default() -> Self {
92 if cfg!(test) {
93 SignalHandlerMode::DeleteTempfilesOnTermination
94 } else {
95 SignalHandlerMode::DeleteTempfilesOnTerminationAndRestoreDefaultBehaviour
96 }
97 }
98}
99
100/// A type expressing the ways we can deal with directories containing a tempfile.
101#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)]
102pub enum ContainingDirectory {
103 /// Assume the directory for the tempfile exists and cause failure if it doesn't
104 Exists,
105 /// Create the directory recursively with the given amount of retries in a way that is somewhat race resistant
106 /// depending on the amount of retries.
107 CreateAllRaceProof(create_dir::Retries),
108}
109
110/// A type expressing the ways we cleanup after ourselves to remove resources we created.
111/// Note that cleanup has no effect if the tempfile is persisted.
112#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
113pub enum AutoRemove {
114 /// Remove the temporary file after usage if it wasn't persisted.
115 Tempfile,
116 /// Remove the temporary file as well the containing directories if they are empty until the given `directory`.
117 TempfileAndEmptyParentDirectoriesUntil {
118 /// The directory which shall not be removed even if it is empty.
119 boundary_directory: PathBuf,
120 },
121}
122
123impl AutoRemove {
124 fn execute_best_effort(self, directory_to_potentially_delete: &Path) -> Option<PathBuf> {
125 match self {
126 AutoRemove::Tempfile => None,
127 AutoRemove::TempfileAndEmptyParentDirectoriesUntil { boundary_directory } => {
128 remove_dir::empty_upward_until_boundary(directory_to_potentially_delete, &boundary_directory).ok();
129 Some(boundary_directory)
130 }
131 }
132 }
133}
134
135/// A registered temporary file which will delete itself on drop or if the program is receiving signals that
136/// should cause it to terminate.
137///
138/// # Note
139///
140/// Signals interrupting the calling thread right after taking ownership of the registered tempfile
141/// will cause all but this tempfile to be removed automatically. In the common case it will persist on disk as destructors
142/// were not called or didn't get to remove the file.
143///
144/// In the best case the file is a true temporary with a non-clashing name that 'only' fills up the disk,
145/// in the worst case the temporary file is used as a lock file which may leave the repository in a locked
146/// state forever.
147///
148/// This kind of raciness exists whenever [`take()`][Handle::take()] is used and can't be circumvented.
149#[derive(Debug)]
150#[must_use = "A handle that is immediately dropped doesn't lock a resource meaningfully"]
151pub struct Handle<Marker: std::fmt::Debug> {
152 id: usize,
153 _marker: PhantomData<Marker>,
154}
155
156/// A shortcut to [`Handle::<Writable>::new()`], creating a writable temporary file with non-clashing name in a directory.
157pub fn new(
158 containing_directory: impl AsRef<Path>,
159 directory: ContainingDirectory,
160 cleanup: AutoRemove,
161) -> io::Result<Handle<Writable>> {
162 Handle::<Writable>::new(containing_directory, directory, cleanup)
163}
164
165/// A shortcut to [`Handle::<Writable>::at()`] providing a writable temporary file at the given path.
166pub fn writable_at(
167 path: impl AsRef<Path>,
168 directory: ContainingDirectory,
169 cleanup: AutoRemove,
170) -> io::Result<Handle<Writable>> {
171 Handle::<Writable>::at(path, directory, cleanup)
172}
173
174/// A shortcut to [`Handle::<Closed>::at()`] providing a closed temporary file to mark the presence of something.
175pub fn mark_at(
176 path: impl AsRef<Path>,
177 directory: ContainingDirectory,
178 cleanup: AutoRemove,
179) -> io::Result<Handle<Closed>> {
180 Handle::<Closed>::at(path, directory, cleanup)
181}
182
183/// Initialize signal handlers and other state to keep track of tempfiles, and **must be called before the first tempfile is created**,
184/// allowing to set the `mode` in which signal handlers are installed.
185///
186/// Only has an effect the first time it is called.
187///
188/// Note that it is possible to not call this function and instead call [handler::cleanup_tempfiles()][crate::handler::cleanup_tempfiles()]
189/// from a handler under the applications control.
190pub fn setup(mode: SignalHandlerMode) {
191 SIGNAL_HANDLER_MODE.store(mode as usize, std::sync::atomic::Ordering::SeqCst);
192 Lazy::force(®ISTER);
193}
194
195/// DO NOT USE - use [`setup()`] instead.
196///
197/// Indeed this is merely the old name of `setup()`, which is now a required part of configuring git-tempfile.
198#[deprecated(
199 since = "2.0.0",
200 note = "call setup(…) instead, this function will be removed in the next major release"
201)]
202#[doc(hidden)]
203pub fn force_setup(mode: SignalHandlerMode) {
204 setup(mode)
205}