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#[cfg(windows)]
13use windows::Win32::Foundation::ERROR_LOCK_VIOLATION;
14
15use crate::{Simplified, is_known_already_locked_error};
16
17static LOCK_TIMEOUT: LazyLock<Duration> = LazyLock::new(|| {
19 let default_timeout = Duration::from_mins(5);
20 let Some(lock_timeout) = env::var_os(EnvVars::UV_LOCK_TIMEOUT) else {
21 return default_timeout;
22 };
23
24 if let Some(lock_timeout) = lock_timeout
25 .to_str()
26 .and_then(|lock_timeout| lock_timeout.parse::<u64>().ok())
27 {
28 Duration::from_secs(lock_timeout)
29 } else {
30 warn!(
31 "Could not parse value of {} as integer: {:?}",
32 EnvVars::UV_LOCK_TIMEOUT,
33 lock_timeout
34 );
35 default_timeout
36 }
37});
38
39#[derive(Debug, Error)]
40pub enum LockedFileError {
41 #[error(
42 "Timeout ({}s) when waiting for lock on `{}` at `{}`, is another uv process running? You can set `{}` to increase the timeout.",
43 timeout.as_secs(),
44 resource,
45 path.user_display(),
46 EnvVars::UV_LOCK_TIMEOUT
47 )]
48 Timeout {
49 timeout: Duration,
50 resource: String,
51 path: PathBuf,
52 },
53 #[error(
54 "Could not acquire lock for `{}` at `{}`",
55 resource,
56 path.user_display()
57 )]
58 Lock {
59 resource: String,
60 path: PathBuf,
61 #[source]
62 source: io::Error,
63 },
64 #[error(transparent)]
65 #[cfg(feature = "tokio")]
66 JoinError(#[from] tokio::task::JoinError),
67 #[error("Could not create temporary file")]
68 CreateTemporary(#[source] io::Error),
69 #[error("Could not persist temporary file `{}`", path.user_display())]
70 PersistTemporary {
71 path: PathBuf,
72 #[source]
73 source: io::Error,
74 },
75 #[error(transparent)]
76 Io(#[from] io::Error),
77}
78
79impl LockedFileError {
80 pub fn as_io_error(&self) -> Option<&io::Error> {
81 match self {
82 Self::Timeout { .. } => None,
83 #[cfg(feature = "tokio")]
84 Self::JoinError(_) => None,
85 Self::Lock { source, .. } => Some(source),
86 Self::CreateTemporary(err) => Some(err),
87 Self::PersistTemporary { source, .. } => Some(source),
88 Self::Io(err) => Some(err),
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy)]
95pub enum LockedFileMode {
96 Shared,
97 Exclusive,
98}
99
100impl LockedFileMode {
101 #[cfg(not(target_os = "android"))]
109 fn try_lock(self, file: &fs_err::File) -> Result<(), std::fs::TryLockError> {
110 match self {
111 Self::Exclusive => file.try_lock()?,
112 Self::Shared => file.try_lock_shared()?,
113 }
114 Ok(())
115 }
116
117 #[cfg(target_os = "android")]
126 fn try_lock(self, file: &fs_err::File) -> Result<(), std::fs::TryLockError> {
127 use std::os::fd::AsFd;
128
129 let operation = match self {
130 Self::Exclusive => rustix::fs::FlockOperation::NonBlockingLockExclusive,
131 Self::Shared => rustix::fs::FlockOperation::NonBlockingLockShared,
132 };
133 rustix::fs::flock(file.as_fd(), operation).map_err(|errno| {
134 if errno == rustix::io::Errno::WOULDBLOCK {
135 std::fs::TryLockError::WouldBlock
136 } else {
137 std::fs::TryLockError::Error(io::Error::from_raw_os_error(errno.raw_os_error()))
138 }
139 })
140 }
141
142 #[cfg(not(target_os = "android"))]
149 fn lock(self, file: &fs_err::File) -> Result<(), io::Error> {
150 match self {
151 Self::Exclusive => file.lock()?,
152 Self::Shared => file.lock_shared()?,
153 }
154 Ok(())
155 }
156
157 #[cfg(target_os = "android")]
165 fn lock(self, file: &fs_err::File) -> Result<(), io::Error> {
166 use std::os::fd::AsFd;
167
168 let operation = match self {
169 Self::Exclusive => rustix::fs::FlockOperation::LockExclusive,
170 Self::Shared => rustix::fs::FlockOperation::LockShared,
171 };
172 rustix::fs::flock(file.as_fd(), operation)
173 .map_err(|errno| io::Error::from_raw_os_error(errno.raw_os_error()))
174 }
175}
176
177impl Display for LockedFileMode {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 match self {
180 Self::Shared => write!(f, "shared"),
181 Self::Exclusive => write!(f, "exclusive"),
182 }
183 }
184}
185
186#[cfg(feature = "tokio")]
188#[derive(Debug)]
189#[must_use]
190pub struct LockedFile(fs_err::File);
191
192#[cfg(feature = "tokio")]
193impl LockedFile {
194 async fn lock_file(
196 file: fs_err::File,
197 mode: LockedFileMode,
198 resource: &str,
199 ) -> Result<Self, LockedFileError> {
200 trace!(
201 "Checking lock for `{resource}` at `{}`",
202 file.path().user_display()
203 );
204 let try_lock_exclusive = tokio::task::spawn_blocking(move || (mode.try_lock(&file), file));
206 let file = match try_lock_exclusive.await? {
207 (Ok(()), file) => {
208 trace!("Acquired {mode} lock for `{resource}`");
209 return Ok(Self(file));
210 }
211 (Err(err), file) => {
212 if !is_known_already_locked_error(&err) {
214 debug!("Try lock {mode} error: {err:?}");
215 }
216 file
217 }
218 };
219
220 info!(
222 "Waiting to acquire {mode} lock for `{resource}` at `{}`",
223 file.path().user_display(),
224 );
225 let path = file.path().to_path_buf();
226 let lock_exclusive = tokio::task::spawn_blocking(move || (mode.lock(&file), file));
227 let (result, file) = tokio::time::timeout(*LOCK_TIMEOUT, lock_exclusive)
228 .await
229 .map_err(|_| LockedFileError::Timeout {
230 timeout: *LOCK_TIMEOUT,
231 resource: resource.to_string(),
232 path: path.clone(),
233 })??;
234 result.map_err(|err| LockedFileError::Lock {
236 resource: resource.to_string(),
237 path,
238 source: err,
239 })?;
240
241 trace!("Acquired {mode} lock for `{resource}`");
242 Ok(Self(file))
243 }
244
245 fn lock_file_no_wait(file: fs_err::File, mode: LockedFileMode, resource: &str) -> Option<Self> {
247 trace!(
248 "Checking lock for `{resource}` at `{}`",
249 file.path().user_display()
250 );
251 match mode.try_lock(&file) {
252 Ok(()) => {
253 trace!("Acquired {mode} lock for `{resource}`");
254 Some(Self(file))
255 }
256 Err(err) => {
257 if !is_known_already_locked_error(&err) {
259 debug!("Try lock error: {err:?}");
260 }
261 debug!("Lock is busy for `{resource}`");
262 None
263 }
264 }
265 }
266
267 pub async fn acquire(
269 path: impl AsRef<Path>,
270 mode: LockedFileMode,
271 resource: impl Display,
272 ) -> Result<Self, LockedFileError> {
273 let file = Self::create(&path)?;
274 let resource = resource.to_string();
275 Self::lock_file(file, mode, &resource).await
276 }
277
278 pub fn acquire_no_wait(
284 path: impl AsRef<Path>,
285 mode: LockedFileMode,
286 resource: impl Display,
287 ) -> Option<Self> {
288 let file = Self::create(path).ok()?;
289 let resource = resource.to_string();
290 Self::lock_file_no_wait(file, mode, &resource)
291 }
292
293 #[cfg(unix)]
294 fn create(path: impl AsRef<Path>) -> Result<fs_err::File, LockedFileError> {
295 use rustix::io::Errno;
296 #[expect(clippy::disallowed_types)]
297 use std::{fs::File, os::unix::fs::PermissionsExt};
298 use tempfile::NamedTempFile;
299
300 const DESIRED_MODE: u32 = 0o666;
302
303 #[expect(clippy::disallowed_types)]
304 fn try_set_permissions(file: &File, path: &Path) {
305 if let Err(err) = file.set_permissions(std::fs::Permissions::from_mode(DESIRED_MODE)) {
306 warn!(
307 "Failed to set permissions on temporary file `{path}`: {err}",
308 path = path.user_display()
309 );
310 }
311 }
312
313 if let Ok(file) = fs_err::OpenOptions::new()
315 .read(true)
316 .write(true)
317 .open(path.as_ref())
318 {
319 return Ok(file);
320 }
321
322 let file = if let Some(parent) = path.as_ref().parent() {
325 NamedTempFile::new_in(parent)
326 } else {
327 NamedTempFile::new()
328 }
329 .map_err(LockedFileError::CreateTemporary)?;
330 try_set_permissions(file.as_file(), file.path());
331
332 match file.persist_noclobber(path.as_ref()) {
334 Ok(file) => Ok(fs_err::File::from_parts(file, path.as_ref())),
335 Err(err) => {
336 if err.error.kind() == std::io::ErrorKind::AlreadyExists {
337 fs_err::OpenOptions::new()
338 .read(true)
339 .write(true)
340 .open(path.as_ref())
341 .map_err(Into::into)
342 } else if matches!(
343 Errno::from_io_error(&err.error),
344 Some(Errno::NOTSUP | Errno::INVAL)
345 ) {
346 let file = fs_err::OpenOptions::new()
360 .read(true)
361 .write(true)
362 .create(true)
363 .open(path.as_ref())?;
364
365 if file
370 .metadata()
371 .is_ok_and(|metadata| metadata.permissions().mode() != DESIRED_MODE)
372 {
373 try_set_permissions(file.file(), path.as_ref());
374 }
375 Ok(file)
376 } else {
377 let temp_path = err.file.into_temp_path();
378 Err(LockedFileError::PersistTemporary {
379 path: <tempfile::TempPath as AsRef<Path>>::as_ref(&temp_path).to_path_buf(),
380 source: err.error,
381 })
382 }
383 }
384 }
385 }
386
387 #[cfg(not(unix))]
388 fn create(path: impl AsRef<Path>) -> Result<fs_err::File, LockedFileError> {
389 fs_err::OpenOptions::new()
390 .read(true)
391 .write(true)
392 .create(true)
393 .open(path.as_ref())
394 .map_err(Into::into)
395 }
396
397 #[cfg(not(target_os = "android"))]
404 fn unlock(&self) -> Result<(), io::Error> {
405 self.0.unlock()
406 }
407
408 #[cfg(target_os = "android")]
416 fn unlock(&self) -> Result<(), io::Error> {
417 use std::os::fd::AsFd;
418
419 rustix::fs::flock(self.0.as_fd(), rustix::fs::FlockOperation::Unlock)
420 .map_err(|errno| io::Error::from_raw_os_error(errno.raw_os_error()))
421 }
422}
423
424#[cfg(feature = "tokio")]
425impl Drop for LockedFile {
426 fn drop(&mut self) {
427 match self.unlock() {
428 Ok(()) => {
429 trace!("Released lock at `{}`", self.0.path().display());
430 }
431 #[cfg(windows)]
433 Err(err)
434 if uv_windows::is_wine()
435 && err.raw_os_error() == Some(ERROR_LOCK_VIOLATION.0.cast_signed()) =>
436 {
437 trace!("Released lock at `{}`", self.0.path().display());
438 }
439 Err(err) => {
440 error!(
441 "Failed to unlock resource at `{}`; program may be stuck: {err}",
442 self.0.path().display()
443 );
444 }
445 }
446 }
447}