1use std::{
2 fmt,
3 path::{Path, PathBuf},
4 time::Duration,
5};
6
7use gix_tempfile::{AutoRemove, ContainingDirectory};
8
9use crate::{DOT_LOCK_SUFFIX, File, Marker, backoff};
10
11#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
13pub enum Fail {
14 #[default]
16 Immediately,
17 AfterDurationWithBackoff(Duration),
20}
21
22impl fmt::Display for Fail {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Fail::Immediately => f.write_str("immediately"),
26 Fail::AfterDurationWithBackoff(duration) => {
27 write!(f, "after {:.02}s", duration.as_secs_f32())
28 }
29 }
30 }
31}
32
33impl From<Duration> for Fail {
34 fn from(value: Duration) -> Self {
35 if value.is_zero() {
36 Fail::Immediately
37 } else {
38 Fail::AfterDurationWithBackoff(value)
39 }
40 }
41}
42
43#[derive(Debug, thiserror::Error)]
45#[allow(missing_docs)]
46pub enum Error {
47 #[error("Another IO error occurred while obtaining the lock")]
48 Io(#[from] std::io::Error),
49 #[error(
50 "The lock for resource '{resource_path}' could not be obtained {mode} after {attempts} attempt(s). The lockfile at '{resource_path}{}' might need manual deletion.",
51 super::DOT_LOCK_SUFFIX
52 )]
53 PermanentlyLocked {
54 resource_path: PathBuf,
55 mode: Fail,
56 attempts: usize,
57 },
58}
59
60impl File {
61 pub fn acquire_to_update_resource(
74 at_path: impl AsRef<Path>,
75 mode: Fail,
76 boundary_directory: Option<PathBuf>,
77 ) -> Result<File, Error> {
78 let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
79 if let Some(permissions) = default_permissions() {
80 gix_tempfile::writable_at_with_permissions(p, d, c, permissions)
81 } else {
82 gix_tempfile::writable_at(p, d, c)
83 }
84 })?;
85 Ok(File {
86 inner: handle,
87 lock_path,
88 })
89 }
90
91 pub fn acquire_to_update_resource_with_permissions(
93 at_path: impl AsRef<Path>,
94 mode: Fail,
95 boundary_directory: Option<PathBuf>,
96 make_permissions: impl Fn() -> std::fs::Permissions,
97 ) -> Result<File, Error> {
98 let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
99 gix_tempfile::writable_at_with_permissions(p, d, c, make_permissions())
100 })?;
101 Ok(File {
102 inner: handle,
103 lock_path,
104 })
105 }
106}
107
108impl Marker {
109 pub fn acquire_to_hold_resource(
123 at_path: impl AsRef<Path>,
124 mode: Fail,
125 boundary_directory: Option<PathBuf>,
126 ) -> Result<Marker, Error> {
127 let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
128 if let Some(permissions) = default_permissions() {
129 gix_tempfile::mark_at_with_permissions(p, d, c, permissions)
130 } else {
131 gix_tempfile::mark_at(p, d, c)
132 }
133 })?;
134 Ok(Marker {
135 created_from_file: false,
136 inner: handle,
137 lock_path,
138 })
139 }
140
141 pub fn acquire_to_hold_resource_with_permissions(
143 at_path: impl AsRef<Path>,
144 mode: Fail,
145 boundary_directory: Option<PathBuf>,
146 make_permissions: impl Fn() -> std::fs::Permissions,
147 ) -> Result<Marker, Error> {
148 let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
149 gix_tempfile::mark_at_with_permissions(p, d, c, make_permissions())
150 })?;
151 Ok(Marker {
152 created_from_file: false,
153 inner: handle,
154 lock_path,
155 })
156 }
157}
158
159fn dir_cleanup(boundary: Option<PathBuf>) -> (ContainingDirectory, AutoRemove) {
160 match boundary {
161 None => (ContainingDirectory::Exists, AutoRemove::Tempfile),
162 Some(boundary_directory) => (
163 ContainingDirectory::CreateAllRaceProof(Default::default()),
164 AutoRemove::TempfileAndEmptyParentDirectoriesUntil { boundary_directory },
165 ),
166 }
167}
168
169fn lock_with_mode<T>(
170 resource: &Path,
171 mode: Fail,
172 boundary_directory: Option<PathBuf>,
173 try_lock: &dyn Fn(&Path, ContainingDirectory, AutoRemove) -> std::io::Result<T>,
174) -> Result<(PathBuf, T), Error> {
175 use std::io::ErrorKind::*;
176 let (directory, cleanup) = dir_cleanup(boundary_directory);
177 let lock_path = add_lock_suffix(resource);
178 let mut attempts = 1;
179 match mode {
180 Fail::Immediately => try_lock(&lock_path, directory, cleanup),
181 Fail::AfterDurationWithBackoff(time) => {
182 for wait in backoff::Quadratic::default_with_random().until_no_remaining(time) {
183 attempts += 1;
184 match try_lock(&lock_path, directory, cleanup.clone()) {
185 Ok(v) => return Ok((lock_path, v)),
186 #[cfg(windows)]
187 Err(err) if err.kind() == AlreadyExists || err.kind() == PermissionDenied => {
188 std::thread::sleep(wait);
189 continue;
190 }
191 #[cfg(not(windows))]
192 Err(err) if err.kind() == AlreadyExists => {
193 std::thread::sleep(wait);
194 continue;
195 }
196 Err(err) => return Err(Error::from(err)),
197 }
198 }
199 try_lock(&lock_path, directory, cleanup)
200 }
201 }
202 .map(|v| (lock_path, v))
203 .map_err(|err| match err.kind() {
204 AlreadyExists => Error::PermanentlyLocked {
205 resource_path: resource.into(),
206 mode,
207 attempts,
208 },
209 _ => Error::Io(err),
210 })
211}
212
213fn add_lock_suffix(resource_path: &Path) -> PathBuf {
214 resource_path.with_extension(resource_path.extension().map_or_else(
215 || DOT_LOCK_SUFFIX.chars().skip(1).collect(),
216 |ext| format!("{}{}", ext.to_string_lossy(), DOT_LOCK_SUFFIX),
217 ))
218}
219
220fn default_permissions() -> Option<std::fs::Permissions> {
221 #[cfg(unix)]
222 {
223 use std::os::unix::fs::PermissionsExt;
224 Some(std::fs::Permissions::from_mode(0o666))
225 }
226 #[cfg(not(unix))]
227 {
228 None
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn add_lock_suffix_to_file_with_extension() {
238 assert_eq!(add_lock_suffix(Path::new("hello.ext")), Path::new("hello.ext.lock"));
239 }
240
241 #[test]
242 fn add_lock_suffix_to_file_without_extension() {
243 assert_eq!(add_lock_suffix(Path::new("hello")), Path::new("hello.lock"));
244 }
245}