Finally fix both rust/C# implementations so they work together on *nix

This commit is contained in:
Caelan Sayler
2024-11-07 19:55:15 +00:00
committed by Caelan
parent a173b33aaf
commit 7fa74e2a50
2 changed files with 174 additions and 166 deletions

View File

@@ -1,19 +1,23 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
namespace Velopack.Util
{
internal class LockFile : IDisposable
{
public bool IsLocked { get; private set; }
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly string _filePath;
private FileStream? _fileStream;
private bool _locked;
private int _fileDescriptor = -1;
public LockFile(string path)
{
@@ -22,131 +26,128 @@ namespace Velopack.Util
public async Task LockAsync()
{
if (_locked) {
if (IsLocked) {
return;
}
try {
await _semaphore.WaitAsync().ConfigureAwait(false);
await IoUtil.RetryAsync(
() => {
Dispose();
async () => {
await Task.Delay(1).ConfigureAwait(false);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
WindowsExclusiveLock();
} else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
UnixExclusiveLock();
}
}).ConfigureAwait(false);
IsLocked = true;
} catch (Exception ex) {
DisposeInternal();
throw new IOException("Failed to acquire exclusive lock file. Is another operation currently running?", ex);
} finally {
_semaphore.Release();
}
}
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
[DllImport("libc", SetLastError = true)]
private static extern int open(byte[] pathname, int flags);
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
[DllImport("libc", SetLastError = true)]
private static extern int creat(byte[] pathname, uint mode);
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
[DllImport("libc", SetLastError = true)]
private static extern int lockf(int fd, int cmd, long len);
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
[DllImport("libc", SetLastError = true)]
private static extern int close(int fd);
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
private void UnixExclusiveLock()
{
if (_fileDescriptor > 0) {
close(_fileDescriptor);
}
var fileBytes = Encoding.UTF8.GetBytes(_filePath).ToArray();
const int O_RDWR = 0x2;
const int O_CLOEXEC = 0x01000000;
const int EINTR = 4;
var filePermissionOctal = Convert.ToUInt16("666", 8);
int fd;
do { fd = open(fileBytes, O_RDWR | O_CLOEXEC); } while (fd == -1 && Marshal.GetLastWin32Error() == EINTR);
// if we cant open the file, try to create it...
if (fd == -1) {
do { fd = creat(fileBytes, filePermissionOctal); } while (fd == -1 && Marshal.GetLastWin32Error() == EINTR);
}
if (fd == -1) {
int errno = Marshal.GetLastWin32Error();
close(fd);
throw new IOException($"creat failed, errno: {errno}", new Win32Exception(errno));
}
int ret;
do { ret = lockf(fd, 2 /* F_TLOCK */, 0); } while (ret == -1 && Marshal.GetLastWin32Error() == EINTR);
if (ret != 0) {
int errno = Marshal.GetLastWin32Error();
close(fd);
throw new IOException($"lockf failed, errno: {errno}", new Win32Exception(errno));
}
_fileDescriptor = fd;
}
[SupportedOSPlatform("windows")]
private void WindowsExclusiveLock()
{
_fileStream?.Dispose();
_fileStream = new FileStream(
_filePath,
FileMode.Create,
FileMode.OpenOrCreate,
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);
FileOptions.None);
_fileStream.Lock(0, 0);
}
_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")]
private void UnixExclusiveLock(int fd)
private void DisposeInternal()
{
int ret;
if (VelopackRuntimeInfo.IsLinux) {
var lockOpt = new linux_flock {
l_type = F_WRLCK,
l_whence = SEEK_SET,
l_start = 0,
l_len = 0, // 0 means to lock the entire file
l_pid = 0,
};
ret = fcntl(fd, F_SETLK, ref lockOpt);
} else if (VelopackRuntimeInfo.IsOSX) {
var lockOpt = new osx_flock {
l_start = 0,
l_len = 0, // 0 means to lock the entire file
l_pid = 0,
l_type = F_WRLCK,
l_whence = SEEK_SET,
};
Console.WriteLine("hello");
ret = fcntl(fd, F_SETLK, ref lockOpt);
} else {
throw new PlatformNotSupportedException();
}
Interlocked.Exchange(ref this._fileStream, null)?.Dispose();
Console.WriteLine(ret);
if (ret == -1) {
int errno = Marshal.GetLastWin32Error();
throw new IOException($"fcntl F_SETLK failed, errno: {errno}", new Win32Exception(errno));
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
if (_fileDescriptor > 0) {
close(_fileDescriptor);
}
}
[SupportedOSPlatform("linux")]
[DllImport("libc", SetLastError = true)]
private static extern int fcntl(int fd, int cmd, ref linux_flock linux_flock);
[SupportedOSPlatform("macos")]
[DllImport("libc", SetLastError = true)]
private static extern int fcntl(int fd, int cmd, ref osx_flock linux_flock);
[SupportedOSPlatform("linux")]
[StructLayout(LayoutKind.Sequential)]
private struct linux_flock
{
public short l_type; /* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */
public short l_whence; /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */
public long l_start; /* Starting offset for lock */
public long l_len; /* Number of bytes to lock */
public int l_pid; /* PID of the process blocking our lock (F_GETLK only) */
}
[SupportedOSPlatform("macos")]
[StructLayout(LayoutKind.Sequential)]
private struct osx_flock
{
public long l_start; /* Starting offset for lock */
public long l_len; /* Number of bytes to lock */
public int l_pid; /* PID of the process blocking our lock (F_GETLK only) */
public short l_type; /* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */
public short l_whence; /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */
}
private const int F_SETLK = 6; /* Non-blocking lock */
private const short F_RDLCK = 0; /* Read lock */
private const short F_WRLCK = 1; /* Write lock */
private const short F_UNLCK = 2; /* Remove lock */
private const short SEEK_SET = 0;
[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();
}
IsLocked = false;
_fileDescriptor = -1;
}
public void Dispose()
{
Interlocked.Exchange(ref this._fileStream, null)?.Dispose();
try {
_semaphore.Wait();
DisposeInternal();
} finally {
_semaphore.Release();
}
}
}
}

