mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Write my own lockfile implementation
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
102
src/lib-csharp/Util/LockFile.cs
Normal file
102
src/lib-csharp/Util/LockFile.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
113
src/lib-rust/src/lockfile.rs
Normal file
113
src/lib-rust/src/lockfile.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user