| 1 | //! Byte transports. Device protocols only see the [`Transport`] trait, so they can be tested |
| 2 | //! against a scripted mock instead of real buds. |
| 3 | |
| 4 | #[cfg(target_os = "linux")] |
| 5 | use std::io; |
| 6 | use std::time::Instant; |
| 7 | |
| 8 | use crate::debug; |
| 9 | #[cfg(target_os = "linux")] |
| 10 | use crate::error::Error; |
| 11 | use crate::error::Result; |
| 12 | |
| 13 | #[cfg(target_os = "linux")] |
| 14 | pub mod rfcomm; |
| 15 | #[cfg(target_os = "linux")] |
| 16 | pub mod sdp; |
| 17 | #[cfg(target_os = "linux")] |
| 18 | mod socket; |
| 19 | |
| 20 | #[cfg(test)] |
| 21 | pub(crate) mod mock; |
| 22 | |
| 23 | /// A bidirectional byte stream to the buds. |
| 24 | pub trait Transport: Send { |
| 25 | /// Send all of `bytes`. |
| 26 | fn send(&mut self, bytes: &[u8]) -> Result<()>; |
| 27 | |
| 28 | /// The next bytes from the buds, or `None` once `deadline` passes with nothing to read. |
| 29 | fn recv(&mut self, deadline: Instant) -> Result<Option<Vec<u8>>>; |
| 30 | } |
| 31 | |
| 32 | /// Wraps a transport and prints every chunk sent and received (`--debug`). |
| 33 | pub struct Hexdump { |
| 34 | inner: Box<dyn Transport>, |
| 35 | start: Instant, |
| 36 | } |
| 37 | |
| 38 | impl Hexdump { |
| 39 | pub fn new(inner: Box<dyn Transport>) -> Self { |
| 40 | Self { |
| 41 | inner, |
| 42 | start: Instant::now(), |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | fn print(&self, direction: &str, bytes: &[u8]) { |
| 47 | let elapsed = self.start.elapsed().as_secs_f64(); |
| 48 | debug::line(&format!("{elapsed:>7.3} {direction} {}", debug::hex(bytes))); |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | impl Transport for Hexdump { |
| 53 | fn send(&mut self, bytes: &[u8]) -> Result<()> { |
| 54 | self.print("TX", bytes); |
| 55 | self.inner.send(bytes) |
| 56 | } |
| 57 | |
| 58 | fn recv(&mut self, deadline: Instant) -> Result<Option<Vec<u8>>> { |
| 59 | let received = self.inner.recv(deadline)?; |
| 60 | if let Some(bytes) = &received { |
| 61 | self.print("RX", bytes); |
| 62 | } |
| 63 | Ok(received) |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | /// What an OS error on a Bluetooth socket means for budctl. |
| 68 | /// `may_be_busy` is set only for the RFCOMM connect, where a refusal means another client |
| 69 | /// holds the channel. Spec: transport.md#errno |
| 70 | #[cfg(target_os = "linux")] |
| 71 | pub(crate) fn classify(error: io::Error, context: &'static str, may_be_busy: bool) -> Error { |
| 72 | let detail = || format!("{context}: {error}"); |
| 73 | match error.raw_os_error() { |
| 74 | Some(libc::EBUSY | libc::EADDRINUSE | libc::ECONNREFUSED) if may_be_busy => Error::ChannelBusy(detail()), |
| 75 | Some( |
| 76 | libc::EHOSTDOWN |
| 77 | | libc::EHOSTUNREACH |
| 78 | | libc::ENOTCONN |
| 79 | | libc::ECONNRESET |
| 80 | | libc::ECONNABORTED |
| 81 | | libc::EPIPE |
| 82 | | libc::ENODEV |
| 83 | | libc::ENETDOWN, |
| 84 | ) => Error::NotConnected(detail()), |
| 85 | Some(libc::ETIMEDOUT | libc::EINPROGRESS | libc::EAGAIN) => Error::Timeout(detail()), |
| 86 | _ => Error::Os { context, source: error }, |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | #[cfg(all(test, target_os = "linux"))] |
| 91 | mod tests { |
| 92 | use super::*; |
| 93 | use crate::error::Kind; |
| 94 | |
| 95 | fn kind_of(errno: i32, may_be_busy: bool) -> Kind { |
| 96 | classify(io::Error::from_raw_os_error(errno), "test", may_be_busy).kind() |
| 97 | } |
| 98 | |
| 99 | #[test] |
| 100 | fn errno_maps_to_the_documented_kinds() { |
| 101 | assert_eq!(kind_of(libc::ECONNREFUSED, true), Kind::Busy); |
| 102 | assert_eq!(kind_of(libc::EBUSY, true), Kind::Busy); |
| 103 | assert_eq!(kind_of(libc::ECONNREFUSED, false), Kind::NotFound); |
| 104 | assert_eq!(kind_of(libc::EHOSTDOWN, true), Kind::NotFound); |
| 105 | assert_eq!(kind_of(libc::ECONNRESET, false), Kind::NotFound); |
| 106 | assert_eq!(kind_of(libc::EINPROGRESS, true), Kind::Failed); |
| 107 | assert_eq!(kind_of(libc::ETIMEDOUT, false), Kind::Failed); |
| 108 | assert_eq!(kind_of(libc::EACCES, false), Kind::NotFound); |
| 109 | } |
| 110 | } |