Write my own lockfile implementation

This commit is contained in:
Caelan Sayler
2024-11-03 10:38:00 +00:00
committed by Caelan
parent d8943faf37
commit 2946b4ea2c
11 changed files with 235 additions and 76 deletions

View File

@@ -189,7 +189,7 @@ fn apply(matches: &ArgMatches) -> Result<()> {
info!(" Exe Args: {:?}", exe_args);
let locator = auto_locate_app_manifest(LocationContext::IAmUpdateExe)?;
let _mutex = locator.get_exclusive_lock_blocking()?;
let _mutex = locator.try_get_exclusive_lock()?;
let _ = commands::apply(&locator, restart, wait, package, exe_args, true)?;
Ok(())
}

View File

@@ -14,10 +14,7 @@ use windows::core::PCWSTR;
use windows::Win32::Storage::FileSystem::GetLongPathNameW;
use windows::Win32::System::SystemInformation::{VerSetConditionMask, VerifyVersionInfoW, OSVERSIONINFOEXW, VER_FLAGS};
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
use windows::Win32::{
Foundation::{self, GetLastError},
System::Threading::CreateMutexW,
};
use windows::Win32::Foundation;
use crate::shared::{self, runtime_arch::RuntimeArch};
use crate::windows::strings::{string_to_u16, u16_to_string};

View File

@@ -462,7 +462,7 @@ namespace Velopack
{
var dir = Directory.CreateDirectory(Locator.PackagesDir!);
var lockPath = Path.Combine(dir.FullName, ".velopack_lock");
var fsLock = new FileLock(lockPath);
var fsLock = new LockFile(lockPath);
await fsLock.LockAsync().ConfigureAwait(false);
return fsLock;
}

View File

