From ecb18d7192a057f79ce6359eaec2442699126baa Mon Sep 17 00:00:00 2001 From: d-b-c-e Date: Mon, 2 Feb 2026 12:10:45 -0600 Subject: [PATCH] Add single-instance enforcement (#460) Prevent multiple BoilR instances from running simultaneously using a lock file with PID checking. This avoids potential data corruption when two instances try to modify Steam shortcuts concurrently. The lock file is stored in the config folder (boilr.lock) and contains the PID of the running instance. On startup, if a lock file exists, we check whether the process is still alive using sysinfo before deciding to block or take over the lock. Co-authored-by: Claude Opus 4.5 --- src/main.rs | 12 +++++++ src/single_instance.rs | 82 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/single_instance.rs diff --git a/src/main.rs b/src/main.rs index 8f9bed8..d581555 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod config; mod migration; mod platforms; mod settings; +mod single_instance; mod steam; mod steamgriddb; mod sync; @@ -20,6 +21,17 @@ use color_eyre::eyre::Result; fn main() -> Result<()> { color_eyre::install()?; ensure_config_folder(); + + // Acquire single instance lock + let _instance_lock = match single_instance::InstanceLock::acquire() { + Ok(lock) => lock, + Err(msg) => { + eprintln!("Error: {}", msg); + eprintln!("Please close the other instance of BoilR first."); + return Ok(()); + } + }; + migration::migrate_config(); let args: Vec = std::env::args().collect(); diff --git a/src/single_instance.rs b/src/single_instance.rs new file mode 100644 index 0000000..fcd558c --- /dev/null +++ b/src/single_instance.rs @@ -0,0 +1,82 @@ +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::path::PathBuf; +use sysinfo::{Pid, System}; + +use crate::config::get_config_folder; + +/// Returns the path to the lock file +fn get_lock_file_path() -> PathBuf { + get_config_folder().join("boilr.lock") +} + +/// Represents a lock on the application instance +pub struct InstanceLock { + _file: File, + path: PathBuf, +} + +impl InstanceLock { + /// Attempts to acquire an exclusive lock for this application instance. + /// Returns Ok(InstanceLock) if successful, or Err with a message if another instance is running. + pub fn acquire() -> Result { + let lock_path = get_lock_file_path(); + + // Ensure the config folder exists + if let Some(parent) = lock_path.parent() { + let _ = std::fs::create_dir_all(parent); + } + + // Check if lock file exists and contains a valid PID + if lock_path.exists() { + if let Ok(mut file) = File::open(&lock_path) { + let mut contents = String::new(); + if file.read_to_string(&mut contents).is_ok() { + if let Ok(pid) = contents.trim().parse::() { + // Check if process with that PID is still running + if is_process_running(pid) { + return Err(format!( + "Another instance of BoilR is already running (PID: {})", + pid + )); + } + } + } + } + } + + // Try to create/overwrite the lock file + match OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&lock_path) + { + Ok(mut file) => { + let pid = std::process::id(); + if let Err(e) = write!(file, "{}", pid) { + return Err(format!("Failed to write lock file: {}", e)); + } + + Ok(InstanceLock { + _file: file, + path: lock_path, + }) + } + Err(e) => Err(format!("Failed to create lock file: {}", e)), + } + } +} + +impl Drop for InstanceLock { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.path); + } +} + +/// Check if a process with the given PID is running using sysinfo +fn is_process_running(pid: usize) -> bool { + let mut system = System::new(); + system.refresh_processes(sysinfo::ProcessesToUpdate::All, true); + system.process(Pid::from(pid)).is_some() +}