blob: c18fcec5f156123d8e10986b16b93187075533f3 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{format_err, Context as _, Error};
use fidl_fuchsia_bluetooth::{HostId as FidlHostId, PeerId as FidlPeerId};
use fidl_fuchsia_bluetooth_sys::{
AccessMarker, AccessProxy, BondableMode, HostWatcherMarker, HostWatcherProxy,
PairingDelegateMarker, PairingMarker, PairingOptions, PairingProxy, PairingSecurityLevel,
ProcedureTokenProxy, TechnologyType,
};
use fuchsia_async as fasync;
use fuchsia_bluetooth::types::io_capabilities::{InputCapability, OutputCapability};
use fuchsia_bluetooth::types::{addresses_to_custom_string, HostId, HostInfo, Peer, PeerId};
use fuchsia_component::client::connect_to_protocol;
use fuchsia_sync::Mutex;
use futures::{channel::mpsc, select, FutureExt, Sink, SinkExt, Stream, StreamExt, TryFutureExt};
use pin_utils::pin_mut;
use prettytable::{cell, format, row, Row, Table};
use regex::Regex;
use rustyline::{error::ReadlineError, CompletionType, Config, EditMode, Editor};
use std::sync::Arc;
use std::thread;
use std::{cmp::Ordering, collections::HashMap, str::FromStr};
use crate::{
commands::{Cmd, CmdHelper, ReplControl},
types::{DeviceClass, MajorClass, MinorClass, TryInto},
};
mod commands;
mod types;
static PROMPT: &str = "\x1b[34mbt>\x1b[0m ";
/// Escape code to clear the pty line on which the cursor is located.
/// Used when event output is intermingled with the REPL prompt.
static CLEAR_LINE: &str = "\x1b[2K";
async fn get_active_host(state: &Mutex<State>) -> Result<String, Error> {
let state = state.lock();
let active_iter = state.hosts.iter().find(|&h| h.active);
match active_iter {
Some(host) => Ok(host.to_string()),
None => Ok(String::from("No host instances")),
}
}
async fn get_hosts(state: &Mutex<State>) -> Result<String, Error> {
let hosts = &state.lock().hosts;
if hosts.is_empty() {
return Ok(String::from("No host instances detected"));
}
// Create table of results
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
let _ = table.set_titles(row![
"HostId",
"Addresses",
"Active",
"Technology",
"Name",
"Discoverable",
"Discovering",
]);
for host in hosts {
let _ = table.add_row(row![
host.id.to_string(),
addresses_to_custom_string(&host.addresses, "\n"),
host.active.to_string(),
format!("{:?}", host.technology),
host.local_name.clone().unwrap_or("(unknown)".to_string()),
host.discoverable.to_string(),
host.discovering.to_string(),
]);
}
Ok(format!("{}", table))
}
async fn set_active_host<'a>(
args: &'a [&'a str],
host_svc: &'a HostWatcherProxy,
) -> Result<String, Error> {
if args.len() != 1 {
return Err(format_err!("usage: {}", Cmd::SetController.cmd_help()));
}
println!("Setting active controller");
let host_id = HostId::from_str(args[0])?;
let fidl_host_id: FidlHostId = host_id.into();
match host_svc.set_active(&fidl_host_id).await {
Ok(_) => Ok(String::new()),
Err(err) => Ok(format!("Error setting active controller: {}", err)),
}
}
async fn set_local_name<'a>(
args: &'a [&'a str],
access_svc: &'a AccessProxy,
) -> Result<String, Error> {
if args.len() != 1 {
return Ok(format!("usage: {}", Cmd::SetLocalName.cmd_help()));
}
println!("Setting local name of the active controller");
match access_svc.set_local_name(args[0]) {
Ok(_) => Ok(String::new()),
Err(err) => Ok(format!("Error setting local name: {:?}", err)),
}
}
/// Set the class of device for the currently active controller. Arguments are optional, and
/// defaults will be used if arguments aren't provided.
///
/// Returns an error if the input is not recognized as a valid device class .
async fn set_device_class<'a>(
args: &'a [&'a str],
access_svc: &'a AccessProxy,
) -> Result<String, Error> {
let mut args = args.iter();
println!("Setting device class of the active controller");
let device_class = DeviceClass {
major: args
.next()
.map(|arg| TryInto::try_into(&**arg))
.unwrap_or(Ok(MajorClass::Uncategorized))?,
minor: args
.next()
.map(|arg| TryInto::try_into(&**arg))
.unwrap_or(Ok(MinorClass::not_set()))?,
service: TryInto::try_into(args)?,
}
.into();
match access_svc.set_device_class(&device_class) {
Ok(_) => Ok(format!("Set device class to 0x{:x}", device_class.value)),
Err(err) => Ok(format!("Error setting device class: {}", err)),
}
}
fn match_peer<'a>(pattern: &'a str, peer: &Peer) -> bool {
let pattern_upper = &pattern.to_uppercase();
peer.id.to_string().to_uppercase().contains(pattern_upper)
|| peer.address.to_string().to_uppercase().contains(pattern_upper)
|| peer.name.as_ref().is_some_and(|p| p.contains(pattern))
}
/// Order connected peers as greater than unconnected peers and bonded peers greater than unbonded
/// peers.
fn cmp_peers(a: &Peer, b: &Peer) -> Ordering {
(a.connected, a.bonded).cmp(&(b.connected, b.bonded))
}
fn get_peers<'a>(args: &'a [&'a str], state: &Mutex<State>, full_details: bool) -> String {
let find = match args.len() {
0 => "",
1 => args[0],
_ => return format!("usage: {}", Cmd::ListPeers.cmd_help()),
};
let state = state.lock();
if state.peers.is_empty() {
return String::from("No known peers");
}
let mut peers: Vec<&Peer> = state.peers.values().filter(|p| match_peer(&find, p)).collect();
peers.sort_by(|a, b| cmp_peers(&*a, &*b));
let matched = format!("Showing {}/{} peers\n", peers.len(), state.peers.len());
if full_details {
return String::from_iter(
std::iter::once(matched).chain(peers.iter().map(|p| p.to_string())),
);
}
// Create table of results
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_NO_BORDER);
let _ = table.set_titles(row![
"PeerId",
"Address",
"Technology",
"Name",
"Appearance",
"Connected",
"Bonded",
]);
for val in peers.into_iter() {
let _ = table.add_row(peer_to_table_row(val));
}
[matched, format!("{}", table)].join("\n")
}
/// Get the string representation of a peer from either an identifier or address
fn get_peer<'a>(args: &'a [&'a str], state: &Mutex<State>) -> String {
if args.len() != 1 {
return format!("usage: {}", Cmd::Peer.cmd_help());
}
to_identifier(state, args[0])
.and_then(|id| state.lock().peers.get(&id).map(|peer| peer.to_string()))
.unwrap_or_else(|| String::from("No known peer"))
}
/// Returns basic peer information formatted as a prettytable Row
fn peer_to_table_row(peer: &Peer) -> Row {
row![
peer.id.to_string(),
peer.address.to_string(),
format! {"{:?}", peer.technology},
peer.name.as_ref().map_or("".to_string(), |x| format!("{:?}", x)),
peer.appearance.as_ref().map_or("".to_string(), |x| format!("{:?}", x)),
peer.connected.to_string(),
peer.bonded.to_string(),
]
}
async fn set_discovery(
discovery: bool,
state: &Mutex<State>,
access_svc: &AccessProxy,
) -> Result<String, Error> {
println!("{} Discovery!", if discovery { "Starting" } else { "Stopping" });
if !discovery {
state.lock().discovery_token = None;
return Ok(String::new());
}
let (token, token_server) = fidl::endpoints::create_proxy()?;
match access_svc.start_discovery(token_server).await? {
Ok(_) => {
state.lock().discovery_token = Some(token);
Ok(String::new())
}
Err(err) => Ok(format!("Discovery error: {:?}", err)),
}
}
// Find the identifier for a `Peer` based on a `key` that is either an identifier or an
// address.
// Returns `None` if the given address does not belong to a known peer.
fn to_identifier(state: &Mutex<State>, key: &str) -> Option<PeerId> {
// Compile regex inline because it is not ever expected to be a bottleneck
let address_pattern = Regex::new(r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$")
.expect("Could not compile mac address regex pattern.");
if address_pattern.is_match(key) {
state.lock().peers.values().find(|peer| &peer.address == key).map(|peer| peer.id)
} else {
key.parse().ok()
}
}
async fn connect<'a>(
args: &'a [&'a str],
state: &'a Mutex<State>,
access_proxy: &'a AccessProxy,
with_pairing: Option<&'a PairingProxy>,
) -> Result<String, Error> {
if args.len() != 1 {
return Ok(format!("usage: {}", Cmd::Connect.cmd_help()));
}
// `args[0]` is the identifier of the peer to connect to
let peer_id = match to_identifier(state, args[0]) {
Some(id) => id,
None => return Ok(format!("Unable to connect: Unknown address {}", args[0])),
};
if let Err(e) = access_proxy.connect(&peer_id.into()).await? {
return Ok(format!("connect error: {:?}", e));
}
let pairing = with_pairing
.map(|p| create_pairing_task(InputCapability::None, OutputCapability::None, &p))
.transpose()?;
if let Some((pairing_task, mut recv)) = pairing {
if let Some((paired_id, paired)) = recv.next().await {
// If pairing was completed, exit.
let _ = pairing_task.cancel().await;
println!(
"Completed {} pairing for {}.",
if paired { "successful" } else { "unsuccessful" },
paired_id
);
} else {
println!("Note: No pairing occurred with {}.", peer_id);
}
}
Ok(String::new())
}
fn parse_disconnect<'a>(args: &'a [&'a str], state: &'a Mutex<State>) -> Result<PeerId, String> {
if args.len() != 1 {
return Err(format!("usage: {}", Cmd::Disconnect.cmd_help()));
}
// `args[0]` is the identifier of the peer to connect to
match to_identifier(state, args[0]) {
Some(id) => Ok(id),
None => return Err(format!("Unable to disconnect: Unknown address {}", args[0])),
}
}
async fn handle_disconnect(id: PeerId, access_svc: &AccessProxy) -> Result<String, Error> {
let response = access_svc.disconnect(&id.into()).await?;
match response {
Ok(_) => Ok(String::new()),
Err(err) => Ok(format!("Disconnect error: {:?}", err)),
}
}
async fn disconnect<'a>(
args: &'a [&'a str],
state: &'a Mutex<State>,
access_svc: &'a AccessProxy,
) -> Result<String, Error> {
match parse_disconnect(args, state) {
Ok(id) => handle_disconnect(id, access_svc).await,
Err(msg) => Ok(msg),
}
}
fn parse_pairing_security_level(level: &str) -> Result<PairingSecurityLevel, String> {
match level.to_ascii_uppercase().as_str() {
"AUTH" => Ok(PairingSecurityLevel::Authenticated),
"ENC" => Ok(PairingSecurityLevel::Encrypted),
_ => {
return Err(
"Unable to pair: security level must be either \"AUTH\" or \"ENC\"".to_string()
)
}
}
}
fn parse_bondable_mode(mode: &str) -> Result<BondableMode, String> {
match mode.to_ascii_uppercase().as_str() {
"T" => Ok(BondableMode::Bondable),
"F" => Ok(BondableMode::NonBondable),
_ => return Err("Bondable mode must be either \"T\" or \"F\"".to_string()),
}
}
fn parse_pairing_transport(transport: &str) -> Result<TechnologyType, String> {
match transport.to_ascii_uppercase().as_str() {
"BREDR" | "CLASSIC" => Ok(TechnologyType::Classic),
"LE" => Ok(TechnologyType::LowEnergy),
_ => {
return Err("If present, transport must be \"BREDR\"/\"CLASSIC\" or \"LE\"".to_string())
}
}
}
fn parse_pair(args: &[&str], state: &Mutex<State>) -> Result<(FidlPeerId, PairingOptions), String> {
if args.len() < 3 || args.len() > 4 {
return Err(format!("usage: {}", Cmd::Pair.cmd_help()));
}
// `args[0]` is the identifier of the peer to connect to
let peer_id = match to_identifier(state, args[0]) {
Some(id) => id,
None => return Err(format!("Unable to pair: invalid peer address: {}", args[0])),
};
let le_security_level = Some(parse_pairing_security_level(args[1])?);
// `args[2]` is the requested bonding preference of the pairing
let bondable_mode = Some(parse_bondable_mode(args[2])?);
// if `args[3]` is present, it corresponds to the connected transport over which to pair
let transport = if args.len() == 4 { Some(parse_pairing_transport(args[3])?) } else { None };
Ok((
peer_id.into(),
PairingOptions { le_security_level, bondable_mode, transport, ..Default::default() },
))
}
async fn handle_pair(
peer_id: FidlPeerId,
pairing_opts: PairingOptions,
access_svc: &AccessProxy,
) -> Result<String, Error> {
match access_svc.pair(&peer_id, &pairing_opts).await? {
Ok(_) => Ok(String::new()),
Err(err) => Ok(format!("Pair error: {:?}", err)),
}
}
async fn pair(
args: &[&str],
state: &Mutex<State>,
access_svc: &AccessProxy,
) -> Result<String, Error> {
match parse_pair(args, state) {
Ok((peer_id, pairing_opts)) => handle_pair(peer_id, pairing_opts, access_svc).await,
Err(e) => Ok(e),
}
}
// Creates a pairing task with the given input / output capabilities which will prompt for consent
// to the pairing. The returned task will continue prompting for requests until it is dropped.
fn create_pairing_task(
input_cap: InputCapability,
output_cap: OutputCapability,
pairing_svc: &PairingProxy,
) -> Result<(fasync::Task<()>, mpsc::Receiver<(PeerId, bool)>), Error> {
let (pairing_delegate_client, delegate_stream) =
fidl::endpoints::create_request_stream::<PairingDelegateMarker>()?;
let (sender, recv) = mpsc::channel(0);
let pairing_delegate_server = pairing_delegate::handle_requests(delegate_stream, sender);
let _ = pairing_svc.set_pairing_delegate(
input_cap.into(),
output_cap.into(),
pairing_delegate_client,
);
let task = fasync::Task::spawn(pairing_delegate_server.map(|res| println!("{res:?}")));
println!(
"Pairing delegate setup with input capability {:?} and output capability {:?}.",
input_cap, output_cap
);
Ok((task, recv))
}
async fn allow_pairing(args: &[&str], access_svc: &PairingProxy) -> Result<String, Error> {
let (input_cap, output_cap) = match args.len() {
0 => (InputCapability::None, OutputCapability::None),
2 => (
InputCapability::from_str(args[0]).map_err(|_| {
format_err!("unknown input capability: {}", Cmd::AllowPairing.cmd_help())
})?,
OutputCapability::from_str(args[1]).map_err(|_| {
format_err!("unknown output capability: {}", Cmd::AllowPairing.cmd_help())
})?,
),
_ => return Err(format_err!("usage: {}", Cmd::AllowPairing.cmd_help())),
};
let (delegate_task, mut receiver) = create_pairing_task(input_cap, output_cap, access_svc)?;
if let Some((paired_id, paired)) = receiver.next().await {
// If pairing was completed, exit.
let _ = delegate_task.cancel().await;
return Ok(format!(
"Completed {} pairing with {}.",
if paired { "successful" } else { "unsuccessful" },
paired_id
));
}
Err(format_err!("Pairing delegate closed without a pairing"))
}
async fn forget<'a>(
args: &'a [&'a str],
state: &'a Mutex<State>,
access_svc: &'a AccessProxy,
) -> Result<String, Error> {
if args.len() != 1 {
return Ok(format!("usage: {}", Cmd::Forget.cmd_help()));
}
// `args[0]` is the identifier of the remote device to connect to
let peer_id = match to_identifier(state, args[0]) {
Some(id) => id,
None => return Ok(format!("Unable to forget: Unknown address {}", args[0])),
};
match access_svc.forget(&peer_id.into()).await? {
Ok(_) => {
println!("Peer has been removed");
Ok(String::new())
}
Err(err) => Ok(format!("Forget error: {:?}", err)),
}
}
async fn set_discoverable(
discoverable: bool,
access_svc: &AccessProxy,
state: &Mutex<State>,
) -> Result<String, Error> {
if discoverable {
println!("Becoming discoverable..");
if state.lock().discoverable_token.is_some() {
return Ok(String::new());
}
let (token, token_server) = fidl::endpoints::create_proxy()?;
match access_svc.make_discoverable(token_server).await? {
Ok(_) => {
state.lock().discoverable_token = Some(token);
Ok(String::new())
}
Err(err) => Ok(format!("MakeDiscoverable error: {:?}", err)),
}
} else {
println!("Revoking discoverability..");
state.lock().discoverable_token = None;
Ok(String::new())
}
}
fn print_peer_state_updates(state: &State, peer: &Peer) {
if let Some(msg) = peer_state_updates(state, peer) {
println!("{} {} {}", peer.id, peer.address, msg)
}
}
fn peer_state_updates(state: &State, peer: &Peer) -> Option<String> {
let previous = state.peers.get(&peer.id);
let was_connected = previous.is_some_and(|p| p.connected);
let was_bonded = previous.is_some_and(|p| p.bonded);
let conn_str = match (was_connected, peer.connected) {
(false, true) => Some("[connected]"),
(true, false) => Some("[disconnected]"),
_ => None,
};
let bond_str = match (was_bonded, peer.bonded) {
(false, true) => Some("[bonded]"),
(true, false) => Some("[unbonded]"),
_ => None,
};
match (conn_str, bond_str) {
(Some(a), Some(b)) => Some(format!("{} {}", a, b)),
(Some(a), None) => Some(a.to_string()),
(None, Some(b)) => Some(b.to_string()),
(None, None) => None,
}
}
/// Tracks all state local to the command line tool.
pub struct State {
pub peers: HashMap<PeerId, Peer>,
pub discoverable_token: Option<ProcedureTokenProxy>,
pub discovery_token: Option<ProcedureTokenProxy>,
pub hosts: Vec<HostInfo>,
}
impl State {
pub fn new() -> State {
State {
peers: HashMap::new(),
discoverable_token: None,
discovery_token: None,
hosts: vec![],
}
}
}
async fn parse_and_handle_cmd(
bt_svc: &AccessProxy,
host_svc: &HostWatcherProxy,
pairing_svc: &PairingProxy,
state: Arc<Mutex<State>>,
line: String,
) -> Result<ReplControl, Error> {
match parse_cmd(line) {
ParseResult::Valid((cmd, args)) => {
handle_cmd(bt_svc, host_svc, pairing_svc, state, cmd, args).await
}
ParseResult::Empty => Ok(ReplControl::Continue),
ParseResult::Error(err) => {
println!("{}", err);
Ok(ReplControl::Continue)
}
}
}
enum ParseResult<T> {
Valid(T),
Empty,
Error(String),
}
/// Parse a single raw input command from a user into the command type and argument list
fn parse_cmd(line: String) -> ParseResult<(Cmd, Vec<String>)> {
let components: Vec<_> = line.trim().split_whitespace().collect();
match components.split_first() {
Some((raw_cmd, args)) => match raw_cmd.parse() {
Ok(cmd) => {
let args = args.into_iter().map(|s| s.to_string()).collect();
ParseResult::Valid((cmd, args))
}
Err(_) => ParseResult::Error(format!("\"{}\" is not a valid command", raw_cmd)),
},
None => ParseResult::Empty,
}
}
/// Handle a single raw input command from a user and indicate whether the command should
/// result in continuation or breaking of the read evaluate print loop.
async fn handle_cmd(
access_svc: &AccessProxy,
host_svc: &HostWatcherProxy,
pairing_svc: &PairingProxy,
state: Arc<Mutex<State>>,
cmd: Cmd,
args: Vec<String>,
) -> Result<ReplControl, Error> {
let args: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let args: &[&str] = &*args;
let res = match cmd {
Cmd::Connect => connect(args, &state, &access_svc, None).await,
Cmd::Bond => connect(args, &state, &access_svc, Some(&pairing_svc)).await,
Cmd::Disconnect => disconnect(args, &state, &access_svc).await,
Cmd::Pair => pair(args, &state, &access_svc).await,
Cmd::AllowPairing => allow_pairing(args, &pairing_svc).await,
Cmd::Forget => forget(args, &state, &access_svc).await,
Cmd::StartDiscovery => set_discovery(true, &state, &access_svc).await,
Cmd::StopDiscovery => set_discovery(false, &state, &access_svc).await,
Cmd::Discoverable => set_discoverable(true, &access_svc, &state).await,
Cmd::NotDiscoverable => set_discoverable(false, &access_svc, &state).await,
Cmd::ListPeers => Ok(get_peers(args, &state, false)),
Cmd::ShowPeers => Ok(get_peers(args, &state, true)),
Cmd::Peer => Ok(get_peer(args, &state)),
Cmd::ListControllers => get_hosts(&state).await,
Cmd::SetController => set_active_host(args, &host_svc).await,
Cmd::SetLocalName => set_local_name(args, &access_svc).await,
Cmd::SetDeviceClass => set_device_class(args, &access_svc).await,
Cmd::Controller => get_active_host(&state).await,
Cmd::Help => Ok(Cmd::help_msg().to_string()),
Cmd::Exit | Cmd::Quit => return Ok(ReplControl::Break),
}?;
if res != "" {
println!("{}", res);
}
Ok(ReplControl::Continue)
}
/// Generates a rustyline `Editor` in a separate thread to manage user input. This input is returned
/// as a `Stream` of lines entered by the user.
///
/// The thread exits and the `Stream` is exhausted when an error occurs on stdin or the user
/// sends a ctrl-c or ctrl-d sequence.
///
/// Because rustyline shares control over output to the screen with other parts of the system, a
/// `Sink` is passed to the caller to send acknowledgements that a command has been processed and
/// that rustyline should handle the next line of input.
fn cmd_stream(
state: Arc<Mutex<State>>,
) -> (
thread::JoinHandle<Result<(), Error>>,
impl Stream<Item = String>,
impl Sink<(), Error = mpsc::SendError>,
) {
// Editor thread and command processing thread must be synchronized so that output
// is printed in the correct order.
let (mut cmd_sender, cmd_receiver) = mpsc::channel(0);
let (ack_sender, mut ack_receiver) = mpsc::channel(0);
let repl_thread = thread::spawn(move || -> Result<(), Error> {
let mut exec = fasync::LocalExecutor::new();
let fut = async {
let config = Config::builder()
.auto_add_history(true)
.history_ignore_space(true)
.completion_type(CompletionType::List)
.edit_mode(EditMode::Emacs)
.build();
let c = CmdHelper::new(state);
let mut rl: Editor<CmdHelper> = Editor::with_config(config);
rl.set_helper(Some(c));
loop {
let readline = rl.readline(PROMPT);
match readline {
Ok(line) => {
cmd_sender.try_send(line)?;
}
Err(ReadlineError::Eof) | Err(ReadlineError::Interrupted) => {
return Ok(());
}
Err(e) => {
println!("Error: {:?}", e);
return Err(e.into());
}
}
// wait until processing thread is finished evaluating the last command
// before running the next loop in the repl
if ack_receiver.next().await.is_none() {
return Ok(());
};
}
};
exec.run_singlethreaded(fut)
});
(repl_thread, cmd_receiver, ack_sender)
}
async fn watch_peers(access_svc: AccessProxy, state: Arc<Mutex<State>>) -> Result<(), Error> {
// Used to avoid printing all peers on first watch_peers() call.
let mut first_loop = true;
loop {
let (updated, removed) = access_svc.watch_peers().await?;
for peer in updated.into_iter() {
let peer = Peer::try_from(peer).context("Malformed FIDL peer")?;
if !first_loop {
print!("{}", CLEAR_LINE);
print_peer_state_updates(&state.lock(), &peer);
}
let _ = state.lock().peers.insert(peer.id, peer);
}
for id in removed.into_iter() {
let peer_id = PeerId::try_from(id).context("Malformed FIDL peer id")?;
let _ = state.lock().peers.remove(&peer_id);
}
first_loop = false;
}
}
async fn watch_hosts(host_svc: HostWatcherProxy, state: Arc<Mutex<State>>) -> Result<(), Error> {
let mut first_result = true;
loop {
let fidl_hosts = host_svc.watch().await?;
let mut hosts = Vec::<HostInfo>::new();
if !first_result && !hosts.is_empty() {
print!("{}", CLEAR_LINE);
}
for fidl_host in &fidl_hosts {
let host = HostInfo::try_from(fidl_host)?;
if !first_result {
println!(
"Controller updated: [addresses: {}, active: {}, discoverable: {}, discovering: {}]",
addresses_to_custom_string(&host.addresses, " "),
host.active,
host.discoverable,
host.discovering
);
}
hosts.push(host);
}
state.lock().hosts = hosts;
first_result = false;
}
}
/// REPL execution
async fn run_repl(
access_svc: AccessProxy,
host_svc: HostWatcherProxy,
pairing_svc: PairingProxy,
state: Arc<Mutex<State>>,
) -> Result<(), Error> {
// `cmd_stream` blocks on input in a separate thread and passes commands and acks back to
// the main thread via async channels.
let (repl_thread, mut commands, mut acks) = cmd_stream(state.clone());
while let Some(cmd) = commands.next().await {
match parse_and_handle_cmd(&access_svc, &host_svc, &pairing_svc, state.clone(), cmd).await {
Ok(ReplControl::Continue) => {} // continue silently
Ok(ReplControl::Break) => break,
Err(e) => println!("Error handling command: {}", e),
}
acks.send(()).await?;
}
// Close out the streams, which will cause the REPL thread to terminate.
drop(commands);
drop(acks);
let _ = repl_thread.join();
Ok(())
}
#[fasync::run_singlethreaded]
async fn main() -> Result<(), Error> {
let access_svc = connect_to_protocol::<AccessMarker>()
.context("failed to connect to bluetooth access interface")?;
let host_watcher_svc =
connect_to_protocol::<HostWatcherMarker>().context("failed to watch hosts")?;
let pairing_svc = connect_to_protocol::<PairingMarker>()
.context("failed to connect to bluetooth pairing interface")?;
let state = Arc::new(Mutex::new(State::new()));
let peer_watcher = watch_peers(access_svc.clone(), state.clone());
let repl = run_repl(access_svc, host_watcher_svc.clone(), pairing_svc, state.clone())
.map_err(|e| e.context("REPL failed unexpectedly").into());
let host_watcher = watch_hosts(host_watcher_svc, state);
pin_mut!(repl);
pin_mut!(peer_watcher);
select! {
r = repl.fuse() => r,
p = peer_watcher.fuse() => p,
h = host_watcher.fuse() => h,
}
}
#[cfg(test)]
mod tests {
use super::*;
use {
assert_matches::assert_matches,
bt_fidl_mocks::sys::{AccessMock, PairingMock},
fidl::endpoints::Proxy,
fidl_fuchsia_bluetooth as fbt, fidl_fuchsia_bluetooth_sys as fsys,
fidl_fuchsia_bluetooth_sys::{InputCapability, OutputCapability},
fuchsia_bluetooth::types::Address,
fuchsia_zircon::{Duration, DurationNum},
futures::join,
std::task::Poll,
};
fn peer(connected: bool, bonded: bool) -> Peer {
Peer {
id: PeerId(0xdeadbeef),
address: Address::Public([1, 0, 0, 0, 0, 0]),
technology: fsys::TechnologyType::LowEnergy,
connected,
bonded,
name: None,
appearance: Some(fbt::Appearance::Phone),
device_class: None,
rssi: None,
tx_power: None,
le_services: vec![],
bredr_services: vec![],
}
}
fn named_peer(id: PeerId, address: Address, name: Option<String>) -> Peer {
Peer {
id,
address,
technology: fsys::TechnologyType::LowEnergy,
connected: false,
bonded: false,
name,
appearance: Some(fbt::Appearance::Phone),
device_class: None,
rssi: None,
tx_power: None,
le_services: vec![],
bredr_services: vec![],
}
}
fn custom_peer(
id: PeerId,
address: Address,
connected: bool,
bonded: bool,
rssi: Option<i8>,
) -> Peer {
Peer {
id,
address,
technology: fsys::TechnologyType::LowEnergy,
connected,
bonded,
name: None,
appearance: Some(fbt::Appearance::Phone),
device_class: None,
rssi,
tx_power: None,
le_services: vec![],
bredr_services: vec![],
}
}
fn custom_host(
id: HostId,
address: Address,
active: bool,
discoverable: bool,
discovering: bool,
name: Option<String>,
) -> HostInfo {
HostInfo {
id,
technology: fsys::TechnologyType::LowEnergy,
addresses: vec![address],
active,
local_name: name,
discoverable,
discovering,
}
}
fn state_with(p: Peer) -> State {
let mut state = State::new();
let _ = state.peers.insert(p.id, p);
state
}
#[fuchsia::test]
async fn test_get_hosts() {
// Fields for table view of hosts
let fields = Regex::new(r"HostId[ \t]*\|[ \t]*Addresses[ \t]*\|[ \t]*Active[ \t]*\|[ \t]*Technology[ \t]*\|[ \t]*Name[ \t]*\|[ \t]*Discoverable[ \t]*\|[ \t]*Discovering").unwrap();
// No hosts
let mut output = get_hosts(&Mutex::new(State::new())).await.unwrap();
assert!(!fields.is_match(&output));
assert!(output.contains("No host instances"));
let mut state = State::new();
state.hosts.push(custom_host(
HostId(0xbeef),
Address::Public([0x11, 0x00, 0x55, 0x7E, 0xDE, 0xAD]),
false,
false,
false,
Some("Sapphire".to_string()),
));
state.hosts.push(custom_host(
HostId(0xabcd),
Address::Random([0x22, 0x00, 0x55, 0x7E, 0xDE, 0xAD]),
false,
false,
true,
None,
));
let state = Mutex::new(state);
// Hosts exist
output = get_hosts(&state).await.unwrap();
assert!(fields.is_match(&output));
assert!(output.contains("ef"));
assert!(output.contains("cd"));
}
#[test]
fn test_match_peer() {
let nameless_peer =
named_peer(PeerId(0xabcd), Address::Public([0xAB, 0x89, 0x67, 0x45, 0x23, 0x01]), None);
let named_peer = named_peer(
PeerId(0xbeef),
Address::Public([0x11, 0x00, 0x55, 0x7E, 0xDE, 0xAD]),
Some("Sapphire".to_string()),
);
assert!(match_peer("23", &nameless_peer));
assert!(!match_peer("23", &named_peer));
assert!(match_peer("cd", &nameless_peer));
assert!(match_peer("bee", &named_peer));
assert!(match_peer("BEE", &named_peer));
assert!(!match_peer("Sapphire", &nameless_peer));
assert!(match_peer("Sapphire", &named_peer));
assert!(match_peer("", &nameless_peer));
assert!(match_peer("", &named_peer));
assert!(match_peer("DE", &named_peer));
assert!(match_peer("de", &named_peer));
}
#[test]
fn test_get_peers_full_details() {
let mut state = State::new();
let _ = state.peers.insert(
PeerId(0xabcd),
named_peer(PeerId(0xabcd), Address::Public([0xAB, 0x89, 0x67, 0x45, 0x23, 0x01]), None),
);
let _ = state.peers.insert(
PeerId(0xbeef),
named_peer(
PeerId(0xbeef),
Address::Public([0x11, 0x00, 0x55, 0x7E, 0xDE, 0xAD]),
Some("Sapphire".to_string()),
),
);
let state = Mutex::new(state);
let get_peers =
|args: &[&str], state: &Mutex<State>| -> String { get_peers(args, state, true) };
// Fields for detailed view of peers
let fields = Regex::new(r"Id(?s).*Address(?s).*Technology(?s).*Name(?s).*Appearance(?s).*Connected(?s).*Bonded(?s).*LE Services(?s).*BR/EDR Serv\.").unwrap();
// Empty arguments matches everything
assert!(fields.is_match(&get_peers(&[], &state)));
assert!(get_peers(&[], &state).contains("2/2 peers"));
assert!(get_peers(&[], &state).contains("01:23:45"));
assert!(get_peers(&[], &state).contains("AD:DE:7E"));
// No matches prints nothing.
assert!(!fields.is_match(&get_peers(&["nomatch"], &state)));
assert!(get_peers(&["nomatch"], &state).contains("0/2 peers"));
assert!(!get_peers(&["nomatch"], &state).contains("01:23:45"));
assert!(!get_peers(&["nomatch"], &state).contains("AD:DE:7E"));
// We can match either one
assert!(get_peers(&["01:23"], &state).contains("1/2 peers"));
assert!(get_peers(&["01:23"], &state).contains("01:23:45"));
assert!(get_peers(&["abcd"], &state).contains("1/2 peers"));
assert!(get_peers(&["beef"], &state).contains("AD:DE:7E"));
}
#[test]
fn test_get_peers_less_details() {
let mut state = State::new();
let _ = state.peers.insert(
PeerId(0xabcd),
named_peer(PeerId(0xabcd), Address::Public([0xAB, 0x89, 0x67, 0x45, 0x23, 0x01]), None),
);
let _ = state.peers.insert(
PeerId(0xbeef),
named_peer(
PeerId(0xbeef),
Address::Public([0x11, 0x00, 0x55, 0x7E, 0xDE, 0xAD]),
Some("Sapphire".to_string()),
),
);
let state = Mutex::new(state);
let get_peers =
|args: &[&str], state: &Mutex<State>| -> String { get_peers(args, state, false) };
// Fields for table view of peers
let fields = Regex::new(r"PeerId[ \t]*\|[ \t]*Address[ \t]*\|[ \t]*Technology[ \t]*\|[ \t]*Name[ \t]*\|[ \t]*Appearance[ \t]*\|[ \t]*Connected[ \t]*\|[ \t]*Bonded").unwrap();
// Empty arguments matches everything
assert!(fields.is_match(&get_peers(&[], &state)));
assert!(get_peers(&[], &state).contains("2/2 peers"));
assert!(get_peers(&[], &state).contains("01:23:45"));
assert!(get_peers(&[], &state).contains("AD:DE:7E"));
// No matches prints nothing.
assert!(!fields.is_match(&get_peers(&["nomatch"], &state)));
assert!(get_peers(&["nomatch"], &state).contains("0/2 peers"));
assert!(!get_peers(&["nomatch"], &state).contains("01:23:45"));
assert!(!get_peers(&["nomatch"], &state).contains("AD:DE:7E"));
// We can match either one
assert!(get_peers(&["01:23"], &state).contains("1/2 peers"));
assert!(get_peers(&["01:23"], &state).contains("01:23:45"));
assert!(get_peers(&["abcd"], &state).contains("1/2 peers"));
assert!(get_peers(&["beef"], &state).contains("AD:DE:7E"));
}
#[test]
fn cmp_peers_correctly_orders_peers() {
// Sorts connected correctly
let peer_a =
custom_peer(PeerId(0xbeef), Address::Public([1, 0, 0, 0, 0, 0]), false, false, None);
let peer_b =
custom_peer(PeerId(0xbaaf), Address::Public([2, 0, 0, 0, 0, 0]), true, false, None);
assert_eq!(cmp_peers(&peer_a, &peer_b), Ordering::Less);
// Sorts bonded correctly
let peer_a =
custom_peer(PeerId(0xbeef), Address::Public([1, 0, 0, 0, 0, 0]), false, false, None);
let peer_b =
custom_peer(PeerId(0xbaaf), Address::Public([2, 0, 0, 0, 0, 0]), false, true, None);
assert_eq!(cmp_peers(&peer_a, &peer_b), Ordering::Less);
}
#[test]
fn test_peer_updates() {
// Expected Value Table
// each row lists:
// (current peer conn/bond state, new peer conn/bond state, expected string)
let test_cases = vec![
// missing
(None, (true, false), Some("[connected]")),
(None, (true, true), Some("[connected] [bonded]")),
(None, (false, true), Some("[bonded]")),
(None, (false, false), None),
// disconnected, unbonded
(Some((false, false)), (true, false), Some("[connected]")),
(Some((false, false)), (true, true), Some("[connected] [bonded]")),
(Some((false, false)), (false, true), Some("[bonded]")),
(Some((false, false)), (false, false), None),
// connected, unbonded
(Some((true, false)), (true, false), None),
(Some((true, false)), (true, true), Some("[bonded]")),
(Some((true, false)), (false, true), Some("[disconnected] [bonded]")),
(Some((true, false)), (false, false), Some("[disconnected]")),
// disconnected, bonded
(Some((false, true)), (true, false), Some("[connected] [unbonded]")),
(Some((false, true)), (true, true), Some("[connected]")),
(Some((false, true)), (false, true), None),
(Some((false, true)), (false, false), Some("[unbonded]")),
// connected, bonded
(Some((true, true)), (true, false), Some("[unbonded]")),
(Some((true, true)), (true, true), None),
(Some((true, true)), (false, true), Some("[disconnected]")),
(Some((true, true)), (false, false), Some("[disconnected] [unbonded]")),
];
for case in test_cases {
let (prev, (connected, bonded), expected) = case;
let state = match prev {
Some((c, b)) => state_with(peer(c, b)),
None => State::new(),
};
assert_eq!(
peer_state_updates(&state, &peer(connected, bonded)),
expected.map(|s| s.to_string())
);
}
}
// Test that command lines entered parse correctly to the expected disconnect calls
#[test]
fn test_parse_disconnect() {
let state = Mutex::new(state_with(peer(true, false)));
let cases = vec![
// valid peer id
("disconnect deadbeef", Ok(PeerId(0xdeadbeef))),
// unknown address
(
"disconnect 00:00:00:00:00:00",
Err("Unable to disconnect: Unknown address 00:00:00:00:00:00".to_string()),
),
// known address
("disconnect 00:00:00:00:00:01", Ok(PeerId(0xdeadbeef))),
// no id param
("disconnect", Err(format!("usage: {}", Cmd::Disconnect.cmd_help()))),
];
for (line, expected) in cases {
assert_eq!(parse_disconnect_id(line, &state), expected);
}
}
fn parse_disconnect_id(line: &str, state: &Mutex<State>) -> Result<PeerId, String> {
let args = match parse_cmd(line.to_string()) {
ParseResult::Valid((Cmd::Disconnect, args)) => Ok(args),
ParseResult::Valid((_, _)) => Err("Command is not disconnect"),
_ => Err("failed"),
}?;
let args: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let args: &[&str] = &*args;
parse_disconnect(args, state)
}
// Tests that command lines entered parse correctly to the expected pairing calls
#[test]
fn test_parse_pairing_security_level() {
let cases = vec![
("Enc", Ok(PairingSecurityLevel::Encrypted)),
("AUTH", Ok(PairingSecurityLevel::Authenticated)),
(
"SC",
Err("Unable to pair: security level must be either \"AUTH\" or \"ENC\"".to_string()),
),
];
for (input_str, expected) in cases {
assert_eq!(parse_pairing_security_level(input_str), expected);
}
}
#[test]
fn test_parse_bondable_mode() {
let cases = vec![
("T", Ok(BondableMode::Bondable)),
("f", Ok(BondableMode::NonBondable)),
("TEST", Err("Bondable mode must be either \"T\" or \"F\"".to_string())),
];
for (input_str, expected) in cases {
assert_eq!(parse_bondable_mode(input_str), expected);
}
}
#[test]
fn test_parse_pairing_transport() {
let cases = vec![
("CLAssIC", Ok(TechnologyType::Classic)),
("BrEdr", Ok(TechnologyType::Classic)),
("LE", Ok(TechnologyType::LowEnergy)),
(
"TEST",
Err("If present, transport must be \"BREDR\"/\"CLASSIC\" or \"LE\"".to_string()),
),
];
for (input_str, expected) in cases {
assert_eq!(parse_pairing_transport(input_str), expected);
}
}
#[test]
fn test_parse_pair() {
let state = Mutex::new(state_with(custom_peer(
PeerId(0xbeef),
Address::Public([1, 0, 0, 0, 0, 0]),
true,
false,
None,
)));
let cases = vec![
// valid peer id
(
"pair beef ENC T LE",
Ok((
FidlPeerId { value: u64::from_str_radix("beef", 16).unwrap() },
PairingOptions {
le_security_level: Some(PairingSecurityLevel::Encrypted),
bondable_mode: Some(BondableMode::Bondable),
transport: Some(TechnologyType::LowEnergy),
..Default::default()
},
)),
),
// known address, no transport
(
"pair 00:00:00:00:00:01 AUTH F",
Ok((
FidlPeerId { value: u64::from_str_radix("beef", 16).unwrap() },
PairingOptions {
le_security_level: Some(PairingSecurityLevel::Authenticated),
bondable_mode: Some(BondableMode::NonBondable),
transport: None,
..Default::default()
},
)),
),
// no id param
("pair", Err(format!("usage: {}", Cmd::Pair.cmd_help()))),
];
for (line, expected) in cases {
assert_eq!(parse_pair_args(line, &state), expected);
}
}
fn parse_pair_args(
line: &str,
state: &Mutex<State>,
) -> Result<(FidlPeerId, PairingOptions), String> {
let args = match parse_cmd(line.to_string()) {
ParseResult::Valid((Cmd::Pair, args)) => Ok(args),
ParseResult::Valid((_, _)) => Err("Command is not pair"),
_ => Err("failed"),
}?;
let args: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let args: &[&str] = &*args;
parse_pair(args, state)
}
fn timeout() -> Duration {
20.seconds()
}
#[fuchsia::test(allow_stalls = false)]
async fn test_disconnect() {
let peer = peer(true, false);
let peer_id = peer.id;
let peer_id_string = peer.id.to_string();
let args = vec![peer_id_string.as_str()];
let state = Mutex::new(state_with(peer));
let (proxy, mut mock) = AccessMock::new(timeout()).expect("failed to create mock");
let cmd = disconnect(args.as_slice(), &state, &proxy);
let mock_expect = mock.expect_disconnect(peer_id.into(), Ok(()));
let (result, mock_result) = join!(cmd, mock_expect);
let _ = mock_result.expect("mock FIDL expectation not satisfied");
assert_eq!("".to_string(), result.expect("expected success"));
}
#[fuchsia::test(allow_stalls = false)]
async fn test_disconnect_error() {
let peer = peer(true, false);
let peer_id = peer.id;
let peer_id_string = peer.id.to_string();
let args = vec![peer_id_string.as_str()];
let state = Mutex::new(state_with(peer));
let (proxy, mut mock) = AccessMock::new(timeout()).expect("failed to create mock");
let cmd = disconnect(args.as_slice(), &state, &proxy);
let mock_expect = mock.expect_disconnect(peer_id.into(), Err(fsys::Error::Failed));
let (result, mock_result) = join!(cmd, mock_expect);
let _ = mock_result.expect("mock FIDL expectation not satisfied");
assert!(result.expect("expected a result").contains("Disconnect error"));
}
#[test]
fn test_allow_pairing_no_args() {
let mut exec = fasync::TestExecutor::new();
let args = vec![];
let (proxy, mut mock) = PairingMock::new(1.second()).expect("failed to create mock");
let pair = allow_pairing(args.as_slice(), &proxy);
pin_mut!(pair);
assert!(exec.run_until_stalled(&mut pair).is_pending());
let mock_expect =
mock.expect_set_pairing_delegate(InputCapability::None, OutputCapability::None);
assert!(exec.run_singlethreaded(mock_expect).is_ok());
}
#[fuchsia::test]
fn test_allow_pairing_args() {
let mut exec = fasync::TestExecutor::new();
let (proxy, mut mock) = PairingMock::new(1.second()).expect("failed to create mock");
// Enable pairing with confirmation input cap and display output cap.
let args = vec!["confirmation", "display"];
let pair = allow_pairing(args.as_slice(), &proxy);
pin_mut!(pair);
// Should be waiting until pairing is completed.
assert!(exec.run_until_stalled(&mut pair).is_pending());
// Expect pairing delegate to be set with the correct capabilities.
let proxy = exec
.run_singlethreaded(mock.expect_set_pairing_delegate(
InputCapability::Confirmation,
OutputCapability::Display,
))
.expect("cannot get proxy");
assert!(!proxy.is_closed());
// Force closing of the proxy.
std::mem::drop(proxy);
// Verify that allow_pairing existed with error.
assert_matches!(exec.run_until_stalled(&mut pair), Poll::Ready(Err(_)));
}
#[fuchsia::test(allow_stalls = false)]
async fn test_allow_pairing_error() {
// Arguments that don't correspond to any capabilities.
let args = vec!["nonsense", "fake"];
let (proxy, _mock) = PairingMock::new(1.second()).expect("failed to create mock");
assert!(allow_pairing(args.as_slice(), &proxy).await.is_err());
// Incorrect number of arguments.
let args = vec!["none"];
let (proxy, _mock) = PairingMock::new(1.second()).expect("failed to create mock");
assert!(allow_pairing(args.as_slice(), &proxy).await.is_err());
}
#[fuchsia::test(allow_stalls = false)]
async fn test_forget() {
let peer = peer(true, false);
let peer_id = peer.id;
let peer_id_string = peer.id.to_string();
let args = vec![peer_id_string.as_str()];
let state = Mutex::new(state_with(peer));
let (proxy, mut mock) = AccessMock::new(1.second()).expect("failed to create mock");
let cmd = forget(args.as_slice(), &state, &proxy);
let mock_expect = mock.expect_forget(peer_id.into(), Ok(()));
let (result, mock_result) = join!(cmd, mock_expect);
assert!(mock_result.is_ok());
assert_eq!("".to_string(), result.expect("expected success"));
}
#[fuchsia::test(allow_stalls = false)]
async fn test_forget_error() {
let peer = peer(true, false);
let peer_id = peer.id;
let peer_id_string = peer.id.to_string();
let args = vec![peer_id_string.as_str()];
let state = Mutex::new(state_with(peer));
let (proxy, mut mock) = AccessMock::new(1.second()).expect("failed to create mock");
let cmd = forget(args.as_slice(), &state, &proxy);
let mock_expect = mock.expect_forget(peer_id.into(), Err(fsys::Error::Failed));
let (result, mock_result) = join!(cmd, mock_expect);
assert!(mock_result.is_ok());
assert!(result.expect("expected a result").contains("Forget error"));
}
#[fuchsia::test(allow_stalls = false)]
async fn test_pair() {
let peer =
custom_peer(PeerId(0xbeef), Address::Public([1, 0, 0, 0, 0, 0]), true, false, None);
let peer_id = peer.id;
let peer_id_string = peer.id.to_string();
let pairing_options = PairingOptions {
le_security_level: Some(PairingSecurityLevel::Encrypted),
bondable_mode: Some(BondableMode::Bondable),
transport: None,
..Default::default()
};
let args = vec![peer_id_string.as_str(), "ENC", "T"];
let state = Mutex::new(state_with(peer));
let (proxy, mut mock) = AccessMock::new(1.second()).expect("failed to create mock");
let cmd = pair(args.as_slice(), &state, &proxy);
let mock_expect = mock.expect_pair(peer_id.into(), pairing_options, Ok(()));
let (result, mock_result) = join!(cmd, mock_expect);
assert!(mock_result.is_ok());
assert_eq!("".to_string(), result.expect("expected success"));
}
#[fuchsia::test(allow_stalls = false)]
async fn test_pair_error() {
let peer =
custom_peer(PeerId(0xbeef), Address::Public([1, 0, 0, 0, 0, 0]), true, false, None);
let peer_id = peer.id;
let peer_id_string = peer.id.to_string();
let pairing_options = PairingOptions {
le_security_level: Some(PairingSecurityLevel::Encrypted),
bondable_mode: Some(BondableMode::Bondable),
transport: None,
..Default::default()
};
let args = vec![peer_id_string.as_str(), "ENC", "T"];
let state = Mutex::new(state_with(peer));
let (proxy, mut mock) = AccessMock::new(1.second()).expect("failed to create mock");
let cmd = pair(args.as_slice(), &state, &proxy);
let mock_expect =
mock.expect_pair(peer_id.into(), pairing_options, Err(fsys::Error::Failed));
let (result, mock_result) = join!(cmd, mock_expect);
assert!(mock_result.is_ok());
assert!(result.expect("expected a result").contains("Pair error"));
}
}