diff --git a/Cargo.lock b/Cargo.lock index ca71d5d..0bea23e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1156,6 +1156,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "futures" version = "0.3.32" @@ -1265,6 +1275,7 @@ dependencies = [ "axum", "chrono", "dotenv", + "fs2", "serde", "serde_json", "serde_with", diff --git a/Cargo.toml b/Cargo.toml index b8320d9..14e5961 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ validator = { version = "0.20", features = ["derive"] } # Error handling anyhow = "1.0" +fs2 = "0.4" [target.'cfg(windows)'.dependencies] tray-icon = "0.15" diff --git a/src/main.rs b/src/main.rs index 54a4745..f8a2047 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] + mod control; mod config; mod connection; @@ -47,6 +49,17 @@ pub struct AppState { async fn main() { dotenv::dotenv().ok(); util::log::init_logger(); + let _single_instance = match util::single_instance::try_acquire("PLCControl.Gateway") { + Ok(guard) => guard, + Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { + tracing::warn!("Another PLC Control instance is already running"); + return; + } + Err(err) => { + tracing::error!("Failed to initialize single instance guard: {}", err); + return; + } + }; let config = AppConfig::from_env().expect("Failed to load configuration"); let pool = init_database(&config.database_url) diff --git a/src/util.rs b/src/util.rs index f2c20f2..edb3ea5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -2,4 +2,5 @@ pub mod datetime; pub mod log; pub mod pagination; pub mod response; -pub mod validator; \ No newline at end of file +pub mod single_instance; +pub mod validator; diff --git a/src/util/single_instance.rs b/src/util/single_instance.rs new file mode 100644 index 0000000..efc1d35 --- /dev/null +++ b/src/util/single_instance.rs @@ -0,0 +1,65 @@ +use fs2::FileExt; +use std::{ + fs::{File, OpenOptions}, + io, + path::PathBuf, +}; + +pub fn try_acquire(name: &str) -> io::Result { + SingleInstanceGuard::acquire(name) +} + +pub struct SingleInstanceGuard { + _file: File, +} + +impl SingleInstanceGuard { + fn acquire(name: &str) -> io::Result { + let lock_path = lock_file_path(name); + let file = OpenOptions::new() + .create(true) + .read(true) + .write(true) + .truncate(false) + .open(lock_path)?; + + if let Err(err) = file.try_lock_exclusive() { + if err.kind() == io::ErrorKind::WouldBlock { + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + "another PLC Control instance is already running", + )); + } + return Err(io::Error::new( + err.kind(), + format!("failed to lock single-instance file: {}", err), + )); + } + + Ok(Self { _file: file }) + } +} + +fn lock_file_path(name: &str) -> PathBuf { + let sanitized: String = name + .chars() + .map(|ch| match ch { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch, + _ => '_', + }) + .collect(); + + std::env::temp_dir().join(format!("{}.lock", sanitized)) +} + +#[cfg(test)] +mod tests { + use super::lock_file_path; + + #[test] + fn lock_path_is_sanitized() { + let path = lock_file_path("PLCControl/Gateway:test"); + let path_str = path.to_string_lossy(); + assert!(path_str.ends_with("PLCControl_Gateway_test.lock")); + } +}