feat(app): prevent multiple instances
This commit is contained in:
parent
7a8b0e6ce7
commit
3f517c5f48
|
|
@ -1156,6 +1156,16 @@ dependencies = [
|
||||||
"percent-encoding",
|
"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]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
|
|
@ -1265,6 +1275,7 @@ dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
|
"fs2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ validator = { version = "0.20", features = ["derive"] }
|
||||||
|
|
||||||
# Error handling
|
# Error handling
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
fs2 = "0.4"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
tray-icon = "0.15"
|
tray-icon = "0.15"
|
||||||
|
|
|
||||||
13
src/main.rs
13
src/main.rs
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
|
||||||
|
|
||||||
mod control;
|
mod control;
|
||||||
mod config;
|
mod config;
|
||||||
mod connection;
|
mod connection;
|
||||||
|
|
@ -47,6 +49,17 @@ pub struct AppState {
|
||||||
async fn main() {
|
async fn main() {
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
util::log::init_logger();
|
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 config = AppConfig::from_env().expect("Failed to load configuration");
|
||||||
let pool = init_database(&config.database_url)
|
let pool = init_database(&config.database_url)
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,5 @@ pub mod datetime;
|
||||||
pub mod log;
|
pub mod log;
|
||||||
pub mod pagination;
|
pub mod pagination;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
|
pub mod single_instance;
|
||||||
pub mod validator;
|
pub mod validator;
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
use fs2::FileExt;
|
||||||
|
use std::{
|
||||||
|
fs::{File, OpenOptions},
|
||||||
|
io,
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn try_acquire(name: &str) -> io::Result<SingleInstanceGuard> {
|
||||||
|
SingleInstanceGuard::acquire(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SingleInstanceGuard {
|
||||||
|
_file: File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SingleInstanceGuard {
|
||||||
|
fn acquire(name: &str) -> io::Result<Self> {
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue