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:
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -865,16 +865,6 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fslock"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -2196,9 +2186,9 @@ dependencies = [
|
|||||||
"async-std",
|
"async-std",
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"derivative",
|
"derivative",
|
||||||
"fslock",
|
|
||||||
"glob",
|
"glob",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"normpath",
|
"normpath",
|
||||||
@@ -2213,6 +2203,7 @@ dependencies = [
|
|||||||
"ts-rs",
|
"ts-rs",
|
||||||
"ureq",
|
"ureq",
|
||||||
"url",
|
"url",
|
||||||
|
"windows",
|
||||||
"xml",
|
"xml",
|
||||||
"zip",
|
"zip",
|
||||||
"zstd",
|
"zstd",
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ filelocksmith = "0.1"
|
|||||||
image = { version = "0.25", default-features = false, features = ["gif", "jpeg", "png"] }
|
image = { version = "0.25", default-features = false, features = ["gif", "jpeg", "png"] }
|
||||||
fs_extra = "1.3"
|
fs_extra = "1.3"
|
||||||
memmap2 = "0.9"
|
memmap2 = "0.9"
|
||||||
fslock = "0.2"
|
webview2-com = "0.33"
|
||||||
|
windows = "0.58"
|
||||||
|
|
||||||
# default to small, optimized workspace release binaries
|
# default to small, optimized workspace release binaries
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ fn apply(matches: &ArgMatches) -> Result<()> {
|
|||||||
info!(" Exe Args: {:?}", exe_args);
|
info!(" Exe Args: {:?}", exe_args);
|
||||||
|
|
||||||
let locator = auto_locate_app_manifest(LocationContext::IAmUpdateExe)?;
|
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)?;
|
let _ = commands::apply(&locator, restart, wait, package, exe_args, true)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ use windows::core::PCWSTR;
|
|||||||
use windows::Win32::Storage::FileSystem::GetLongPathNameW;
|
use windows::Win32::Storage::FileSystem::GetLongPathNameW;
|
||||||
use windows::Win32::System::SystemInformation::{VerSetConditionMask, VerifyVersionInfoW, OSVERSIONINFOEXW, VER_FLAGS};
|
use windows::Win32::System::SystemInformation::{VerSetConditionMask, VerifyVersionInfoW, OSVERSIONINFOEXW, VER_FLAGS};
|
||||||
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
|
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
|
||||||
use windows::Win32::{
|
use windows::Win32::Foundation;
|
||||||
Foundation::{self, GetLastError},
|
|
||||||
System::Threading::CreateMutexW,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::shared::{self, runtime_arch::RuntimeArch};
|
use crate::shared::{self, runtime_arch::RuntimeArch};
|
||||||
use crate::windows::strings::{string_to_u16, u16_to_string};
|
use crate::windows::strings::{string_to_u16, u16_to_string};
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ namespace Velopack
|
|||||||
{
|
{
|
||||||
var dir = Directory.CreateDirectory(Locator.PackagesDir!);
|
var dir = Directory.CreateDirectory(Locator.PackagesDir!);
|
||||||
var lockPath = Path.Combine(dir.FullName, ".velopack_lock");
|
var lockPath = Path.Combine(dir.FullName, ".velopack_lock");
|
||||||
var fsLock = new FileLock(lockPath);
|
var fsLock = new LockFile(lockPath);
|
||||||
await fsLock.LockAsync().ConfigureAwait(false);
|
await fsLock.LockAsync().ConfigureAwait(false);
|
||||||
return fsLock;
|
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
|
native-tls.workspace = true
|
||||||
sha1.workspace = true
|
sha1.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
fslock.workspace = true
|
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
ts-rs = { workspace = true, optional = true }
|
ts-rs = { workspace = true, optional = true }
|
||||||
@@ -56,3 +55,9 @@ zstd = { workspace = true, optional = true }
|
|||||||
|
|
||||||
# async
|
# async
|
||||||
async-std = { workspace = true, optional = true }
|
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.
|
/// Functions to patch files and reconstruct Velopack delta packages.
|
||||||
pub mod delta;
|
pub mod delta;
|
||||||
|
|
||||||
|
/// Acquire and manage file-system based lock files.
|
||||||
|
pub mod lockfile;
|
||||||
|
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
pub use manager::*;
|
pub use manager::*;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use semver::Version;
|
|||||||
use crate::{
|
use crate::{
|
||||||
bundle::{self, Manifest},
|
bundle::{self, Manifest},
|
||||||
util, Error,
|
util, Error,
|
||||||
|
lockfile::LockFile
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Returns the default channel name for the current OS.
|
/// 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.
|
/// Attemps to open / lock a file in the app's package directory for exclusive write access.
|
||||||
/// Fails immediately if the lock cannot be acquired.
|
/// 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)...");
|
info!("Attempting to acquire exclusive lock on packages directory (non-blocking)...");
|
||||||
let packages_dir = self.get_packages_dir();
|
let packages_dir = self.get_packages_dir();
|
||||||
std::fs::create_dir_all(&packages_dir)?;
|
std::fs::create_dir_all(&packages_dir)?;
|
||||||
let lock_file_path = packages_dir.join(".velopack_lock");
|
let lock_file_path = packages_dir.join(".velopack_lock");
|
||||||
let mut file = fslock::LockFile::open(&lock_file_path)?;
|
let lock_file = LockFile::try_acquire_lock(&lock_file_path)?;
|
||||||
let result = file.try_lock_with_pid()?;
|
Ok(lock_file)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path_as_string(path: &PathBuf) -> String {
|
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