1use std::{
2 fmt,
3 path::{Path, PathBuf},
4 time::Duration,
5};
6
7use git_tempfile::{AutoRemove, ContainingDirectory};
8use quick_error::quick_error;
9
10use crate::{backoff, File, Marker, DOT_LOCK_SUFFIX};
11
12#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
14pub enum Fail {
15 Immediately,
17 AfterDurationWithBackoff(Duration),
20}
21
22impl Default for Fail {
23 fn default() -> Self {
24 Fail::Immediately
25 }
26}
27
28impl fmt::Display for Fail {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Fail::Immediately => f.write_str("immediately"),
32 Fail::AfterDurationWithBackoff(duration) => {
33 write!(f, "after {:.02}s", duration.as_secs_f32())
34 }
35 }
36 }
37}
38
39quick_error! {
40 #[derive(Debug)]
42 #[allow(missing_docs)]
43 pub enum Error {
44 Io(err: std::io::Error) {
45 display("Another IO error occurred while obtaining the lock")
46 from()
47 source(err)
48 }
49 PermanentlyLocked { resource_path: PathBuf, mode: Fail, attempts: usize } {
50 display("The lock for resource '{} could not be obtained {} after {} attempt(s). The lockfile at '{}{}' might need manual deletion.", resource_path.display(), mode, attempts, resource_path.display(), super::DOT_LOCK_SUFFIX)
51 }
52 }
53}
54
55impl File {
56 pub fn acquire_to_update_resource(
61 at_path: impl AsRef<Path>,
62 mode: Fail,
63 boundary_directory: Option<PathBuf>,
64 ) -> Result<File, Error> {
65 let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, |p, d, c| {
66 git_tempfile::writable_at(p, d, c)
67 })?;
68 Ok(File {
69 inner: handle,
70 lock_path,
71 })
72 }
73}
74
75impl Marker {
76 pub fn acquire_to_hold_resource(
82 at_path: impl AsRef<Path>,
83 mode: Fail,
84 boundary_directory: Option<PathBuf>,
85 ) -> Result<Marker, Error> {
86 let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, |p, d, c| {
87 git_tempfile::mark_at(p, d, c)
88 })?;
89 Ok(Marker {
90 created_from_file: false,
91 inner: handle,
92 lock_path,
93 })
94 }
95}
96
97fn dir_cleanup(boundary: Option<PathBuf>) -> (ContainingDirectory, AutoRemove) {
98 match boundary {
99 None => (ContainingDirectory::Exists, AutoRemove::Tempfile),
100 Some(boundary_directory) => (
101 ContainingDirectory::CreateAllRaceProof(Default::default()),
102 AutoRemove::TempfileAndEmptyParentDirectoriesUntil { boundary_directory },
103 ),
104 }
105}
106
107fn lock_with_mode<T>(
108 resource: &Path,
109 mode: Fail,
110 boundary_directory: Option<PathBuf>,
111 try_lock: impl Fn(&Path, ContainingDirectory, AutoRemove) -> std::io::Result<T>,
112) -> Result<(PathBuf, T), Error> {
113 use std::io::ErrorKind::*;
114 let (directory, cleanup) = dir_cleanup(boundary_directory);
115 let lock_path = add_lock_suffix(resource);
116 let mut attempts = 1;
117 match mode {
118 Fail::Immediately => try_lock(&lock_path, directory, cleanup),
119 Fail::AfterDurationWithBackoff(time) => {
120 for wait in backoff::Exponential::default_with_random().until_no_remaining(time) {
121 attempts += 1;
122 match try_lock(&lock_path, directory, cleanup.clone()) {
123 Ok(v) => return Ok((lock_path, v)),
124 #[cfg(windows)]
125 Err(err) if err.kind() == AlreadyExists || err.kind() == PermissionDenied => {
126 std::thread::sleep(wait);
127 continue;
128 }
129 #[cfg(not(windows))]
130 Err(err) if err.kind() == AlreadyExists => {
131 std::thread::sleep(wait);
132 continue;
133 }
134 Err(err) => return Err(Error::from(err)),
135 }
136 }
137 try_lock(&lock_path, directory, cleanup)
138 }
139 }
140 .map(|v| (lock_path, v))
141 .map_err(|err| match err.kind() {
142 AlreadyExists => Error::PermanentlyLocked {
143 resource_path: resource.into(),
144 mode,
145 attempts,
146 },
147 _ => Error::Io(err),
148 })
149}
150
151fn add_lock_suffix(resource_path: &Path) -> PathBuf {
152 resource_path.with_extension(resource_path.extension().map_or_else(
153 || DOT_LOCK_SUFFIX.chars().skip(1).collect(),
154 |ext| format!("{}{}", ext.to_string_lossy(), DOT_LOCK_SUFFIX),
155 ))
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn add_lock_suffix_to_file_with_extension() {
164 assert_eq!(add_lock_suffix(Path::new("hello.ext")), Path::new("hello.ext.lock"));
165 }
166
167 #[test]
168 fn add_lock_suffix_to_file_without_extension() {
169 assert_eq!(add_lock_suffix(Path::new("hello")), Path::new("hello.lock"));
170 }
171}