diff --git a/Cargo.lock b/Cargo.lock index 06ebeb81..3e5653aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -865,16 +865,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "futures-channel" version = "0.3.31" @@ -2196,9 +2186,9 @@ dependencies = [ "async-std", "bitflags 2.6.0", "derivative", - "fslock", "glob", "lazy_static", + "libc", "log", "native-tls", "normpath", @@ -2213,6 +2203,7 @@ dependencies = [ "ts-rs", "ureq", "url", + "windows", "xml", "zip", "zstd", diff --git a/Cargo.toml b/Cargo.toml index 1442e151..455969e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,8 @@ filelocksmith = "0.1" image = { version = "0.25", default-features = false, features = ["gif", "jpeg", "png"] } fs_extra = "1.3" memmap2 = "0.9" -fslock = "0.2" +webview2-com = "0.33" +windows = "0.58" # default to small, optimized workspace release binaries [profile.release] diff --git a/src/bins/src/update.rs b/src/bins/src/update.rs index 086d24e3..f0a05c52 100644 --- a/src/bins/src/update.rs +++ b/src/bins/src/update.rs @@ -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(()) } diff --git a/src/bins/src/windows/util.rs b/src/bins/src/windows/util.rs index fdb36f9d..779acf3b 100644 --- a/src/bins/src/windows/util.rs +++ b/src/bins/src/windows/util.rs @@ -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}; diff --git a/src/lib-csharp/UpdateManager.cs b/src/lib-csharp/UpdateManager.cs index b2b480d0..2e55f0d9 100644 --- a/src/lib-csharp/UpdateManager.cs +++ b/src/lib-csharp/UpdateManager.cs @@ -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; } diff --git a/src/lib-csharp/Util/FileLock.cs b/src/lib-csharp/Util/FileLock.cs deleted file mode 100644 index c7e179ab..00000000 --- a/src/lib-csharp/Util/FileLock.cs +++ /dev/null @@ -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(); - } - } -} \ No newline at end of file diff --git a/src/lib-csharp/Util/LockFile.cs b/src/lib-csharp/Util/LockFile.cs new file mode 100644 index 00000000..07281a79 --- /dev/null +++ b/src/lib-csharp/Util/LockFile.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/src/lib-rust/Cargo.toml b/src/lib-rust/Cargo.toml index c05588b6..d87bd3cf 100644 --- a/src/lib-rust/Cargo.toml +++ b/src/lib-rust/Cargo.toml @@ -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 diff --git a/src/lib-rust/src/lib.rs b/src/lib-rust/src/lib.rs index 808f7f7b..329747fb 100644 --- a/src/lib-rust/src/lib.rs +++ b/src/lib-rust/src/lib.rs @@ -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::*; diff --git a/src/lib-rust/src/locator.rs b/src/lib-rust/src/locator.rs index cc63f30d..86f033e6 100644 --- a/src/lib-rust/src/locator.rs +++ b/src/lib-rust/src/locator.rs @@ -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 { + pub fn try_get_exclusive_lock(&self) -> Result { 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 { - 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 { diff --git a/src/lib-rust/src/lockfile.rs b/src/lib-rust/src/lockfile.rs new file mode 100644 index 00000000..b03ab3fc --- /dev/null +++ b/src/lib-rust/src/lockfile.rs @@ -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, +} + +impl LockFile { + /// Creates a new `FileLock` with the given file path. + pub fn try_acquire_lock>(path: P) -> Result { + 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(); + } +} \ No newline at end of file