fix(mux/rust): prevent multiple instances via fd-lock (v0.1.1)
This commit is contained in:
5 files changed
+77
-5
No files matched your search
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.1 — 2026-04-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent multiple mux instances from running simultaneously via file-based
|
||||
exclusive lock (`fd-lock`); stale sockets from crashed instances are cleaned
|
||||
up automatically
|
||||
- Clearer error message when TCP port is already in use
|
||||
|
||||
## 0.1.0 — 2026-04-06
|
||||
|
||||
Initial release.
|
||||
|
||||
Generated
+41
-1
@@ -171,11 +171,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "donglora-mux"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"donglora-client",
|
||||
"fd-lock",
|
||||
"tokio",
|
||||
"tokio-serial",
|
||||
"tokio-util",
|
||||
@@ -193,6 +194,17 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fd-lock"
|
||||
version = "4.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
@@ -335,6 +347,12 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
@@ -522,6 +540,19 @@ version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -826,6 +857,15 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "donglora-mux"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
authors = ["Stephen Waits <steve@waits.net>"]
|
||||
license = "MIT"
|
||||
@@ -18,6 +18,7 @@ tokio-serial = "5"
|
||||
tokio-util = "0.7"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
fd-lock = "4"
|
||||
|
||||
[lints.clippy]
|
||||
unwrap_used = "deny"
|
||||
|
||||
@@ -11,6 +11,7 @@ dongle simultaneously.
|
||||
- RxPacket frames broadcast to all connected clients
|
||||
- StartRx/StopRx reference-counted across clients
|
||||
- SetConfig locked once set (single client can change freely)
|
||||
- Single-instance enforcement via file lock (stale sockets auto-cleaned)
|
||||
- Automatic dongle reconnect on hot-plug
|
||||
- No panics — enforced by clippy deny lints
|
||||
|
||||
|
||||
+24
-3
@@ -91,7 +91,24 @@ impl MuxDaemon {
|
||||
if let Some(parent) = sock_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await.ok();
|
||||
}
|
||||
// Acquire an exclusive lock to prevent multiple mux instances.
|
||||
// The lock is held for the process lifetime and auto-released on exit (even SIGKILL).
|
||||
let lock_path = format!("{}.lock", self.socket_path);
|
||||
let lock_file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(false)
|
||||
.open(&lock_path)
|
||||
.map_err(|e| anyhow::anyhow!("failed to open lock file {lock_path}: {e}"))?;
|
||||
let mut lock = fd_lock::RwLock::new(lock_file);
|
||||
let _lock_guard = match lock.try_write() {
|
||||
Ok(guard) => guard,
|
||||
Err(_) => anyhow::bail!("another donglora-mux is already running (lock held on {lock_path})"),
|
||||
};
|
||||
|
||||
if sock_path.exists() {
|
||||
// Lock acquired but socket file exists — stale from a crashed instance.
|
||||
info!("removing stale socket {}", self.socket_path);
|
||||
tokio::fs::remove_file(sock_path).await.ok();
|
||||
}
|
||||
|
||||
@@ -103,9 +120,13 @@ impl MuxDaemon {
|
||||
// Start TCP server (optional)
|
||||
let tcp_listener = if let Some((ref host, port)) = self.tcp_addr {
|
||||
let addr = format!("{host}:{port}");
|
||||
let listener = TcpListener::bind(&addr)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("failed to bind TCP {addr}: {e}"))?;
|
||||
let listener = TcpListener::bind(&addr).await.map_err(|e| {
|
||||
if e.kind() == std::io::ErrorKind::AddrInUse {
|
||||
anyhow::anyhow!("another donglora-mux is already listening on TCP {addr}")
|
||||
} else {
|
||||
anyhow::anyhow!("failed to bind TCP {addr}: {e}")
|
||||
}
|
||||
})?;
|
||||
info!("TCP listening on {addr}");
|
||||
Some(listener)
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user