Skip to content

Commit 8620cdb

Browse files
committed
feat(#177): add is_connecting() for concurrency protection
1 parent 2e99b5c commit 8620cdb

File tree

3 files changed

+69
-1
lines changed

3 files changed

+69
-1
lines changed

nmrs/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
All notable changes to the `nmrs` crate will be documented in this file.
44

55
## [Unreleased]
6+
### Added
7+
- Concurrency protection ([#268](https://github.com/cachebag/nmrs/pull/268))
68
### Changed
79
- Convert BDADDR to BlueZ device path via `bluez_device_path` helper ([#266](https://github.com/cachebag/nmrs/pull/266))
810

nmrs/src/api/network_manager.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use crate::core::connection_settings::{
1111
get_saved_connection_path, has_saved_connection, list_saved_connections,
1212
};
1313
use crate::core::device::{
14-
list_bluetooth_devices, list_devices, set_wifi_enabled, wait_for_wifi_ready, wifi_enabled,
14+
is_connecting, list_bluetooth_devices, list_devices, set_wifi_enabled, wait_for_wifi_ready,
15+
wifi_enabled,
1516
};
1617
use crate::core::scan::{current_network, list_networks, scan_networks};
1718
use crate::core::vpn::{connect_vpn, disconnect_vpn, get_vpn_info, list_vpn_connections};
@@ -114,6 +115,14 @@ use crate::Result;
114115
///
115116
/// `NetworkManager` is `Clone` and can be safely shared across async tasks.
116117
/// Each clone shares the same underlying D-Bus connection.
118+
///
119+
/// # Concurrency
120+
///
121+
/// Concurrent connection operations (e.g. calling [`connect`](Self::connect)
122+
/// from multiple tasks simultaneously) are **not supported** and may cause
123+
/// race conditions. Use [`is_connecting`](Self::is_connecting) to check
124+
/// whether a connection operation is already in progress before starting
125+
/// a new one.
117126
#[derive(Debug, Clone)]
118127
pub struct NetworkManager {
119128
conn: Connection,
@@ -425,6 +434,32 @@ impl NetworkManager {
425434
scan_networks(&self.conn).await
426435
}
427436

437+
/// Returns whether any network device is currently in a transitional state.
438+
///
439+
/// A device is considered "connecting" when its state is one of:
440+
/// Prepare, Config, NeedAuth, IpConfig, IpCheck, Secondaries, or Deactivating.
441+
///
442+
/// Use this to guard against concurrent connection attempts, which are
443+
/// not supported and may cause undefined behavior.
444+
///
445+
/// # Example
446+
///
447+
/// ```no_run
448+
/// use nmrs::NetworkManager;
449+
///
450+
/// # async fn example() -> nmrs::Result<()> {
451+
/// let nm = NetworkManager::new().await?;
452+
///
453+
/// if nm.is_connecting().await? {
454+
/// eprintln!("A connection operation is already in progress");
455+
/// }
456+
/// # Ok(())
457+
/// # }
458+
/// ```
459+
pub async fn is_connecting(&self) -> Result<bool> {
460+
is_connecting(&self.conn).await
461+
}
462+
428463
/// Check if a network is connected
429464
pub async fn is_connected(&self, ssid: &str) -> Result<bool> {
430465
is_connected(&self.conn, ssid).await

nmrs/src/core/device.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,37 @@ pub(crate) async fn list_devices(conn: &Connection) -> Result<Vec<Device>> {
134134
Ok(devices)
135135
}
136136

137+
/// Returns `true` if any network device is in a transitional state
138+
/// (preparing, configuring, authenticating, obtaining IP, etc.).
139+
///
140+
/// Useful for guarding against concurrent connection attempts.
141+
pub(crate) async fn is_connecting(conn: &Connection) -> Result<bool> {
142+
let nm = NMProxy::new(conn).await?;
143+
let devices = nm.get_devices().await?;
144+
145+
for dp in devices {
146+
let dev = NMDeviceProxy::builder(conn)
147+
.path(dp.clone())?
148+
.build()
149+
.await?;
150+
151+
let raw_state = dev
152+
.state()
153+
.await
154+
.map_err(|e| ConnectionError::DbusOperation {
155+
context: format!("failed to get state for device {}", dp.as_str()),
156+
source: e,
157+
})?;
158+
159+
let state: DeviceState = raw_state.into();
160+
if state.is_transitional() {
161+
return Ok(true);
162+
}
163+
}
164+
165+
Ok(false)
166+
}
167+
137168
pub(crate) async fn list_bluetooth_devices(conn: &Connection) -> Result<Vec<BluetoothDevice>> {
138169
let proxy = NMProxy::new(conn).await?;
139170
let paths = proxy.get_devices().await?;

0 commit comments

Comments
 (0)