1use std::convert::Into;
2use std::fmt::Display;
3use std::path::{Path, PathBuf};
4use std::sync::LazyLock;
5use std::time::Duration;
6use std::{env, io};
7
8use thiserror::Error;
9use tracing::{debug, error, info, trace, warn};
10
11use uv_static::EnvVars;
12
13use crate::{Simplified, is_known_already_locked_error};
14
15static LOCK_TIMEOUT: LazyLock<Duration> = LazyLock::new(|| {
17 let default_timeout = Duration::from_secs(300);
18 let Some(lock_timeout) = env::var_os(EnvVars::UV_LOCK_TIMEOUT) else {
19 return default_timeout;
20 };
21
22 if let Some(lock_timeout) = lock_timeout
23 .to_str()
24 .and_then(|lock_timeout| lock_timeout.parse::<u64>().ok())
25 {
26 Duration::from_secs(lock_timeout)
27 } else {
28 warn!(
29 "Could not parse value of {} as integer: {:?}",
30 EnvVars::UV_LOCK_TIMEOUT,
31 lock_timeout
32 );
33 default_timeout
34 }
35});
36
37#[derive(Debug, Error)]
38pub enum LockedFileError {
39 #[error(
40 "Timeout ({}s) when waiting for lock on `{}` at `{}`, is another uv process running? You can set `{}` to increase the timeout.",
41 timeout.as_secs(),
42 resource,
43 path.user_display(),
44 EnvVars::UV_LOCK_TIMEOUT
45 )]
46 Timeout {
47 timeout: Duration,
48 resource: String,
49 path: PathBuf,
50 },
51 #[error(
52 "Could not acquire lock for `{}` at `{}`",
53 resource,
54 path.user_display()
55 )]
56 Lock {
57 resource: String,
58 path: PathBuf,
59 #[source]
60 source: io::Error,
61 },
62 #[error(transparent)]
63 #[cfg(feature = "tokio")]
64 JoinError(#[from] tokio::task::JoinError),
65 #[error("Could not create temporary file")]
66 CreateTemporary(#[source] io::Error),
67 #[error("Could not persist temporary file `{}`", path.user_display())]
68 PersistTemporary {
69 path: PathBuf,
70 #[source]
71 source: io::Error,
72 },
73 #[error(transparent)]
74 Io(#[from] io::Error),
75}
76
77impl LockedFileError {
78 pub fn as_io_error(&self) -> Option<&io::Error> {
79 match self {
80 Self::Timeout { .. } => None,
81 #[cfg(feature = "tokio")]
82 Self::JoinError(_) => None,
83 Self::Lock { source, .. } => Some(source),
84 Self::CreateTemporary(err) => Some(err),
85 Self::PersistTemporary { source, .. } => Some(source),
86 Self::Io(err) => Some(err),
87 }
88 }
89}
90
91#[derive(Debug, Clone, Copy)]
93pub enum LockedFileMode {
94 Shared,
95 Exclusive,
96}
97
98impl LockedFileMode {
99 fn try_lock(self, file: &fs_err::File) -> Result<(), std::fs::TryLockError> {
102 match self {
103 Self::Exclusive => file.try_lock()?,
104 Self::Shared => file.try_lock_shared()?,
105 }
106 Ok(())
107 }
108
109 fn lock(self, file: &fs_err::File) -> Result<(), io::Error> {
111 match self {
112 Self::Exclusive => file.lock()?,
113 Self::Shared => file.lock_shared()?,
114 }
115 Ok(())
116 }
117}
118
119impl Display for LockedFileMode {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 match self {
122 Self::Shared => write!(f, "shared"),
123 Self::Exclusive => write!(f, "exclusive"),
124 }
125 }
126}
127
128#[cfg(feature = "tokio")]
130#[derive(Debug)]
131#[must_use]
132pub struct LockedFile(fs_err::File);
133
134#[cfg(feature = "tokio")]
135impl LockedFile {
136 async fn lock_file(
138 file: fs_err::File,
139 mode: LockedFileMode,
140 resource: &str,
141 ) -> Result<Self, LockedFileError> {
142 trace!(
143 "Checking lock for `{resource}` at `{}`",
144 file.path().user_display()
145 );
146 let try_lock_exclusive = tokio::task::spawn_blocking(move || (mode.try_lock(&file), file));
148 let file = match try_lock_exclusive.await? {
149 (Ok(()), file) => {
150 debug!("Acquired {mode} lock for `{resource}`");
151 return Ok(Self(file));
152 }
153 (Err(err), file) => {
154 if !is_known_already_locked_error(&err) {
156 debug!("Try lock {mode} error: {err:?}");
157 }
158 file
159 }
160 };
161
162 info!(
164 "Waiting to acquire {mode} lock for `{resource}` at `{}`",
165 file.path().user_display(),
166 );
167 let path = file.path().to_path_buf();
168 let lock_exclusive = tokio::task::spawn_blocking(move || (mode.lock(&file), file));
169 let (result, file) = tokio::time::timeout(*LOCK_TIMEOUT, lock_exclusive)
170 .await
171 .map_err(|_| LockedFileError::Timeout {
172 timeout: *LOCK_TIMEOUT,
173 resource: resource.to_string(),
174 path: path.clone(),
175 })??;
176 result.map_err(|err| LockedFileError::Lock {
178 resource: resource.to_string(),
179 path,
180 source: err,
181 })?;
182
183 debug!("Acquired {mode} lock for `{resource}`");
184 Ok(Self(file))
185 }
186
187 fn lock_file_no_wait(file: fs_err::File, mode: LockedFileMode, resource: &str) -> Option<Self> {
189 trace!(
190 "Checking lock for `{resource}` at `{}`",
191 file.path().user_display()
192 );
193 match mode.try_lock(&file) {
194 Ok(()) => {
195 debug!("Acquired {mode} lock for `{resource}`");
196 Some(Self(file))
197 }
198 Err(err) => {
199 if !is_known_already_locked_error(&err) {
201 debug!("Try lock error: {err:?}");
202 }
203 debug!("Lock is busy for `{resource}`");
204 None
205 }
206 }
207 }
208
209 pub async fn acquire(
211 path: impl AsRef<Path>,
212 mode: LockedFileMode,
213 resource: impl Display,
214 ) -> Result<Self, LockedFileError> {
215 let file = Self::create(&path)?;
216 let resource = resource.to_string();
217 Self::lock_file(file, mode, &resource).await
218 }
219
220 pub fn acquire_no_wait(
226 path: impl AsRef<Path>,
227 mode: LockedFileMode,
228 resource: impl Display,
229 ) -> Option<Self> {
230 let file = Self::create(path).ok()?;
231 let resource = resource.to_string();
232 Self::lock_file_no_wait(file, mode, &resource)
233 }
234
235 #[cfg(unix)]
236 fn create(path: impl AsRef<Path>) -> Result<fs_err::File, LockedFileError> {
237 use rustix::io::Errno;
238 #[allow(clippy::disallowed_types)]
239 use std::{fs::File, os::unix::fs::PermissionsExt};
240 use tempfile::NamedTempFile;
241
242 const DESIRED_MODE: u32 = 0o666;
244
245 #[allow(clippy::disallowed_types)]
246 fn try_set_permissions(file: &File, path: &Path) {
247 if let Err(err) = file.set_permissions(std::fs::Permissions::from_mode(DESIRED_MODE)) {
248 warn!(
249 "Failed to set permissions on temporary file `{path}`: {err}",
250 path = path.user_display()
251 );
252 }
253 }
254
255 if let Ok(file) = fs_err::OpenOptions::new()
257 .read(true)
258 .write(true)
259 .open(path.as_ref())
260 {
261 return Ok(file);
262 }
263
264 let file = if let Some(parent) = path.as_ref().parent() {
267 NamedTempFile::new_in(parent)
268 } else {
269 NamedTempFile::new()
270 }
271 .map_err(LockedFileError::CreateTemporary)?;
272 try_set_permissions(file.as_file(), file.path());
273
274 match file.persist_noclobber(path.as_ref()) {
276 Ok(file) => Ok(fs_err::File::from_parts(file, path.as_ref())),
277 Err(err) => {
278 if err.error.kind() == std::io::ErrorKind::AlreadyExists {
279 fs_err::OpenOptions::new()
280 .read(true)
281 .write(true)
282 .open(path.as_ref())
283 .map_err(Into::into)
284 } else if matches!(
285 Errno::from_io_error(&err.error),
286 Some(Errno::NOTSUP | Errno::INVAL)
287 ) {
288 let file = fs_err::OpenOptions::new()
302 .read(true)
303 .write(true)
304 .create(true)
305 .open(path.as_ref())?;
306
307 if file
312 .metadata()
313 .is_ok_and(|metadata| metadata.permissions().mode() != DESIRED_MODE)
314 {
315 try_set_permissions(file.file(), path.as_ref());
316 }
317 Ok(file)
318 } else {
319 let temp_path = err.file.into_temp_path();
320 Err(LockedFileError::PersistTemporary {
321 path: <tempfile::TempPath as AsRef<Path>>::as_ref(&temp_path).to_path_buf(),
322 source: err.error,
323 })
324 }
325 }
326 }
327 }
328
329 #[cfg(not(unix))]
330 fn create(path: impl AsRef<Path>) -> Result<fs_err::File, LockedFileError> {
331 fs_err::OpenOptions::new()
332 .read(true)
333 .write(true)
334 .create(true)
335 .open(path.as_ref())
336 .map_err(Into::into)
337 }
338}
339
340#[cfg(feature = "tokio")]
341impl Drop for LockedFile {
342 fn drop(&mut self) {
344 if let Err(err) = self.0.unlock() {
345 error!(
346 "Failed to unlock resource at `{}`; program may be stuck: {err}",
347 self.0.path().display()
348 );
349 } else {
350 debug!("Released lock at `{}`", self.0.path().display());
351 }
352 }
353}