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 <noreply@anthropic.com>
This commit is contained in:
d-b-c-e
2026-02-02 19:10:45 +01:00
committed by GitHub
co-authored by Claude Opus 4.5
parent e890ef940c
commit ecb18d7192
2 changed files with 94 additions and 0 deletions
+12
View File
@@ -10,6 +10,7 @@ mod config;
mod migration; mod migration;
mod platforms; mod platforms;
mod settings; mod settings;
mod single_instance;
mod steam; mod steam;
mod steamgriddb; mod steamgriddb;
mod sync; mod sync;
@@ -20,6 +21,17 @@ use color_eyre::eyre::Result;
fn main() -> Result<()> { fn main() -> Result<()> {
color_eyre::install()?; color_eyre::install()?;
ensure_config_folder(); 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(); migration::migrate_config();
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
+82
View File
@@ -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<Self, String> {
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::<usize>() {
// 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()
}