1use std::{
2 fmt,
3 path::{Path, PathBuf},
4 time::Duration,
5};
6
7use gix_tempfile::{AutoRemove, ContainingDirectory};
8
9use crate::{backoff, File, Marker, DOT_LOCK_SUFFIX};
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("The lock for resource '{resource_path}' could not be obtained {mode} after {attempts} attempt(s). The lockfile at '{resource_path}{}' might need manual deletion.", super::DOT_LOCK_SUFFIX)]
50 PermanentlyLocked {
51 resource_path: PathBuf,
52 mode: Fail,
53 attempts: usize,
54 },
55}
56
57impl File {
58 pub fn acquire_to_update_resource(
71 at_path: impl AsRef<Path>,
72 mode: Fail,
73 boundary_directory: Option<PathBuf>,
74 ) -> Result<File, Error> {
75 let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
76 if let Some(permissions) = default_permissions() {
77 gix_tempfile::writable_at_with_permissions(p, d, c, permissions)
78 } else {
79 gix_tempfile::writable_at(p, d, c)
80 }
81 })?;
82 Ok(File {
83 inner: handle,
84 lock_path,
85 })
86 }
87
88 pub fn acquire_to_update_resource_with_permissions(
90 at_path: impl AsRef<Path>,
91 mode: Fail,
92 boundary_directory: Option<PathBuf>,
93 make_permissions: impl Fn() -> std::fs::Permissions,
94 ) -> Result<File, Error> {
95 let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
96 gix_tempfile::writable_at_with_permissions(p, d, c, make_permissions())
97 })?;
98 Ok(File {
99 inner: handle,
100 lock_path,
101 })
102 }
103}
104
105impl Marker {
106 pub fn acquire_to_hold_resource(
120 at_path: impl AsRef<Path>,
121 mode: Fail,
122 boundary_directory: Option<PathBuf>,
123 ) -> Result<Marker, Error> {
124 let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
125 if let Some(permissions) = default_permissions() {
126 gix_tempfile::mark_at_with_permissions(p, d, c, permissions)
127 } else {
128 gix_tempfile::mark_at(p, d, c)
129 }
130 })?;
131 Ok(Marker {
132 created_from_file: false,
133 inner: handle,
134 lock_path,
135 })
136 }
137
138 pub fn acquire_to_hold_resource_with_permissions(
140 at_path: impl AsRef<Path>,
141 mode: Fail,
142 boundary_directory: Option<PathBuf>,
143 make_permissions: impl Fn() -> std::fs::Permissions,
144 ) -> Result<Marker, Error> {
145 let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
146 gix_tempfile::mark_at_with_permissions(p, d, c, make_permissions())
147 })?;
148 Ok(Marker {
149 created_from_file: false,
150 inner: handle,
151 lock_path,
152 })
153 }
154}
155
156fn dir_cleanup(boundary: Option<PathBuf>) -> (ContainingDirectory, AutoRemove) {
157 match boundary {
158 None => (ContainingDirectory::Exists, AutoRemove::Tempfile),
159 Some(boundary_directory) => (
160 ContainingDirectory::CreateAllRaceProof(Default::default()),
161 AutoRemove::TempfileAndEmptyParentDirectoriesUntil { boundary_directory },
162 ),
163 }
164}
165
166fn lock_with_mode<T>(
167 resource: &Path,
168 mode: Fail,
169 boundary_directory: Option<PathBuf>,
170 try_lock: &dyn Fn(&Path, ContainingDirectory, AutoRemove) -> std::io::Result<T>,
171) -> Result<(PathBuf, T), Error> {
172 use std::io::ErrorKind::*;
173 let (directory, cleanup) = dir_cleanup(boundary_directory);
174 let lock_path = add_lock_suffix(resource);
175 let mut attempts = 1;
176 match mode {
177 Fail::Immediately => try_lock(&lock_path, directory, cleanup),
178 Fail::AfterDurationWithBackoff(time) => {
179 for wait in backoff::Exponential::default_with_random().until_no_remaining(time) {
180 attempts += 1;
181 match try_lock(&lock_path, directory, cleanup.clone()) {
182 Ok(v) => return Ok((lock_path, v)),
183 #[cfg(windows)]
184 Err(err) if err.kind() == AlreadyExists || err.kind() == PermissionDenied => {
185 std::thread::sleep(wait);
186 continue;
187 }
188 #[cfg(not(windows))]
189 Err(err) if err.kind() == AlreadyExists => {
190 std::thread::sleep(wait);
191 continue;
192 }
193 Err(err) => return Err(Error::from(err)),
194 }
195 }
196 try_lock(&lock_path, directory, cleanup)
197 }
198 }
199 .map(|v| (lock_path, v))
200 .map_err(|err| match err.kind() {
201 AlreadyExists => Error::PermanentlyLocked {
202 resource_path: resource.into(),
203 mode,
204 attempts,
205 },
206 _ => Error::Io(err),
207 })
208}
209
210fn add_lock_suffix(resource_path: &Path) -> PathBuf {
211 resource_path.with_extension(resource_path.extension().map_or_else(
212 || DOT_LOCK_SUFFIX.chars().skip(1).collect(),
213 |ext| format!("{}{}", ext.to_string_lossy(), DOT_LOCK_SUFFIX),
214 ))
215}
216
217fn default_permissions() -> Option<std::fs::Permissions> {
218 #[cfg(unix)]
219 {
220 use std::os::unix::fs::PermissionsExt;
221 Some(std::fs::Permissions::from_mode(0o666))
222 }
223 #[cfg(not(unix))]
224 {
225 None
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn add_lock_suffix_to_file_with_extension() {
235 assert_eq!(add_lock_suffix(Path::new("hello.ext")), Path::new("hello.ext.lock"));
236 }
237
238 #[test]
239 fn add_lock_suffix_to_file_without_extension() {
240 assert_eq!(add_lock_suffix(Path::new("hello")), Path::new("hello.lock"));
241 }
242}