diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fb18e9..23164e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.2.1 — 2026-04-07 + +### Fixed + +- **Exclusive serial port lock.** The mux now acquires an `flock`-based + exclusive lock on the serial port before opening it. This prevents other + processes (bridges, other mux instances) from directly opening the same + device while the mux owns it. The lock is held for the duration of the + dongle session and auto-released on disconnect or crash. + ## 0.2.0 — 2026-04-07 ### Changed diff --git a/Cargo.lock b/Cargo.lock index 68933b1..4ad22db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -161,9 +161,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "donglora-client" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1936935d31ab56a8cd70ceecfd6d447f2628a73a123e1cfe37ee4987eab0fa70" +checksum = "343d750fe1e5dad94d4b31e6dbbb05214b39e66f3c94a2ff2a29b0031ffd2826" dependencies = [ "anyhow", "serialport", @@ -173,7 +173,7 @@ dependencies = [ [[package]] name = "donglora-mux" -version = "0.2.0" +version = "0.2.1" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 7dc5260..73773b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "donglora-mux" -version = "0.2.0" +version = "0.2.1" edition = "2024" rust-version = "1.85" authors = ["Stephen Waits "] diff --git a/src/daemon.rs b/src/daemon.rs index 7f61473..54f26cf 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -156,12 +156,12 @@ impl MuxDaemon { } info!("opening dongle on {}", self.serial_port); - let serial = loop { + let (serial, _serial_lock_guard) = loop { if self.shutdown.is_cancelled() { return; } match open_and_ping(&self.serial_port).await { - Ok(s) => break s, + Ok(pair) => break pair, Err(e) => { debug!("could not open dongle: {e}"); tokio::select! { @@ -224,7 +224,39 @@ impl MuxDaemon { // ── Dongle I/O ───────────────────────────────────────────────────── -async fn open_and_ping(port: &str) -> anyhow::Result { +/// Lock file path for exclusive serial port access. +fn serial_lock_path(port: &str) -> String { + format!("/tmp/donglora-serial-{}.lock", port.replace('/', "_")) +} + +/// Open the serial port with an exclusive lock and verify the dongle responds to ping. +/// +/// Returns the serial stream and the lock. The caller must keep `_serial_lock` alive +/// for the duration of the dongle session. Dropping it releases the exclusive flock, +/// allowing other processes to claim the port. +async fn open_and_ping( + port: &str, +) -> anyhow::Result<(tokio_serial::SerialStream, fd_lock::RwLock)> { + // Acquire exclusive flock before opening the serial port. This prevents bridges + // or other processes from directly opening the same device while the mux owns it. + let lock_path = serial_lock_path(port); + let lock_file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(false) + .open(&lock_path) + .map_err(|e| anyhow::anyhow!("failed to open serial lock file {lock_path}: {e}"))?; + let mut lock = fd_lock::RwLock::new(lock_file); + // Acquire exclusive flock (non-blocking). Fails if another process holds it. + let guard = lock + .try_write() + .map_err(|_| anyhow::anyhow!("serial port {port} is locked by another process"))?; + // Forget the guard so its Drop doesn't call flock(UNLOCK). The underlying flock + // stays held as long as the file descriptor (owned by `lock`) remains open. + // When `lock` is dropped at the end of the dongle session, the fd closes and + // the OS releases the flock automatically. + std::mem::forget(guard); + let builder = tokio_serial::new(port, 115_200).timeout(Duration::from_secs(2)); let mut serial = tokio_serial::SerialStream::open(&builder).map_err(|e| anyhow::anyhow!("failed to open {port}: {e}"))?; @@ -260,7 +292,7 @@ async fn open_and_ping(port: &str) -> anyhow::Result for frame in reader.feed(&buf[..n]) { if frame.first() == Some(&0) { info!("dongle responded to Ping"); - return Ok(serial); + return Ok((serial, lock)); } } }