diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d2099d..07452c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Cargo.lock b/Cargo.lock index 39504ea..ec5d4d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index ebd6742..2591586 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "donglora-mux" -version = "0.1.0" +version = "0.1.1" edition = "2024" authors = ["Stephen Waits "] 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" diff --git a/README.md b/README.md index 5f2b6d5..013aade 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/daemon.rs b/src/daemon.rs index 8b20912..92001e6 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -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 {