feat(app): prevent multiple instances

This commit is contained in:
caoqianming 2026-03-31 10:18:57 +08:00
parent 7a8b0e6ce7
commit 3f517c5f48
5 changed files with 92 additions and 1 deletions

11
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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)

View File

@ -2,4 +2,5 @@ pub mod datetime;
pub mod log;
pub mod pagination;
pub mod response;
pub mod validator;
pub mod single_instance;
pub mod validator;

View File

@ -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"));
}
}