mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 11:36:15 +00:00
feat(ble/android): adopt a radio without rebuilding the node
`AndroidIo` captured the injected bridge `Arc` at construction, and the BLE transport was only built if a bridge already existed when the node started. Both facts are wrong for the platform this backend serves: the radio is owned by an Android foreground service whose lifetime is independent of the node's — it can start *after* the node, and it mints a fresh bridge every time it starts. So the only way for a running node to adopt a radio was to stop and rebuild it, which drops every peer, every session, and every route with it. Turning Bluetooth on took the whole mesh down for as long as re-handshaking took, which is not what enabling a transport should cost. `AndroidIo` now resolves the process-wide bridge per operation, so a fresh bridge is picked up in place, and the transport is constructed whether or not one exists yet. Operations attempted with no radio present return a transport error instead of being unreachable, and recover on their own once a radio appears. `AndroidIo::new` is kept for tests that drive a specific bridge. Live streams still hold the radio they were opened on rather than migrating to a new one — a channel belongs to the socket that created it, and those die with the radio that owned them.
This commit is contained in:
+27
-23
@@ -1065,29 +1065,33 @@ impl Node {
|
||||
.iter()
|
||||
.map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
|
||||
.collect();
|
||||
match crate::transport::ble::android_io::android_ble_bridge() {
|
||||
Some(bridge) => {
|
||||
for (name, ble_config) in ble_instances {
|
||||
let transport_id = self.allocate_transport_id();
|
||||
let io = crate::transport::ble::android_io::AndroidIo::new(
|
||||
std::sync::Arc::clone(&bridge),
|
||||
);
|
||||
let mut ble = crate::transport::ble::BleTransport::new(
|
||||
transport_id,
|
||||
name,
|
||||
ble_config,
|
||||
io,
|
||||
packet_tx.clone(),
|
||||
);
|
||||
ble.set_local_pubkey(self.identity().pubkey().serialize());
|
||||
transports.push(TransportHandle::Ble(ble));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !ble_instances.is_empty() {
|
||||
tracing::warn!("BLE configured but no Android radio bridge injected");
|
||||
}
|
||||
}
|
||||
// Built whether or not a radio bridge exists yet, and without
|
||||
// capturing the one that does: `AndroidIo` resolves the process-wide
|
||||
// bridge per operation. The radio is owned by an Android foreground
|
||||
// service whose lifetime is independent of the node's — it can start
|
||||
// after the node, and it mints a fresh bridge every time it starts.
|
||||
// Binding either fact into the transport at construction meant the
|
||||
// only way to pick up a radio was to stop and rebuild the node,
|
||||
// which drops every peer and session. Operations attempted while no
|
||||
// radio is present fail as transport errors and recover on their own
|
||||
// once one appears.
|
||||
if !ble_instances.is_empty()
|
||||
&& crate::transport::ble::android_io::android_ble_bridge().is_none()
|
||||
{
|
||||
tracing::info!("BLE configured; waiting for the Android radio bridge");
|
||||
}
|
||||
for (name, ble_config) in ble_instances {
|
||||
let transport_id = self.allocate_transport_id();
|
||||
let io = crate::transport::ble::android_io::AndroidIo::from_global();
|
||||
let mut ble = crate::transport::ble::BleTransport::new(
|
||||
transport_id,
|
||||
name,
|
||||
ble_config,
|
||||
io,
|
||||
packet_tx.clone(),
|
||||
);
|
||||
ble.set_local_pubkey(self.identity().pubkey().serialize());
|
||||
transports.push(TransportHandle::Ble(ble));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -387,12 +387,44 @@ pub fn android_ble_bridge() -> Option<Arc<AndroidBleBridge>> {
|
||||
|
||||
/// macOS/Android-style external-radio backend over [`AndroidBleBridge`].
|
||||
pub struct AndroidIo {
|
||||
bridge: Arc<AndroidBleBridge>,
|
||||
/// Bridge to use when one was supplied explicitly (tests). In the app this
|
||||
/// is `None` and every operation resolves [`android_ble_bridge`] instead.
|
||||
///
|
||||
/// Resolving per call is what lets the radio be replaced without rebuilding
|
||||
/// the node. The Android service mints a fresh bridge each time it starts
|
||||
/// (a new `BleRadio`, a new JNI handle); if this type captured that `Arc` at
|
||||
/// construction, a running node would keep driving the dead one, and the
|
||||
/// only way to adopt the new radio would be to stop and restart the node —
|
||||
/// dropping every peer and every session with it.
|
||||
bridge: Option<Arc<AndroidBleBridge>>,
|
||||
}
|
||||
|
||||
impl AndroidIo {
|
||||
/// Resolve the bridge from the process-wide slot on each operation.
|
||||
pub fn from_global() -> Self {
|
||||
Self { bridge: None }
|
||||
}
|
||||
|
||||
/// Pin a specific bridge, for tests that drive one directly.
|
||||
pub fn new(bridge: Arc<AndroidBleBridge>) -> Self {
|
||||
Self { bridge }
|
||||
Self {
|
||||
bridge: Some(bridge),
|
||||
}
|
||||
}
|
||||
|
||||
/// The bridge for this operation. `None` once the radio has gone away —
|
||||
/// callers surface that as a transport error rather than panicking, since
|
||||
/// it is reachable simply by toggling Bluetooth off mid-operation.
|
||||
fn bridge(&self) -> Option<Arc<AndroidBleBridge>> {
|
||||
match &self.bridge {
|
||||
Some(b) => Some(Arc::clone(b)),
|
||||
None => android_ble_bridge(),
|
||||
}
|
||||
}
|
||||
|
||||
fn require_bridge(&self) -> Result<Arc<AndroidBleBridge>, TransportError> {
|
||||
self.bridge()
|
||||
.ok_or_else(|| TransportError::Io(std::io::Error::other("BLE radio not available")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,42 +565,40 @@ impl BleIo for AndroidIo {
|
||||
type Scanner = AndroidScanner;
|
||||
|
||||
async fn listen(&self, _psm: u16) -> Result<AndroidAcceptor, TransportError> {
|
||||
let bridge = self.require_bridge()?;
|
||||
// Android assigns the listener PSM; the `psm` arg from FIPS is ignored.
|
||||
let os_psm = self.bridge.radio.listen();
|
||||
let os_psm = bridge.radio.listen();
|
||||
if os_psm != 0 {
|
||||
self.bridge.local_psm.store(os_psm, Ordering::Relaxed);
|
||||
bridge.local_psm.store(os_psm, Ordering::Relaxed);
|
||||
}
|
||||
let rx = self
|
||||
.bridge
|
||||
let rx = bridge
|
||||
.accept_rx
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.take();
|
||||
Ok(AndroidAcceptor {
|
||||
rx,
|
||||
radio: Arc::clone(&self.bridge.radio),
|
||||
radio: Arc::clone(&bridge.radio),
|
||||
})
|
||||
}
|
||||
|
||||
async fn connect(&self, addr: &BleAddr, psm: u16) -> Result<AndroidStream, TransportError> {
|
||||
let bridge = self.require_bridge()?;
|
||||
// Substitute the learned per-peer PSM for this address, if known.
|
||||
let dial_psm = self.bridge.psm_map.resolve(addr, psm);
|
||||
let connect_id = self.bridge.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
let dial_psm = bridge.psm_map.resolve(addr, psm);
|
||||
let connect_id = bridge.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.bridge
|
||||
bridge
|
||||
.connects
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.insert(connect_id, tx);
|
||||
self.bridge.radio.connect(connect_id, addr, dial_psm);
|
||||
bridge.radio.connect(connect_id, addr, dial_psm);
|
||||
// FIPS already wraps connect() in a timeout, so we just await the result.
|
||||
match rx.await {
|
||||
Ok(ep) => Ok(AndroidStream::from_endpoints(
|
||||
ep,
|
||||
Arc::clone(&self.bridge.radio),
|
||||
)),
|
||||
Ok(ep) => Ok(AndroidStream::from_endpoints(ep, Arc::clone(&bridge.radio))),
|
||||
Err(_) => {
|
||||
self.bridge
|
||||
bridge
|
||||
.connects
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
@@ -581,24 +611,26 @@ impl BleIo for AndroidIo {
|
||||
}
|
||||
|
||||
async fn start_advertising(&self) -> Result<(), TransportError> {
|
||||
self.bridge
|
||||
let bridge = self.require_bridge()?;
|
||||
bridge
|
||||
.radio
|
||||
.start_advertising(self.bridge.local_psm.load(Ordering::Relaxed));
|
||||
.start_advertising(bridge.local_psm.load(Ordering::Relaxed));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop_advertising(&self) -> Result<(), TransportError> {
|
||||
self.bridge.radio.stop_advertising();
|
||||
let bridge = self.require_bridge()?;
|
||||
bridge.radio.stop_advertising();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_scanning(&self) -> Result<AndroidScanner, TransportError> {
|
||||
let bridge = self.require_bridge()?;
|
||||
// Re-learn PSMs / adverts each scan cycle (addresses rotate with MAC).
|
||||
self.bridge.psm_map.clear();
|
||||
self.bridge.clear_adverts();
|
||||
self.bridge.radio.start_scanning();
|
||||
let rx = self
|
||||
.bridge
|
||||
bridge.psm_map.clear();
|
||||
bridge.clear_adverts();
|
||||
bridge.radio.start_scanning();
|
||||
let rx = bridge
|
||||
.scan_rx
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
|
||||
Reference in New Issue
Block a user