Skip to content

Commit ecb18d7

Browse files
d-b-c-eclaude
andauthored
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>
1 parent e890ef9 commit ecb18d7

File tree

2 files changed

+94
-0
lines changed

2 files changed

+94
-0
lines changed

src/main.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod config;
1010
mod migration;
1111
mod platforms;
1212
mod settings;
13+
mod single_instance;
1314
mod steam;
1415
mod steamgriddb;
1516
mod sync;
@@ -20,6 +21,17 @@ use color_eyre::eyre::Result;
2021
fn main() -> Result<()> {
2122
color_eyre::install()?;
2223
ensure_config_folder();
24+
25+
// Acquire single instance lock
26+
let _instance_lock = match single_instance::InstanceLock::acquire() {
27+
Ok(lock) => lock,
28+
Err(msg) => {
29+
eprintln!("Error: {}", msg);
30+
eprintln!("Please close the other instance of BoilR first.");
31+
return Ok(());
32+
}
33+
};
34+
2335
migration::migrate_config();
2436

2537
let args: Vec<String> = std::env::args().collect();

src/single_instance.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use std::fs::{File, OpenOptions};
2+
use std::io::{Read, Write};
3+
use std::path::PathBuf;
4+
use sysinfo::{Pid, System};
5+
6+
use crate::config::get_config_folder;
7+
8+
/// Returns the path to the lock file
9+
fn get_lock_file_path() -> PathBuf {
10+
get_config_folder().join("boilr.lock")
11+
}
12+
13+
/// Represents a lock on the application instance
14+
pub struct InstanceLock {
15+
_file: File,
16+
path: PathBuf,
17+
}
18+
19+
impl InstanceLock {
20+
/// Attempts to acquire an exclusive lock for this application instance.
21+
/// Returns Ok(InstanceLock) if successful, or Err with a message if another instance is running.
22+
pub fn acquire() -> Result<Self, String> {
23+
let lock_path = get_lock_file_path();
24+
25+
// Ensure the config folder exists
26+
if let Some(parent) = lock_path.parent() {
27+
let _ = std::fs::create_dir_all(parent);
28+
}
29+
30+
// Check if lock file exists and contains a valid PID
31+
if lock_path.exists() {
32+
if let Ok(mut file) = File::open(&lock_path) {
33+
let mut contents = String::new();
34+
if file.read_to_string(&mut contents).is_ok() {
35+
if let Ok(pid) = contents.trim().parse::<usize>() {
36+
// Check if process with that PID is still running
37+
if is_process_running(pid) {
38+
return Err(format!(
39+
"Another instance of BoilR is already running (PID: {})",
40+
pid
41+
));
42+
}
43+
}
44+
}
45+
}
46+
}
47+
48+
// Try to create/overwrite the lock file
49+
match OpenOptions::new()
50+
.write(true)
51+
.create(true)
52+
.truncate(true)
53+
.open(&lock_path)
54+
{
55+
Ok(mut file) => {
56+
let pid = std::process::id();
57+
if let Err(e) = write!(file, "{}", pid) {
58+
return Err(format!("Failed to write lock file: {}", e));
59+
}
60+
61+
Ok(InstanceLock {
62+
_file: file,
63+
path: lock_path,
64+
})
65+
}
66+
Err(e) => Err(format!("Failed to create lock file: {}", e)),
67+
}
68+
}
69+
}
70+
71+
impl Drop for InstanceLock {
72+
fn drop(&mut self) {
73+
let _ = std::fs::remove_file(&self.path);
74+
}
75+
}
76+
77+
/// Check if a process with the given PID is running using sysinfo
78+
fn is_process_running(pid: usize) -> bool {
79+
let mut system = System::new();
80+
system.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
81+
system.process(Pid::from(pid)).is_some()
82+
}

0 commit comments

Comments
 (0)