diff --git a/Cargo.lock b/Cargo.lock index d2f69483..06ebeb81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -865,6 +865,16 @@ 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" @@ -2186,6 +2196,7 @@ dependencies = [ "async-std", "bitflags 2.6.0", "derivative", + "fslock", "glob", "lazy_static", "log", diff --git a/Cargo.toml b/Cargo.toml index 5cd5d962..1442e151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,7 @@ filelocksmith = "0.1" image = { version = "0.25", default-features = false, features = ["gif", "jpeg", "png"] } fs_extra = "1.3" memmap2 = "0.9" +fslock = "0.2" # default to small, optimized workspace release binaries [profile.release] diff --git a/src/bins/src/commands/install.rs b/src/bins/src/commands/install.rs index 2d6d2902..3fa45174 100644 --- a/src/bins/src/commands/install.rs +++ b/src/bins/src/commands/install.rs @@ -21,6 +21,7 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op info!("Reading package manifest..."); let app = pkg.read_manifest()?; + info!("Package manifest loaded successfully."); info!(" Package ID: {}", &app.id); info!(" Package Version: {}", &app.version); @@ -30,8 +31,6 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op info!(" Package Machine Architecture: {}", &app.machine_architecture); info!(" Package Runtime Dependencies: {}", &app.runtime_dependencies); - let _mutex = shared::retry_io(|| windows::create_global_mutex(&app.id))?; - if !windows::prerequisite::prompt_and_install_all_missing(&app, None)? { info!("Cancelling setup. Pre-requisites not installed."); return Ok(()); @@ -114,6 +113,11 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op info!("Preparing and cleaning installation directory..."); remove_dir_all::ensure_empty_dir(&root_path)?; + + info!("Acquiring lock..."); + let paths = create_config_from_root_dir(&root_path); + let locator = VelopackLocator::new(paths, app); + let _mutex = locator.try_get_exclusive_lock()?; let tx = if dialogs::get_silent() { info!("Will not show splash because silent mode is on."); @@ -122,10 +126,10 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op } else { info!("Reading splash image..."); let splash_bytes = pkg.get_splash_bytes(); - windows::splash::show_splash_dialog(app.title.to_owned(), splash_bytes) + windows::splash::show_splash_dialog(locator.get_manifest_title(), splash_bytes) }; - let install_result = install_impl(pkg, &root_path, &tx, start_args); + let install_result = install_impl(pkg, &locator, &tx, start_args); let _ = tx.send(windows::splash::MSG_CLOSE); if install_result.is_ok() { @@ -148,13 +152,9 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op Ok(()) } -fn install_impl(pkg: &mut BundleZip, root_path: &PathBuf, tx: &std::sync::mpsc::Sender, start_args: Option>) -> Result<()> { +fn install_impl(pkg: &mut BundleZip, locator: &VelopackLocator, tx: &std::sync::mpsc::Sender, start_args: Option>) -> Result<()> { info!("Starting installation!"); - let app_manifest = pkg.read_manifest()?; - let paths = create_config_from_root_dir(root_path); - let locator = VelopackLocator::new(paths, app_manifest); - // all application paths let updater_path = locator.get_update_path(); let packages_path = locator.get_packages_dir(); diff --git a/src/bins/src/commands/start_windows_impl.rs b/src/bins/src/commands/start_windows_impl.rs index d914f7b1..d5fe9651 100644 --- a/src/bins/src/commands/start_windows_impl.rs +++ b/src/bins/src/commands/start_windows_impl.rs @@ -146,34 +146,31 @@ fn try_legacy_migration(root_dir: &PathBuf, manifest: &Manifest) -> Result Result<()> { info!(" Exe Args: {:?}", exe_args); let locator = auto_locate_app_manifest(LocationContext::IAmUpdateExe)?; - #[cfg(target_os = "windows")] - let _mutex = shared::retry_io(|| windows::create_global_mutex(&locator.get_manifest_id()))?; + let _mutex = locator.get_exclusive_lock_blocking()?; 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 955fe5ee..fdb36f9d 100644 --- a/src/bins/src/windows/util.rs +++ b/src/bins/src/windows/util.rs @@ -78,21 +78,6 @@ impl Drop for MutexDropGuard { } } -pub fn create_global_mutex(app_id: &str) -> Result { - let mutex_name = format!("velopack-{}", app_id); - info!("Attempting to open global system mutex: '{}'", &mutex_name); - let encodedu16 = super::strings::string_to_u16(mutex_name); - let encoded = PCWSTR(encodedu16.as_ptr()); - let mutex = unsafe { CreateMutexW(None, true, encoded) }?; - match unsafe { GetLastError() } { - Foundation::ERROR_SUCCESS => Ok(MutexDropGuard { mutex }), - Foundation::ERROR_ALREADY_EXISTS => { - Err(anyhow!("Another installer or updater for this application is running, quit that process and try again.")) - } - err => Err(anyhow!("Unable to create global mutex. Error code {:?}", err)), - } -} - pub fn expand_environment_strings>(input: P) -> Result { use windows::Win32::System::Environment::ExpandEnvironmentStringsW; let encoded_u16 = super::strings::string_to_u16(input); diff --git a/src/lib-rust/Cargo.toml b/src/lib-rust/Cargo.toml index 747eb9ba..c05588b6 100644 --- a/src/lib-rust/Cargo.toml +++ b/src/lib-rust/Cargo.toml @@ -46,6 +46,7 @@ rand.workspace = true native-tls.workspace = true sha1.workspace = true sha2.workspace = true +fslock.workspace = true # typescript ts-rs = { workspace = true, optional = true } diff --git a/src/lib-rust/src/locator.rs b/src/lib-rust/src/locator.rs index 16829869..cc63f30d 100644 --- a/src/lib-rust/src/locator.rs +++ b/src/lib-rust/src/locator.rs @@ -274,6 +274,33 @@ impl VelopackLocator { pub fn get_is_portable(&self) -> bool { self.paths.IsPortable } + + /// 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 { + 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) + } fn path_as_string(path: &PathBuf) -> String { path.to_string_lossy().to_string() diff --git a/src/lib-rust/src/manager.rs b/src/lib-rust/src/manager.rs index 5fafff1a..817bb1db 100644 --- a/src/lib-rust/src/manager.rs +++ b/src/lib-rust/src/manager.rs @@ -295,6 +295,7 @@ impl UpdateManager { /// - If there is no delta update available, or there is an error preparing delta /// packages, this method will fall back to downloading the full version of the update. pub fn download_updates(&self, update: &UpdateInfo, progress: Option>) -> Result<(), Error> { + let _mutex = &self.locator.try_get_exclusive_lock()?; let name = &update.TargetFullRelease.FileName; let packages_dir = &self.locator.get_packages_dir();