// Copyright 2020 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.//! Windows file locking implementation using std's `File::lock()` which maps//! to `LockFileEx`.use std::fs::File;use std::fs::OpenOptions;use std::fs::TryLockError;use std::io;use std::os::windows::fs::OpenOptionsExt as _;use std::path::Path;use std::path::PathBuf;use std::thread;use tracing::instrument;use super::FileLockError;use super::backoff::BackoffIterator;const FILE_SHARE_READ: u32 = 1;const FILE_SHARE_WRITE: u32 = 2;const ERROR_SHARING_VIOLATION: u32 = 32;pub struct FileLock { path: PathBuf, /// `Option` so `Drop` can close the handle before deleting. file: Option<File>,}impl FileLock { /// Acquire an exclusive lock on `path`, blocking until it's available. pub fn lock(path: PathBuf) -> Result<Self, FileLockError> { // In blocking mode, `lock_inner` never returns `Ok(None)`. Ok(Self::lock_inner(path, true)?.expect("blocking lock should return a lock")) } /// Try to acquire an exclusive lock on `path` without blocking. Returns /// `Ok(None)` if the lock is currently held by another process. pub fn try_lock(path: PathBuf) -> Result<Option<Self>, FileLockError> { Self::lock_inner(path, false) } fn lock_inner(path: PathBuf, blocking: bool) -> Result<Option<Self>, FileLockError> { tracing::info!("Attempting to lock {path:?}"); let file = try_create_file(&path).map_err(|err| FileLockError { message: "Failed to open lock file", path: path.clone(), err, })?; if blocking { // Acquire exclusive lock (blocks until available) file.lock().map_err(|err| FileLockError { message: "Failed to lock lock file", path: path.clone(), err, })?; } else { // Acquire the lock, or report that another process holds it. match file.try_lock() { Ok(()) => {} Err(TryLockError::WouldBlock) => return Ok(None), Err(TryLockError::Error(err)) => { return Err(FileLockError { message: "Failed to lock lock file", path, err, }); } } } tracing::info!("Locked {path:?}"); Ok(Some(Self { path, file: Some(file), })) }}impl Drop for FileLock { #[instrument(skip_all)] fn drop(&mut self) { if let Some(file) = self.file.take() { file.unlock() .inspect_err(|err| tracing::warn!(?err, ?self.path, "Failed to unlock lock file")) .ok(); // file is dropped here, closing the handle so the delete below // can succeed. Another process holding the file open prevents // deletion (they open without FILE_SHARE_DELETE), avoiding races. } std::fs::remove_file(&self.path).ok(); }}fn try_create_file(path: &Path) -> io::Result<File> { let mut backoff_iterator = BackoffIterator::new(); let mut options = OpenOptions::new(); options .create(true) .write(true) // Don't share delete access. This ensures that std::fs::remove_file // (which uses DeleteFileW) will fail if any other process has the file // open — so deletion in Drop only succeeds when we're the last handle // holder. .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE); loop { match options.open(path) { Ok(file) => return Ok(file), // The file may be open for deletion in Drop. Err(err) if err.kind() == io::ErrorKind::PermissionDenied || err.raw_os_error() == Some(ERROR_SHARING_VIOLATION as _) => { let Some(duration) = backoff_iterator.next() else { return Err(err); }; // Ensure that a file can be created in the target directory. tempfile::tempfile_in(path.parent().expect("file path should have parent"))?; thread::sleep(duration); } Err(err) => return Err(err), } }}
// Copyright 2020 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Windows file locking implementation using std's `File::lock()` which maps
//! to `LockFileEx`.
use std::fs::File;
use std::fs::OpenOptions;
use std::fs::TryLockError;
use std::io;
use std::os::windows::fs::OpenOptionsExt as _;
use std::path::Path;
use std::path::PathBuf;
use std::thread;
use tracing::instrument;
use super::FileLockError;
use super::backoff::BackoffIterator;
const FILE_SHARE_READ: u32 = 1;
const FILE_SHARE_WRITE: u32 = 2;
const ERROR_SHARING_VIOLATION: u32 = 32;
pub struct FileLock {
path: PathBuf,
/// `Option` so `Drop` can close the handle before deleting.
file: Option<File>,
}
impl FileLock {
/// Acquire an exclusive lock on `path`, blocking until it's available.
pub fn lock(path: PathBuf) -> Result<Self, FileLockError> {
// In blocking mode, `lock_inner` never returns `Ok(None)`.
Ok(Self::lock_inner(path, true)?.expect("blocking lock should return a lock"))
}
/// Try to acquire an exclusive lock on `path` without blocking. Returns
/// `Ok(None)` if the lock is currently held by another process.
pub fn try_lock(path: PathBuf) -> Result<Option<Self>, FileLockError> {
Self::lock_inner(path, false)
}
fn lock_inner(path: PathBuf, blocking: bool) -> Result<Option<Self>, FileLockError> {
tracing::info!("Attempting to lock {path:?}");
let file = try_create_file(&path).map_err(|err| FileLockError {
message: "Failed to open lock file",
path: path.clone(),
err,
})?;
if blocking {
// Acquire exclusive lock (blocks until available)
file.lock().map_err(|err| FileLockError {
message: "Failed to lock lock file",
path: path.clone(),
err,
})?;
} else {
// Acquire the lock, or report that another process holds it.
match file.try_lock() {
Ok(()) => {}
Err(TryLockError::WouldBlock) => return Ok(None),
Err(TryLockError::Error(err)) => {
return Err(FileLockError {
message: "Failed to lock lock file",
path,
err,
});
}
}
}
tracing::info!("Locked {path:?}");
Ok(Some(Self {
path,
file: Some(file),
}))
}
}
impl Drop for FileLock {
#[instrument(skip_all)]
fn drop(&mut self) {
if let Some(file) = self.file.take() {
file.unlock()
.inspect_err(|err| tracing::warn!(?err, ?self.path, "Failed to unlock lock file"))
.ok();
// file is dropped here, closing the handle so the delete below
// can succeed. Another process holding the file open prevents
// deletion (they open without FILE_SHARE_DELETE), avoiding races.
}
std::fs::remove_file(&self.path).ok();
}
}
fn try_create_file(path: &Path) -> io::Result<File> {
let mut backoff_iterator = BackoffIterator::new();
let mut options = OpenOptions::new();
options
.create(true)
.write(true)
// Don't share delete access. This ensures that std::fs::remove_file
// (which uses DeleteFileW) will fail if any other process has the file
// open — so deletion in Drop only succeeds when we're the last handle
// holder.
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE);
loop {
match options.open(path) {
Ok(file) => return Ok(file),
// The file may be open for deletion in Drop.
Err(err)
if err.kind() == io::ErrorKind::PermissionDenied
|| err.raw_os_error() == Some(ERROR_SHARING_VIOLATION as _) =>
{
let Some(duration) = backoff_iterator.next() else {
return Err(err);
};
// Ensure that a file can be created in the target directory.
tempfile::tempfile_in(path.parent().expect("file path should have parent"))?;
thread::sleep(duration);
}
Err(err) => return Err(err),
}
}
}