@@ -1,38 +0,0 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Velopack.Util
{
internal class FileLock : IDisposable
{
private readonly string _filePath;
private FileStream? _fileStream;
private bool _locked;
public FileLock(string path)
{
_filePath = path;
}
public async Task LockAsync()
{
if (_locked) {
return;
}
await IoUtil.RetryAsync(
() => {
_fileStream = new FileStream(_filePath, FileMode.Create, FileAccess.Read, FileShare.None, bufferSize: 1, FileOptions.DeleteOnClose);
_locked = true;
return Task.CompletedTask;
});
}
public void Dispose()
{
Interlocked.Exchange(ref this._fileStream, null)?.Dispose();
}
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
namespace Velopack.Util
{
internal class LockFile : IDisposable
{
private readonly string _filePath;
private FileStream? _fileStream;
private bool _locked;
public LockFile(string path)
{
_filePath = path;
}
public async Task LockAsync()
{
if (_locked) {
return;
}
try {
await IoUtil.RetryAsync(
() => {
Dispose();
_fileStream = new FileStream(
_filePath,
FileMode.Create,
FileAccess.ReadWrite,
FileShare.None,
bufferSize: 1,
FileOptions.DeleteOnClose);
SafeFileHandle safeFileHandle = _fileStream.SafeFileHandle!;
if (VelopackRuntimeInfo.IsLinux || VelopackRuntimeInfo.IsOSX) {
int fd = safeFileHandle.DangerousGetHandle().ToInt32();
UnixExclusiveLock(fd);
} else if (VelopackRuntimeInfo.IsWindows) {
WindowsExclusiveLock(safeFileHandle);
}
_locked = true;
return Task.CompletedTask;
}).ConfigureAwait(false);
} catch (Exception ex) {
Dispose();
throw new IOException("Failed to acquire exclusive lock file. Is another operation currently running?", ex);
}
}
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
[DllImport("libc", SetLastError = true)]
private static extern int flock(int fd, int operation);
private const int LOCK_SH = 1; // Shared lock
private const int LOCK_EX = 2; // Exclusive lock
private const int LOCK_NB = 4; // Non-blocking
private const int LOCK_UN = 8; // Unlock
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
private void UnixExclusiveLock(int fd)
{
int ret = flock(fd, LOCK_EX | LOCK_NB);
if (ret != 0) {
throw new IOException("flock returned error: " + ret);
}
}
[SupportedOSPlatform("windows")]
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool LockFileEx(SafeFileHandle hFile, uint dwFlags, uint dwReserved, uint nNumberOfBytesToLockLow, uint nNumberOfBytesToLockHigh,
[In] ref NativeOverlapped lpOverlapped);
private const uint LOCKFILE_EXCLUSIVE_LOCK = 0x00000002;
private const uint LOCKFILE_FAIL_IMMEDIATELY = 0x00000001;
[SupportedOSPlatform("windows")]
private void WindowsExclusiveLock(SafeFileHandle safeFileHandle)
{
NativeOverlapped overlapped = default;
bool ret = LockFileEx(safeFileHandle, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, ref overlapped);
if (!ret) {
throw new Win32Exception();
}
}
public void Dispose()
{
Interlocked.Exchange(ref this._fileStream, null)?.Dispose();
}
}
}

View File

@@ -46,7 +46,6 @@ rand.workspace = true
native-tls.workspace = true
sha1.workspace = true
sha2.workspace = true
fslock.workspace = true
# typescript
ts-rs = { workspace = true, optional = true }
@@ -56,3 +55,9 @@ zstd = { workspace = true, optional = true }
# async
async-std = { workspace = true, optional = true }
[target.'cfg(windows)'.dependencies]
windows = { workspace = true, features = ["Win32_Foundation", "Win32_Storage", "Win32_Storage_FileSystem", "Win32_System_IO"] }
[target.'cfg(unix)'.dependencies]
libc.workspace = true

View File

@@ -101,6 +101,9 @@ pub mod sources;
/// Functions to patch files and reconstruct Velopack delta packages.
pub mod delta;
/// Acquire and manage file-system based lock files.
pub mod lockfile;
pub use app::*;
pub use manager::*;

View File

@@ -3,6 +3,7 @@ use semver::Version;
use crate::{
bundle::{self, Manifest},
util, Error,
lockfile::LockFile
};
/// Returns the default channel name for the current OS.
@@ -277,29 +278,13 @@ impl VelopackLocator {
/// Attemps to open / lock a file in the app's package directory for exclusive write access.
/// Fails immediately if the lock cannot be acquired.
pub fn try_get_exclusive_lock(&self) -> Result<fslock::LockFile, Error> {
pub fn try_get_exclusive_lock(&self) -> Result<LockFile, Error> {
info!("Attempting to acquire exclusive lock on packages directory (non-blocking)...");
let packages_dir = self.get_packages_dir();
std::fs::create_dir_all(&packages_dir)?;
let lock_file_path = packages_dir.join(".velopack_lock");
let mut file = fslock::LockFile::open(&lock_file_path)?;
let result = file.try_lock_with_pid()?;
if !result {
return Err(Error::Generic("Could not acquire exclusive lock on packages directory".to_owned()));
}
Ok(file)
}
/// Attemps to open / lock a file in the app's package directory for exclusive write access.
/// Blocks until the lock can be acquired.
pub fn get_exclusive_lock_blocking(&self) -> Result<fslock::LockFile, Error> {
info!("Attempting to acquire exclusive lock on packages directory (blocking)...");
let packages_dir = self.get_packages_dir();
std::fs::create_dir_all(&packages_dir)?;
let lock_file_path = packages_dir.join(".velopack_lock");
let mut file = fslock::LockFile::open(&lock_file_path)?;
file.lock_with_pid()?;
Ok(file)
let lock_file = LockFile::try_acquire_lock(&lock_file_path)?;
Ok(lock_file)
}
fn path_as_string(path: &PathBuf) -> String {

View File

@@ -0,0 +1,113 @@
use std::fs::{File, OpenOptions};
use std::io::{Error, ErrorKind, Result};
use std::path::PathBuf;
/// A lock file that is used to prevent multiple instances of the application from running.
/// This lock file is automatically released and deleted when the `LockFile` is dropped.
pub struct LockFile {
file_path: PathBuf,
file: Option<File>,
}
impl LockFile {
/// Creates a new `FileLock` with the given file path.
pub fn try_acquire_lock<P: Into<PathBuf>>(path: P) -> Result<Self> {
let path: PathBuf = path.into();
crate::util::retry_io(|| {
let mut lock_file = LockFile {
file_path: path.clone(),
file: None,
};
let mut options = OpenOptions::new();
options.read(true).write(true).create(true).truncate(true);
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
options.custom_flags(0x04000000); // FILE_FLAG_DELETE_ON_CLOSE
options.share_mode(0);
}
let file = options.open(&path)?;
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "android"))]
{
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
self.unix_exclusive_lock(fd)?;
}
#[cfg(target_os = "windows")]
{
use std::os::windows::io::AsRawHandle;
let handle = file.as_raw_handle();
lock_file.windows_exclusive_lock(handle)?;
}
lock_file.file = Some(file);
Ok(lock_file)
})
}
/// Releases the lock and closes the file.
fn dispose(&mut self) {
{
let _ = self.file.take();
}
let _ = std::fs::remove_file(&self.file_path);
}
/// Acquires an exclusive, non-blocking lock on Unix-like systems.
#[cfg(unix)]
fn unix_exclusive_lock(&self, fd: std::os::unix::io::RawFd) -> Result<()> {
use libc::{flock, LOCK_EX, LOCK_NB};
let ret = unsafe { flock(fd, LOCK_EX | LOCK_NB) };
if ret != 0 {
let err = Error::last_os_error();
Err(Error::new(
ErrorKind::Other,
format!("Failed to lock file: {}", err),
))
} else {
Ok(())
}
}
/// Acquires an exclusive, non-blocking lock on Windows systems.
#[cfg(windows)]
fn windows_exclusive_lock(&self, handle: std::os::windows::io::RawHandle) -> Result<()> {
use windows::Win32::Foundation::HANDLE;
use windows::Win32::Storage::FileSystem::{LockFileEx, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY};
use windows::Win32::System::IO::OVERLAPPED;
let mut overlapped = OVERLAPPED::default();
let res = unsafe {
LockFileEx(
HANDLE(handle.into()),
LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
0,
1,
0,
&mut overlapped,
)
};
if res.is_err() {
let err = Error::last_os_error();
return Err(Error::new(
ErrorKind::Other,
format!("Failed to lock file: {}", err),
));
}
Ok(())
}
}
impl Drop for LockFile {
fn drop(&mut self) {
self.dispose();
}
}