jj_lib/lock/unix.rs
1// Copyright 2023 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![expect(missing_docs)]
16
17use std::fs::File;
18use std::path::PathBuf;
19
20use rustix::fs::FlockOperation;
21use tracing::instrument;
22
23use super::FileLockError;
24
25pub struct FileLock {
26 path: PathBuf,
27 file: File,
28}
29
30impl FileLock {
31 /// Acquire an exclusive lock on `path`, blocking until it's available.
32 pub fn lock(path: PathBuf) -> Result<Self, FileLockError> {
33 // In blocking mode, `lock_inner` never returns `Ok(None)`.
34 Ok(Self::lock_inner(path, true)?.expect("blocking lock should return a lock"))
35 }
36
37 /// Try to acquire an exclusive lock on `path` without blocking. Returns
38 /// `Ok(None)` if the lock is currently held by another process.
39 pub fn try_lock(path: PathBuf) -> Result<Option<Self>, FileLockError> {
40 Self::lock_inner(path, false)
41 }
42
43 fn lock_inner(path: PathBuf, blocking: bool) -> Result<Option<Self>, FileLockError> {
44 tracing::info!("Attempting to lock {path:?}");
45 let operation = if blocking {
46 FlockOperation::LockExclusive
47 } else {
48 FlockOperation::NonBlockingLockExclusive
49 };
50 loop {
51 // Create lockfile, or open pre-existing one
52 let file = File::create(&path).map_err(|err| FileLockError {
53 message: "Failed to open lock file",
54 path: path.clone(),
55 err,
56 })?;
57 // If the lock was already held, block until it's released, or (in
58 // non-blocking mode) report that it's currently unavailable.
59 match rustix::fs::flock(&file, operation) {
60 Ok(()) => {}
61 Err(rustix::io::Errno::WOULDBLOCK) if !blocking => return Ok(None),
62 Err(errno) => {
63 return Err(FileLockError {
64 message: "Failed to lock lock file",
65 path: path.clone(),
66 err: errno.into(),
67 });
68 }
69 }
70
71 match rustix::fs::fstat(&file) {
72 Ok(stat) => {
73 if stat.st_nlink == 0 {
74 // Lockfile was deleted, probably by the previous holder's `Drop` impl;
75 // create a new one so our ownership is visible,
76 // rather than hidden in an unlinked file. Not
77 // always necessary, since the previous holder might
78 // have exited abruptly.
79 continue;
80 }
81 }
82 Err(rustix::io::Errno::STALE) => {
83 // The file handle is stale.
84 // This can happen when using NFS,
85 // likely caused by a remote deletion of the lockfile.
86 // Treat this like a normal lockfile deletion and retry.
87 continue;
88 }
89 Err(errno) => {
90 return Err(FileLockError {
91 message: "failed to stat lock file",
92 path: path.clone(),
93 err: errno.into(),
94 });
95 }
96 }
97
98 tracing::info!("Locked {path:?}");
99 return Ok(Some(Self { path, file }));
100 }
101 }
102}
103
104impl Drop for FileLock {
105 #[instrument(skip_all)]
106 fn drop(&mut self) {
107 // Removing the file isn't strictly necessary, but reduces confusion.
108 std::fs::remove_file(&self.path).ok();
109 // Unblock any processes that tried to acquire the lock while we held it.
110 // They're responsible for creating and locking a new lockfile, since we
111 // just deleted this one.
112 rustix::fs::flock(&self.file, FlockOperation::Unlock).ok();
113 }
114}