1use std::sync::{Arc, LazyLock, Mutex, PoisonError};
71use std::time::Duration;
72
73use btleplug::api::Peripheral as _;
74use btleplug::platform::Peripheral;
75use dbus::channel::MatchingReceiver;
76use dbus::message::MatchRule;
77use dbus::nonblock::stdintf::org_freedesktop_dbus::Properties;
78use dbus::nonblock::{Proxy, SyncConnection};
79use dbus_crossroads::{Crossroads, IfaceBuilder, MethodErr};
80use dbus_tokio::connection::IOResource;
81use tokio::sync::oneshot;
82use tokio_util::task::AbortOnDropHandle;
83use tracing::{debug, info, warn};
84
85use crate::pairing::{BluezError, PAIRING_RETRY_AFTER, PairOutcome, PairingBus, PairingCooldown};
86
87const AGENT_PATH: &str = "/dev/rye/aranet/agent";
89const AGENT_CAPABILITY: &str = "NoInputNoOutput";
91const BLUEZ_SERVICE: &str = "org.bluez";
92const BLUEZ_ROOT: &str = "/org/bluez";
93const AGENT_MANAGER_IFACE: &str = "org.bluez.AgentManager1";
94const DEVICE_IFACE: &str = "org.bluez.Device1";
95const CALL_TIMEOUT: Duration = Duration::from_secs(1);
102const PAIR_REPLY_TIMEOUT: Duration = Duration::from_secs(24 * 60 * 60);
106const RETRY_MINUTES: u64 = PAIRING_RETRY_AFTER.as_secs() / 60;
108
109static COOLDOWN: LazyLock<Mutex<PairingCooldown>> =
112 LazyLock::new(|| Mutex::new(PairingCooldown::default()));
113
114#[deprecated(
121 since = "0.3.0",
122 note = "aranet-core now pairs devices itself while connecting; no process-wide agent is registered"
123)]
124pub fn ensure_agent() {}
125
126pub(crate) async fn pair_if_needed(
132 peripheral: &Peripheral,
133 budget: Duration,
134) -> Option<PairOutcome> {
135 let address = peripheral.address().to_string();
136 let Some(device) = device_path(&peripheral.id().to_string()) else {
137 let outcome = PairOutcome::Unavailable(BluezError {
138 name: "org.freedesktop.DBus.Error.InvalidArgs".into(),
139 message: format!("no BlueZ object path for {}", peripheral.id()),
140 });
141 log_outcome(&address, &outcome);
142 return Some(outcome);
143 };
144 let session = run_session(device.clone(), budget);
145 let outcome = crate::pairing::pair_with_cooldown(&COOLDOWN, &device, session).await;
146 match &outcome {
147 Some(outcome) => log_outcome(&address, outcome),
148 None => debug!(
149 "Not pairing with {address}: it refused to pair less than {RETRY_MINUTES} minutes ago"
150 ),
151 }
152 outcome
153}
154
155async fn run_session(device: dbus::Path<'static>, budget: Duration) -> PairOutcome {
160 let Some(runtime) = crate::link::cleanup_runtime() else {
164 return PairOutcome::Unavailable(failed("no tokio runtime to pair on"));
165 };
166 let (report, outcome) = oneshot::channel();
167 let session = AbortOnDropHandle::new(runtime.spawn(pair_session(device, budget, report)));
168 let outcome = outcome.await;
169 drop(session.detach());
171 outcome.unwrap_or_else(|_| {
172 PairOutcome::Unavailable(failed("the pairing session ended without an outcome"))
173 })
174}
175
176async fn pair_session(
180 device: dbus::Path<'static>,
181 budget: Duration,
182 report: oneshot::Sender<PairOutcome>,
183) {
184 let (resource, conn) = match connect_system_bus().await {
185 Ok(connection) => connection,
186 Err(e) => {
187 let _ = report.send(PairOutcome::Unavailable(e));
188 return;
189 }
190 };
191 let mut cr = agent_crossroads(device.clone());
192 conn.start_receive(
193 MatchRule::new_method_call(),
194 Box::new(move |msg, conn| {
195 if cr.handle_message(msg, conn).is_err() {
196 warn!("BlueZ agent: failed to handle a D-Bus message");
197 }
198 true
199 }),
200 );
201 let bus = DbusPairingBus {
202 conn,
203 device,
204 report: Mutex::new(Some(report)),
205 };
206 let mut resource = std::pin::pin!(resource);
207 tokio::select! {
208 biased;
212 err = &mut resource => bus.report(&PairOutcome::Unavailable(BluezError {
213 name: "org.freedesktop.DBus.Error.Disconnected".into(),
214 message: err.to_string(),
215 })),
216 _ = crate::pairing::run_pairing(&bus, budget) => {}
217 }
218 }
222
223async fn connect_system_bus()
226-> Result<(IOResource<SyncConnection>, Arc<SyncConnection>), BluezError> {
227 let connect = tokio::task::spawn_blocking(dbus_tokio::connection::new_system_sync);
228 match tokio::time::timeout(CALL_TIMEOUT, connect).await {
229 Ok(Ok(connection)) => connection.map_err(BluezError::from),
230 Ok(Err(e)) => Err(failed(e.to_string())),
231 Err(_) => Err(BluezError {
232 name: "org.freedesktop.DBus.Error.Timeout".into(),
233 message: "the system bus did not answer".into(),
234 }),
235 }
236}
237
238fn failed(message: impl Into<String>) -> BluezError {
240 BluezError {
241 name: "org.freedesktop.DBus.Error.Failed".into(),
242 message: message.into(),
243 }
244}
245
246fn device_path(id_display: &str) -> Option<dbus::Path<'static>> {
250 if id_display.is_empty() {
251 return None;
252 }
253 dbus::Path::new(format!("{BLUEZ_ROOT}/{id_display}")).ok()
254}
255
256struct AgentData {
258 device: dbus::Path<'static>,
259}
260
261fn check_session_device(data: &AgentData, device: &dbus::Path) -> Result<(), MethodErr> {
263 if *device == data.device {
264 Ok(())
265 } else {
266 warn!("BlueZ agent: rejecting request for {device} (not the device aranet is pairing)");
267 Err((
268 "org.bluez.Error.Rejected",
269 "Device is not being paired by aranet",
270 )
271 .into())
272 }
273}
274
275fn authorize_service(device: &dbus::Path, uuid: &str) -> Result<(), MethodErr> {
284 warn!(
285 "BlueZ agent: rejecting AuthorizeService {uuid} for {device} (aranet never accepts host profile connections)"
286 );
287 Err((
288 "org.bluez.Error.Rejected",
289 "aranet does not authorize host profile connections",
290 )
291 .into())
292}
293
294fn request_passkey(device: &dbus::Path) -> Result<(u32,), MethodErr> {
302 warn!("BlueZ agent: rejecting RequestPasskey for {device} (aranet can't enter a passkey)");
303 Err(("org.bluez.Error.Rejected", "aranet can't enter a passkey").into())
304}
305
306fn agent_crossroads(device: dbus::Path<'static>) -> Crossroads {
309 let mut cr = Crossroads::new();
310 let token = cr.register("org.bluez.Agent1", |b: &mut IfaceBuilder<AgentData>| {
311 b.method("Release", (), (), |_, _, ()| {
312 debug!("BlueZ agent: Release");
313 Ok(())
314 });
315
316 b.method(
317 "RequestPasskey",
318 ("device",),
319 ("passkey",),
320 |_, _, (device,): (dbus::Path,)| request_passkey(&device),
321 );
322
323 b.method(
324 "RequestConfirmation",
325 ("device", "passkey"),
326 (),
327 |_, data, (device, passkey): (dbus::Path, u32)| {
328 debug!("BlueZ agent: RequestConfirmation for {device}, passkey {passkey}");
329 check_session_device(data, &device)
330 },
331 );
332
333 b.method(
334 "RequestAuthorization",
335 ("device",),
336 (),
337 |_, data, (device,): (dbus::Path,)| {
338 debug!("BlueZ agent: RequestAuthorization for {device}");
339 check_session_device(data, &device)
340 },
341 );
342
343 b.method(
344 "AuthorizeService",
345 ("device", "uuid"),
346 (),
347 |_, _, (device, uuid): (dbus::Path, String)| authorize_service(&device, &uuid),
348 );
349
350 b.method("Cancel", (), (), |_, _, ()| {
351 debug!("BlueZ agent: Cancel");
352 Ok(())
353 });
354 });
355 cr.insert(AGENT_PATH, &[token], AgentData { device });
356 cr
357}
358
359struct DbusPairingBus {
361 conn: Arc<SyncConnection>,
362 device: dbus::Path<'static>,
364 report: Mutex<Option<oneshot::Sender<PairOutcome>>>,
366}
367
368impl DbusPairingBus {
369 fn proxy(
370 &self,
371 path: dbus::Path<'static>,
372 timeout: Duration,
373 ) -> Proxy<'static, Arc<SyncConnection>> {
374 Proxy::new(BLUEZ_SERVICE, path, timeout, Arc::clone(&self.conn))
375 }
376}
377
378impl PairingBus for DbusPairingBus {
379 async fn is_paired(&self) -> Result<bool, BluezError> {
380 self.proxy(self.device.clone(), CALL_TIMEOUT)
382 .get::<bool>(DEVICE_IFACE, "Paired")
383 .await
384 .map_err(BluezError::from)
385 }
386
387 async fn register_agent(&self) -> Result<(), BluezError> {
388 self.proxy(dbus::Path::from(BLUEZ_ROOT), CALL_TIMEOUT)
389 .method_call(
390 AGENT_MANAGER_IFACE,
391 "RegisterAgent",
392 (dbus::Path::from(AGENT_PATH), AGENT_CAPABILITY),
393 )
394 .await
395 .map_err(BluezError::from)
396 }
397
398 async fn pair(&self) -> Result<(), BluezError> {
399 self.proxy(self.device.clone(), PAIR_REPLY_TIMEOUT)
400 .method_call(DEVICE_IFACE, "Pair", ())
401 .await
402 .map_err(BluezError::from)
403 }
404
405 async fn cancel_pairing(&self) -> Result<(), BluezError> {
406 self.proxy(self.device.clone(), CALL_TIMEOUT)
407 .method_call(DEVICE_IFACE, "CancelPairing", ())
408 .await
409 .map_err(BluezError::from)
410 }
411
412 async fn unregister_agent(&self) -> Result<(), BluezError> {
413 self.proxy(dbus::Path::from(BLUEZ_ROOT), CALL_TIMEOUT)
414 .method_call(
415 AGENT_MANAGER_IFACE,
416 "UnregisterAgent",
417 (dbus::Path::from(AGENT_PATH),),
418 )
419 .await
420 .map_err(BluezError::from)
421 }
422
423 fn report(&self, outcome: &PairOutcome) {
424 let report = self
425 .report
426 .lock()
427 .unwrap_or_else(PoisonError::into_inner)
428 .take();
429 if let Some(report) = report {
430 let _ = report.send(outcome.clone());
431 }
432 }
433}
434
435impl From<dbus::Error> for BluezError {
436 fn from(e: dbus::Error) -> Self {
437 BluezError {
438 name: e
439 .name()
440 .unwrap_or("org.freedesktop.DBus.Error.Failed")
441 .to_owned(),
442 message: e.message().unwrap_or("").to_owned(),
443 }
444 }
445}
446
447fn log_outcome(address: &str, outcome: &PairOutcome) {
449 let failure = match outcome {
450 PairOutcome::AlreadyPaired => {
451 debug!("{address} is already paired");
452 return;
453 }
454 PairOutcome::Paired => {
455 info!("Paired with {address}");
456 return;
457 }
458 PairOutcome::Unavailable(e) => {
459 warn!("Could not pair with {address}: {e}");
460 return;
461 }
462 PairOutcome::Failed(e) => format!("failed ({e})"),
463 PairOutcome::TimedOut => "timed out".to_owned(),
464 };
465 let retry = if outcome.refused_by_sensor() {
466 format!("aranet will not try to pair it again for {RETRY_MINUTES} minutes")
467 } else {
468 "aranet will try again at the next connection".to_owned()
469 };
470 warn!(
471 "Pairing with {address} {failure}; {retry}. Until it is paired, BlueZ asks for pairing \
472 at each connection, which can make reads time out on a host without a Bluetooth agent. \
473 aranet can't enter a PIN: if the sensor asks for one, pair it by hand once. Stop aranet \
474 and the aranet service first (a connect during the pairing cancels it), run \
475 `bluetoothctl remove {address}` if it was paired with this computer before, then run \
476 `bluetoothctl` and, at its prompt, `scan on` until the sensor is listed, `scan off` \
477 and `pair {address}`, entering the PIN when asked"
478 );
479}
480
481#[cfg(test)]
482mod tests {
483 use std::cell::RefCell;
484 use std::ffi::CString;
485 use std::sync::Weak;
486
487 use dbus::Message;
488 use dbus::arg::Variant;
489 use dbus::channel::Sender;
490 use dbus::nonblock::stdintf::org_freedesktop_dbus::RequestNameReply;
491 use dbus::strings::ErrorName;
492 use tokio::sync::Notify;
493
494 use super::*;
495 use crate::pairing::ALREADY_EXISTS;
496 use crate::test_support::within;
497
498 const A: &str = "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_01";
500 const B: &str = "/org/bluez/hci0/dev_11_22_33_44_55_66";
501 const HID_SERVICE: &str = "00001124-0000-1000-8000-00805f9b34fb";
503 const PASSKEY: u32 = 123_456;
504
505 fn path(p: &str) -> dbus::Path<'static> {
506 dbus::Path::new(p.to_owned()).unwrap()
507 }
508
509 fn agent_call(method: &str) -> Message {
510 Message::new_method_call("org.bluez", AGENT_PATH, "org.bluez.Agent1", method).unwrap()
511 }
512
513 fn dispatch(session_device: &str, mut call: Message) -> Message {
516 let mut cr = agent_crossroads(path(session_device));
517 call.set_serial(1);
518 let sent = RefCell::new(Vec::new());
519 cr.handle_message(call, &sent).unwrap();
520 let mut replies = sent.into_inner();
521 assert_eq!(replies.len(), 1, "the agent should send exactly one reply");
522 replies.remove(0)
523 }
524
525 fn assert_approved(mut reply: Message) {
526 reply
527 .as_result()
528 .expect("the agent should approve this request");
529 }
530
531 fn assert_rejected(mut reply: Message) {
532 let err = reply
533 .as_result()
534 .expect_err("the agent should reject this request");
535 assert_eq!(err.name(), Some("org.bluez.Error.Rejected"));
536 }
537
538 #[test]
542 #[allow(deprecated)]
543 fn deprecated_ensure_agent_is_a_no_op() {
544 ensure_agent();
547 }
548
549 #[test]
550 fn agent_approves_only_the_device_being_paired() {
551 assert_approved(dispatch(
552 A,
553 agent_call("RequestAuthorization").append1(path(A)),
554 ));
555 assert_approved(dispatch(
556 A,
557 agent_call("RequestConfirmation").append2(path(A), PASSKEY),
558 ));
559
560 assert_rejected(dispatch(
561 A,
562 agent_call("RequestAuthorization").append1(path(B)),
563 ));
564 assert_rejected(dispatch(
565 A,
566 agent_call("RequestConfirmation").append2(path(B), PASSKEY),
567 ));
568 }
569
570 #[test]
571 fn agent_sessions_do_not_share_approvals() {
572 assert_approved(dispatch(
576 A,
577 agent_call("RequestAuthorization").append1(path(A)),
578 ));
579 assert_rejected(dispatch(
580 B,
581 agent_call("RequestAuthorization").append1(path(A)),
582 ));
583 }
584
585 #[test]
586 fn agent_rejects_authorize_service_even_for_the_device_being_paired() {
587 assert_rejected(dispatch(
588 A,
589 agent_call("AuthorizeService").append2(path(A), HID_SERVICE),
590 ));
591 }
592
593 #[test]
594 fn agent_rejects_request_passkey_even_for_the_device_being_paired() {
595 for device in [A, B] {
598 assert_rejected(dispatch(
599 A,
600 agent_call("RequestPasskey").append1(path(device)),
601 ));
602 }
603 }
604
605 #[test]
606 fn agent_acknowledges_release_and_cancel() {
607 assert_approved(dispatch(A, agent_call("Release")));
608 assert_approved(dispatch(A, agent_call("Cancel")));
609 }
610
611 #[test]
612 fn device_path_from_peripheral_id() {
613 assert_eq!(
614 device_path("hci0/dev_AA_BB_CC_DD_EE_FF"),
615 Some(path("/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF"))
616 );
617 assert_eq!(device_path(""), None);
618 assert_eq!(device_path("hci0/dev AA"), None);
619 }
620
621 #[test]
622 fn bluez_error_keeps_dbus_name() {
623 let error = BluezError::from(dbus::Error::new_custom(ALREADY_EXISTS, "Already Paired"));
624 assert_eq!(error.name, ALREADY_EXISTS);
625 assert_eq!(error.message, "Already Paired");
626 }
627
628 const GET: &str = "org.freedesktop.DBus.Properties.Get";
632 const REGISTER: &str = "org.bluez.AgentManager1.RegisterAgent";
633 const PAIR: &str = "org.bluez.Device1.Pair";
634 const CANCEL: &str = "org.bluez.Device1.CancelPairing";
635 const UNREGISTER: &str = "org.bluez.AgentManager1.UnregisterAgent";
636
637 #[derive(Clone, Copy)]
639 enum PairAnswer {
640 AskAbout(&'static str),
644 Hang,
646 BondThenHold,
650 }
651
652 struct Scenario {
654 paired: bool,
656 pair: PairAnswer,
657 received: Vec<(String, String, String)>,
659 pair_held: Arc<Notify>,
661 held_pair: Option<Message>,
663 }
664
665 struct FakeBluez {
668 conn: Arc<SyncConnection>,
669 scenario: Arc<Mutex<Scenario>>,
670 }
671
672 impl FakeBluez {
673 async fn start() -> Self {
674 let (resource, conn) =
675 dbus_tokio::connection::new_system_sync().expect("connect to the test bus");
676 tokio::spawn(async move {
677 let err = resource.await;
678 eprintln!("the fake BlueZ lost its bus connection: {err}");
679 });
680 let reply = conn
681 .request_name("org.bluez", false, false, true)
682 .await
683 .expect("RequestName");
684 assert_eq!(
685 reply,
686 RequestNameReply::PrimaryOwner,
687 "another connection owns org.bluez on the test bus"
688 );
689 let scenario = Arc::new(Mutex::new(Scenario {
690 paired: false,
691 pair: PairAnswer::Hang,
692 received: Vec::new(),
693 pair_held: Arc::new(Notify::new()),
694 held_pair: None,
695 }));
696 let state = Arc::clone(&scenario);
697 let me = Arc::downgrade(&conn);
698 conn.start_receive(
699 MatchRule::new_method_call(),
700 Box::new(move |msg, conn| {
701 fake_bluez_answer(msg, conn, &state, &me);
702 true
703 }),
704 );
705 Self { conn, scenario }
706 }
707
708 fn script(&self, paired: bool, pair: PairAnswer) -> Arc<Notify> {
711 let mut scenario = self.scenario.lock().unwrap();
712 scenario.paired = paired;
713 scenario.pair = pair;
714 scenario.received.clear();
715 scenario.pair_held = Arc::new(Notify::new());
716 scenario.held_pair = None;
717 Arc::clone(&scenario.pair_held)
718 }
719
720 fn calls(&self) -> Vec<String> {
723 let scenario = self.scenario.lock().unwrap();
724 let received = &scenario.received;
725 assert!(
726 received
727 .iter()
728 .all(|(sender, _, _)| *sender == received[0].0),
729 "the calls came from more than one connection: {received:?}"
730 );
731 received
732 .iter()
733 .map(|(_, interface, member)| format!("{interface}.{member}"))
734 .collect()
735 }
736
737 fn answer_held_pair(&self) {
739 let pair = self.scenario.lock().unwrap().held_pair.take();
740 let pair = pair.expect("no Pair call is being held");
741 let _ = self.conn.send(pair.method_return());
742 }
743
744 async fn session_is_open(&self) -> bool {
747 let name = self.scenario.lock().unwrap().received[0].0.clone();
748 let dbus = Proxy::new(
749 "org.freedesktop.DBus",
750 "/org/freedesktop/DBus",
751 Duration::from_secs(5),
752 Arc::clone(&self.conn),
753 );
754 let (owned,): (bool,) = dbus
755 .method_call("org.freedesktop.DBus", "NameHasOwner", (name.as_str(),))
756 .await
757 .expect("NameHasOwner");
758 owned
759 }
760
761 async fn assert_session_closed(&self) {
765 let closed = tokio::time::timeout(Duration::from_secs(2), async {
766 while self.session_is_open().await {
767 tokio::time::sleep(Duration::from_millis(10)).await;
768 }
769 })
770 .await;
771 assert!(
772 closed.is_ok(),
773 "the session's connection was still open 2 s after the session ended"
774 );
775 }
776 }
777
778 fn fake_bluez_answer(
782 msg: Message,
783 conn: &SyncConnection,
784 scenario: &Mutex<Scenario>,
785 me: &Weak<SyncConnection>,
786 ) {
787 let sender = msg.sender().map(|s| s.to_string()).unwrap_or_default();
788 let interface = msg.interface().map(|i| i.to_string()).unwrap_or_default();
789 let member = msg.member().map(|m| m.to_string()).unwrap_or_default();
790 let on_sensor = msg.path().is_some_and(|p| &*p == A);
791 let on_manager = msg.path().is_some_and(|p| &*p == "/org/bluez");
792 let (paired, pair, pair_held) = {
793 let mut scenario = scenario.lock().unwrap();
794 scenario
795 .received
796 .push((sender.clone(), interface.clone(), member.clone()));
797 (
798 scenario.paired,
799 scenario.pair,
800 Arc::clone(&scenario.pair_held),
801 )
802 };
803 let reply = match (interface.as_str(), member.as_str()) {
804 ("org.freedesktop.DBus.Properties", "Get")
805 if on_sensor
806 && msg
807 .read2::<&str, &str>()
808 .is_ok_and(|args| args == ("org.bluez.Device1", "Paired")) =>
809 {
810 msg.method_return().append1(Variant(paired))
811 }
812 ("org.bluez.AgentManager1", "RegisterAgent")
813 if on_manager
814 && msg
815 .read2::<dbus::Path, &str>()
816 .is_ok_and(|(agent, capability)| {
817 &*agent == "/dev/rye/aranet/agent" && capability == "NoInputNoOutput"
818 }) =>
819 {
820 msg.method_return()
821 }
822 ("org.bluez.AgentManager1", "UnregisterAgent")
823 if on_manager
824 && msg
825 .read1::<dbus::Path>()
826 .is_ok_and(|agent| &*agent == "/dev/rye/aranet/agent") =>
827 {
828 msg.method_return()
829 }
830 ("org.bluez.Device1", "CancelPairing") if on_sensor => msg.method_return(),
831 ("org.bluez.Device1", "Pair") if on_sensor => {
832 match pair {
833 PairAnswer::Hang => pair_held.notify_one(),
834 PairAnswer::BondThenHold => {
835 let mut scenario = scenario.lock().unwrap();
836 scenario.paired = true;
837 scenario.held_pair = Some(msg);
838 pair_held.notify_one();
839 }
840 PairAnswer::AskAbout(device) => {
841 if let Some(conn) = me.upgrade() {
842 tokio::spawn(answer_pair(conn, msg, sender, device));
843 }
844 }
845 }
846 return;
847 }
848 _ => msg.error(
849 &ErrorName::from("org.bluez.Error.NotSupported"),
850 &CString::new("not expected by this test").unwrap(),
851 ),
852 };
853 let _ = conn.send(reply);
854 }
855
856 async fn answer_pair(
859 conn: Arc<SyncConnection>,
860 pair: Message,
861 owner: String,
862 device: &'static str,
863 ) {
864 let agent = Proxy::new(
865 owner,
866 "/dev/rye/aranet/agent",
867 Duration::from_secs(5),
868 Arc::clone(&conn),
869 );
870 let confirmed: Result<(), dbus::Error> = agent
871 .method_call(
872 "org.bluez.Agent1",
873 "RequestConfirmation",
874 (path(device), PASSKEY),
875 )
876 .await;
877 let reply = match confirmed {
878 Ok(()) => pair.method_return(),
879 Err(_) => pair.error(
880 &ErrorName::from("org.bluez.Error.AuthenticationRejected"),
881 &CString::new("Authentication Rejected").unwrap(),
882 ),
883 };
884 let _ = conn.send(reply);
885 }
886
887 #[tokio::test]
911 #[ignore = "needs a throwaway dbus-daemon; see this test's doc for the Docker command"]
912 async fn pairing_session_over_a_private_bus() {
913 assert!(
914 std::env::var_os("DBUS_SYSTEM_BUS_ADDRESS").is_some(),
915 "set DBUS_SYSTEM_BUS_ADDRESS to a throwaway dbus-daemon: this test takes the name org.bluez on that bus"
916 );
917 within(Duration::from_secs(120), async {
918 let bluez = FakeBluez::start().await;
919 let budget = Duration::from_secs(10);
920
921 bluez.script(false, PairAnswer::AskAbout(A));
924 assert_eq!(run_session(path(A), budget).await, PairOutcome::Paired);
925 bluez.assert_session_closed().await;
926 assert_eq!(bluez.calls(), [GET, REGISTER, PAIR, UNREGISTER]);
927
928 bluez.script(false, PairAnswer::AskAbout(B));
930 let outcome = run_session(path(A), budget).await;
931 assert!(
932 matches!(&outcome, PairOutcome::Failed(e) if e.name == "org.bluez.Error.AuthenticationRejected"),
933 "{outcome:?}"
934 );
935 bluez.assert_session_closed().await;
936 assert_eq!(bluez.calls(), [GET, REGISTER, PAIR, UNREGISTER]);
937
938 bluez.script(true, PairAnswer::Hang);
940 assert_eq!(
941 run_session(path(A), budget).await,
942 PairOutcome::AlreadyPaired
943 );
944 bluez.assert_session_closed().await;
945 assert_eq!(bluez.calls(), [GET]);
946
947 bluez.script(false, PairAnswer::Hang);
950 assert_eq!(
951 run_session(path(A), Duration::from_secs(1)).await,
952 PairOutcome::TimedOut
953 );
954 bluez.assert_session_closed().await;
955 assert_eq!(bluez.calls(), [GET, REGISTER, PAIR, GET, CANCEL, UNREGISTER]);
956
957 let pair_held = bluez.script(false, PairAnswer::Hang);
960 tokio::select! {
961 outcome = run_session(path(A), budget) => {
962 panic!("the session should still be pairing, got {outcome:?}")
963 }
964 () = pair_held.notified() => {}
965 }
966 bluez.assert_session_closed().await;
967 assert_eq!(bluez.calls(), [GET, REGISTER, PAIR]);
968
969 bluez.script(false, PairAnswer::BondThenHold);
976 assert_eq!(
977 run_session(path(A), Duration::from_secs(3)).await,
978 PairOutcome::Paired
979 );
980 tokio::time::sleep(Duration::from_millis(1500)).await;
981 assert!(
982 bluez.session_is_open().await,
983 "the session closed its connection before BlueZ answered Pair"
984 );
985 bluez.answer_held_pair();
986 bluez.assert_session_closed().await;
987 assert_eq!(bluez.calls(), [GET, REGISTER, PAIR, GET, UNREGISTER]);
988 })
989 .await;
990 }
991}