Add file-based locking to rust library

This commit is contained in:
Caelan Sayler
2024-11-02 17:21:06 +00:00
committed by Caelan
parent 36e87c0e7d
commit 4ee99ce4e0
9 changed files with 60 additions and 38 deletions

11
Cargo.lock generated
View File

@@ -865,6 +865,16 @@ 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"
@@ -2186,6 +2196,7 @@ dependencies = [
"async-std", "async-std",
"bitflags 2.6.0", "bitflags 2.6.0",
"derivative", "derivative",
"fslock",
"glob", "glob",
"lazy_static", "lazy_static",
"log", "log",

View File

@@ -78,6 +78,7 @@ 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"
# default to small, optimized workspace release binaries # default to small, optimized workspace release binaries
[profile.release] [profile.release]

View File

@@ -21,6 +21,7 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op
info!("Reading package manifest..."); info!("Reading package manifest...");
let app = pkg.read_manifest()?; let app = pkg.read_manifest()?;
info!("Package manifest loaded successfully."); info!("Package manifest loaded successfully.");
info!(" Package ID: {}", &app.id); info!(" Package ID: {}", &app.id);
info!(" Package Version: {}", &app.version); 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 Machine Architecture: {}", &app.machine_architecture);
info!(" Package Runtime Dependencies: {}", &app.runtime_dependencies); 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)? { if !windows::prerequisite::prompt_and_install_all_missing(&app, None)? {
info!("Cancelling setup. Pre-requisites not installed."); info!("Cancelling setup. Pre-requisites not installed.");
return Ok(()); 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..."); info!("Preparing and cleaning installation directory...");
remove_dir_all::ensure_empty_dir(&root_path)?; 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() { let tx = if dialogs::get_silent() {
info!("Will not show splash because silent mode is on."); 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 { } else {
info!("Reading splash image..."); info!("Reading splash image...");
let splash_bytes = pkg.get_splash_bytes(); 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); let _ = tx.send(windows::splash::MSG_CLOSE);
if install_result.is_ok() { if install_result.is_ok() {
@@ -148,13 +152,9 @@ pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Op
Ok(()) Ok(())
} }
fn install_impl(pkg: &mut BundleZip, root_path: &PathBuf, tx: &std::sync::mpsc::Sender<i16>, start_args: Option<Vec<&str>>) -> Result<()> { fn install_impl(pkg: &mut BundleZip, locator: &VelopackLocator, tx: &std::sync::mpsc::Sender<i16>, start_args: Option<Vec<&str>>) -> Result<()> {
info!("Starting installation!"); 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 // all application paths
let updater_path = locator.get_update_path(); let updater_path = locator.get_update_path();
let packages_path = locator.get_packages_dir(); let packages_path = locator.get_packages_dir();

View File

@@ -146,34 +146,31 @@ fn try_legacy_migration(root_dir: &PathBuf, manifest: &Manifest) -> Result<Velop
// if started by legacy Squirrel, the working dir of Update.exe may be inside the app-* folder, // if started by legacy Squirrel, the working dir of Update.exe may be inside the app-* folder,
// meaning we can not clean up properly. // meaning we can not clean up properly.
std::env::set_current_dir(&root_dir)?; std::env::set_current_dir(&root_dir)?;
let _mutex = shared::retry_io(|| crate::windows::create_global_mutex(&manifest.id))?;
let path_config = locator::create_config_from_root_dir(root_dir); let path_config = locator::create_config_from_root_dir(root_dir);
let package = locator::find_latest_full_package(&path_config.PackagesDir).ok_or_else(|| anyhow!("Unable to find latest full package."))?; let package = locator::find_latest_full_package(&path_config.PackagesDir).ok_or_else(|| anyhow!("Unable to find latest full package."))?;
warn!("This application is installed in a folder prefixed with 'app-'. Attempting to migrate..."); warn!("This application is installed in a folder prefixed with 'app-'. Attempting to migrate...");
let _ = shared::force_stop_package(&root_dir); let _ = shared::force_stop_package(&root_dir);
let current_dir = &path_config.CurrentBinaryDir; // reset current manifest shortcuts, so when the new manifest is being read
if !Path::new(&current_dir).exists() { // new shortcuts will be force-created
let mut modified_manifest = manifest.clone();
modified_manifest.shortcut_locations = String::new();
let locator = VelopackLocator::new(path_config, modified_manifest);
let _mutex = locator.try_get_exclusive_lock()?;
if !locator.get_current_bin_dir().exists() {
info!("Renaming latest app-* folder to current."); info!("Renaming latest app-* folder to current.");
if let Some((latest_app_dir, _latest_ver)) = shared::get_latest_app_version_folder(&root_dir)? { if let Some((latest_app_dir, _latest_ver)) = shared::get_latest_app_version_folder(&root_dir)? {
fs::rename(latest_app_dir, &current_dir)?; fs::rename(latest_app_dir, locator.get_current_bin_dir())?;
} }
} }
info!("Removing old shortcuts..."); info!("Removing old shortcuts...");
win::remove_all_shortcuts_for_root_dir(&root_dir); win::remove_all_shortcuts_for_root_dir(&root_dir);
// reset current manifest shortcuts, so when the new manifest is being read
// new shortcuts will be force-created
let mut modified_manifest = manifest.clone();
modified_manifest.shortcut_locations = String::new();
info!("Applying latest full package..."); info!("Applying latest full package...");
let buf = Path::new(&package.0).to_path_buf(); let buf = Path::new(&package.0).to_path_buf();
let locator = VelopackLocator::new(path_config, modified_manifest);
let new_locator = super::apply(&locator, false, OperationWait::NoWait, Some(&buf), None, false)?; let new_locator = super::apply(&locator, false, OperationWait::NoWait, Some(&buf), None, false)?;
info!("Removing old app-* folders..."); info!("Removing old app-* folders...");

View File

@@ -189,8 +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)?;
#[cfg(target_os = "windows")] let _mutex = locator.get_exclusive_lock_blocking()?;
let _mutex = shared::retry_io(|| windows::create_global_mutex(&locator.get_manifest_id()))?;
let _ = commands::apply(&locator, restart, wait, package, exe_args, true)?; let _ = commands::apply(&locator, restart, wait, package, exe_args, true)?;
Ok(()) Ok(())
} }

View File

@@ -78,21 +78,6 @@ impl Drop for MutexDropGuard {
} }
} }
pub fn create_global_mutex(app_id: &str) -> Result<MutexDropGuard> {
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<P: AsRef<str>>(input: P) -> Result<String> { pub fn expand_environment_strings<P: AsRef<str>>(input: P) -> Result<String> {
use windows::Win32::System::Environment::ExpandEnvironmentStringsW; use windows::Win32::System::Environment::ExpandEnvironmentStringsW;
let encoded_u16 = super::strings::string_to_u16(input); let encoded_u16 = super::strings::string_to_u16(input);

View File

@@ -46,6 +46,7 @@ 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 }

View File

@@ -274,6 +274,33 @@ impl VelopackLocator {
pub fn get_is_portable(&self) -> bool { pub fn get_is_portable(&self) -> bool {
self.paths.IsPortable 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<fslock::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)
}
fn path_as_string(path: &PathBuf) -> String { fn path_as_string(path: &PathBuf) -> String {
path.to_string_lossy().to_string() path.to_string_lossy().to_string()

View File

@@ -295,6 +295,7 @@ impl UpdateManager {
/// - If there is no delta update available, or there is an error preparing delta /// - 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. /// packages, this method will fall back to downloading the full version of the update.
pub fn download_updates(&self, update: &UpdateInfo, progress: Option<Sender<i16>>) -> Result<(), Error> { pub fn download_updates(&self, update: &UpdateInfo, progress: Option<Sender<i16>>) -> Result<(), Error> {
let _mutex = &self.locator.try_get_exclusive_lock()?;
let name = &update.TargetFullRelease.FileName; let name = &update.TargetFullRelease.FileName;
let packages_dir = &self.locator.get_packages_dir(); let packages_dir = &self.locator.get_packages_dir();