mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Finally fix both rust/C# implementations so they work together on *nix
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user