View File

@@ -1,117 +1,124 @@
use std::fs::{File, OpenOptions};
use std::io::{Error, ErrorKind, Result};
use std::fs::File;
use std::io::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.
#[allow(dead_code)]
pub struct LockFile {
file_path: PathBuf,
file: Option<File>,
file_descriptor: Option<i32>,
}
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 = Self::windows_exclusive_lock(&path)?;
let lock = LockFile {
file_path: path.clone(),
file: Some(file),
file_descriptor: None,
};
Ok(lock)
}
let file = options.open(&path)?;
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "android"))]
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
lock_file.unix_exclusive_lock(fd)?;
let fd = unsafe { Self::unix_exclusive_lock(&path)? };
let lock = LockFile {
file_path: path.clone(),
file: None,
file_descriptor: Some(fd),
};
Ok(lock)
}
#[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(); // dispose file handle
#[cfg(unix)]
{
let _ = self.file.take();
if let Some(fd) = self.file_descriptor.take() {
unsafe { libc::close(fd); }
}
}
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::{fcntl, F_SETLK, F_WRLCK, SEEK_SET};
unsafe fn unix_exclusive_lock<P: Into<PathBuf>>(path: P) -> Result<i32> {
use std::os::unix::ffi::OsStrExt;
use libc::{open, creat, lockf, close, F_TLOCK, O_RDWR, O_CLOEXEC, EINTR};
use std::io::{Error, ErrorKind};
let lock = libc::flock {
l_type: F_WRLCK as libc::c_short,
l_whence: SEEK_SET as libc::c_short,
l_start: 0,
l_len: 0, // 0 means to lock the entire file
l_pid: 0,
};
let path = path.into();
let c_path = std::ffi::CString::new(path.as_os_str().as_bytes())?;
let ret = unsafe { fcntl(fd, F_SETLK, &lock) };
// try to open existing file
let mut fd;
loop {
fd = open(c_path.as_ptr(), O_RDWR | O_CLOEXEC);
if fd != -1 || Error::last_os_error().raw_os_error() != Some(EINTR) { break; }
}
// create it if that fails
if fd == -1 {
loop {
fd = creat(c_path.as_ptr(), 0o666);
if fd != -1 || Error::last_os_error().raw_os_error() != Some(EINTR) { break; }
}
}
if fd == -1 {
let err = std::io::Error::last_os_error();
let _ = close(fd);
return Err(std::io::Error::new(
ErrorKind::Other,
format!("Failed to open lock file: {}", err),
))
}
let mut ret;
loop {
ret = lockf(fd, F_TLOCK, 0);
if ret != -1 || Error::last_os_error().raw_os_error() != Some(EINTR) { break; }
}
if ret == -1 {
let err = Error::last_os_error();
Err(Error::new(
let err = std::io::Error::last_os_error();
let _ = close(fd);
Err(std::io::Error::new(
ErrorKind::Other,
format!("Failed to lock file: {}", err),
))
} else {
Ok(())
Ok(fd)
}
}
/// 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;
fn windows_exclusive_lock<P: Into<PathBuf>>(path: P) -> Result<File> {
use std::os::windows::fs::OpenOptionsExt;
use std::fs::OpenOptions;
let mut overlapped = OVERLAPPED::default();
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
// .custom_flags(0x04000000) // FILE_FLAG_DELETE_ON_CLOSE
.share_mode(0)
.open(path.into())?;
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(())
Ok(file)
}
}