Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5afaf32b authored by Joseph Hwang's avatar Joseph Hwang
Browse files

floss: btclient: run commands only after adapter is ready

When btclient processes a command in a non-interactive mode, it can
only handle the commands that do not require the adapter readiness
for now.

In order to let btclient be able to handle commands that require the
adapter readiness, in this patch, both interactive and non-interactive
modes are processed in handle_client_command().

As one of the differences between the two modes, the interactive
shell is only started in the interactive mode.

In the non-interactive mode, the command is processed immediately
after the adapter becomes ready. If there is no callback to wait for
the command, btclient exits. Otherwise, btclient will need to wait
until the callback is invoked.

One thing worth noting is that any btclient command that has a
follow-up callback has to invoke the callback through
ForegroundActions::RunCallback(callback) so that the foreground
thread is aware of when to exit.

Bug: 288193772
Tag: #floss
Test: mma -j32
Test: Conduct the following manual tests.
  $ btclient -c "media log"
  > It needs the adapter readiness. It is able dump the media debug log.

  $ btclient --command "media log" --timeout 2
  > It needs the adapter readiness. It is able dump the media debug log.
    It runs the command with a timeout of 2 seconds.

  $ btclient -c "help"
  > It does not need the adapter readiness. It shows the help menu
    and exits immediately.

  $ btclient -c "media"
  > It shows “btclient:error: Invalid arguments” and exits immediately.

  $ btclient -c "hello"
  > It shows "'hello' is an invalid command!" and exits immediately.

Change-Id: Ieff642697b4fa5ba986b06ab945bc87d7ddc636a
parent 87d40daf
Loading
Loading
Loading
Loading
+45 −39
Original line number Diff line number Diff line
@@ -1312,6 +1312,7 @@ impl RPCProxy for QACallback {

pub(crate) struct MediaCallback {
    objpath: String,
    context: Arc<Mutex<ClientContext>>,

    dbus_connection: Arc<SyncConnection>,
    dbus_crossroads: Arc<Mutex<Crossroads>>,
@@ -1320,10 +1321,11 @@ pub(crate) struct MediaCallback {
impl MediaCallback {
    pub(crate) fn new(
        objpath: String,
        context: Arc<Mutex<ClientContext>>,
        dbus_connection: Arc<SyncConnection>,
        dbus_crossroads: Arc<Mutex<Crossroads>>,
    ) -> Self {
        Self { objpath, dbus_connection, dbus_crossroads }
        Self { objpath, context, dbus_connection, dbus_crossroads }
    }
}

@@ -1351,6 +1353,9 @@ impl IBluetoothMediaCallback for MediaCallback {
        pkt_status_in_hex: String,
        pkt_status_in_binary: String,
    ) {
        // Invoke run_callback so that the callback can be handled through
        // ForegroundActions::RunCallback in main.rs.
        self.context.lock().unwrap().run_callback(Box::new(move |_context| {
            let wbs_dump = if active && wbs {
                let mut to_split_binary = pkt_status_in_binary.clone();
                let mut wrapped_binary = String::new();
@@ -1391,6 +1396,7 @@ impl IBluetoothMediaCallback for MediaCallback {
                if wbs { "mSBC" } else { "CVSD" },
                wbs_dump
            );
        }));
    }
}

+7 −4
Original line number Diff line number Diff line
@@ -366,29 +366,32 @@ impl CommandHandler {
    }

    /// Entry point for command and arguments
    pub fn process_cmd_line(&mut self, command: &str, args: &Vec<String>) {
    pub fn process_cmd_line(&mut self, command: &str, args: &Vec<String>) -> bool {
        // Ignore empty line
        match command {
            "" => {}
            "" => false,
            _ => match self.command_options.get(command) {
                Some(cmd) => {
                    let rules = cmd.rules.clone();
                    match (cmd.function_pointer)(self, &args) {
                        Ok(()) => {}
                        Ok(()) => true,
                        Err(CommandError::InvalidArgs) => {
                            print_error!("Invalid arguments. Usage:\n{}", rules.join("\n"));
                            false
                        }
                        Err(CommandError::Failed(msg)) => {
                            print_error!("Command failed: {}", msg);
                            false
                        }
                    }
                }
                None => {
                    println!("'{}' is an invalid command!", command);
                    self.cmd_help(&args).ok();
                    false
                }
            },
        };
        }
    }

    fn lock_context(&self) -> std::sync::MutexGuard<ClientContext> {
+110 −38
Original line number Diff line number Diff line
@@ -2,12 +2,14 @@ use clap::{value_t, App, Arg};

use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use std::time::Duration;

use dbus::channel::MatchingReceiver;
use dbus::message::MatchRule;
use dbus::nonblock::SyncConnection;
use dbus_crossroads::Crossroads;
use tokio::sync::mpsc;
use tokio::time::timeout;

use crate::bt_adv::AdvSet;
use crate::bt_gatt::GattClientContext;
@@ -140,6 +142,9 @@ pub(crate) struct ClientContext {

    /// The handle of the SDP record for MPS (Multi-Profile Specification).
    mps_sdp_handle: Option<i32>,

    /// The set of client commands that need to wait for callbacks.
    client_commands_with_callbacks: Vec<String>,
}

impl ClientContext {
@@ -148,6 +153,7 @@ impl ClientContext {
        dbus_crossroads: Arc<Mutex<Crossroads>>,
        tx: mpsc::Sender<ForegroundActions>,
        is_restricted: bool,
        client_commands_with_callbacks: Vec<String>,
    ) -> ClientContext {
        // Manager interface is almost always available but adapter interface
        // requires that the specific adapter is enabled.
@@ -187,6 +193,7 @@ impl ClientContext {
            gatt_client_context: GattClientContext::new(),
            socket_test_schedule: None,
            mps_sdp_handle: None,
            client_commands_with_callbacks,
        }
    }

@@ -305,10 +312,24 @@ enum ForegroundActions {
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let matches = App::new("btclient")
        .arg(Arg::with_name("restricted").long("restricted").takes_value(false))
        .arg(Arg::with_name("command").short("c").long("command").takes_value(true))
        .arg(
            Arg::with_name("command")
                .short("c")
                .long("command")
                .takes_value(true)
                .help("Executes a non-interactive command"),
        )
        .arg(
            Arg::with_name("timeout")
                .short("t")
                .long("timeout")
                .takes_value(true)
                .help("Specify a timeout in seconds for a non-interactive command"),
        )
        .get_matches();
    let command = value_t!(matches, "command", String);
    let is_restricted = matches.is_present("restricted");
    let timeout_secs = value_t!(matches, "timeout", u64);

    topstack::get_runtime().block_on(async move {
        // Connect to D-Bus system bus.
@@ -341,12 +362,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
        // Accept foreground actions with mpsc
        let (tx, rx) = mpsc::channel::<ForegroundActions>(10);

        // Include the commands
        // (1) that will be run as non-interactive client commands, and
        // (2) that will need to wait for the callbacks to complete.
        let client_commands_with_callbacks = vec!["media".to_string()];

        // Create the context needed for handling commands
        let context = Arc::new(Mutex::new(ClientContext::new(
            conn.clone(),
            cr.clone(),
            tx.clone(),
            is_restricted,
            client_commands_with_callbacks,
        )));

        // Check if manager interface is valid. We only print some help text before failing on the
@@ -385,36 +412,57 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
            }
        }

        let mut handler = CommandHandler::new(context.clone());

        // Allow command line arguments to be read
        match command {
            Ok(command) => {
                let mut iter = command.split(' ').map(String::from);
                handler.process_cmd_line(
                    &iter.next().unwrap_or(String::from("")),
                    &iter.collect::<Vec<String>>(),
                );
        let handler = CommandHandler::new(context.clone());
        if let Ok(_) = command {
            // Timeout applies only to non-interactive commands.
            if let Ok(timeout_secs) = timeout_secs {
                let timeout_duration = Duration::from_secs(timeout_secs);
                match timeout(
                    timeout_duration,
                    handle_client_command(handler, tx, rx, context, command),
                )
                .await
                {
                    Ok(_) => {
                        return Result::Ok(());
                    }
            _ => {
                start_interactive_shell(handler, tx, rx, context).await?;
                    Err(_) => {
                        return Result::Err("btclient timeout".into());
                    }
                };
            }
        }
        // There are two scenarios in which handle_client_command is run without a timeout.
        // - Interactive commands: none of these commands require a timeout.
        // - Non-interactive commands that have not specified a timeout.
        handle_client_command(handler, tx, rx, context, command).await?;
        return Result::Ok(());
    })
}

async fn start_interactive_shell(
// If btclient runs without command arguments, the interactive shell
// actions are performed.
// If btclient runs with command arguments, the command is executed
// once. There are two cases to exit.
//   Case 1: if the command does not need a callback, e.g., "help",
//           it will exit after running handler.process_cmd_line().
//   Case 2: if the command needs a callback, e.g., "media log",
//           it will exit after the callback has been run in the arm
//           of ForegroundActions::RunCallback(callback).
async fn handle_client_command(
    mut handler: CommandHandler,
    tx: mpsc::Sender<ForegroundActions>,
    mut rx: mpsc::Receiver<ForegroundActions>,
    context: Arc<Mutex<ClientContext>>,
    command: Result<String, clap::Error>,
) -> Result<(), Box<dyn std::error::Error>> {
    let semaphore_fg = Arc::new(tokio::sync::Semaphore::new(1));

    // If there are no command arguments, start the interactive shell.
    if let Err(_) = command {
        let command_rule_list = handler.get_command_rule_list().clone();
        let context_for_closure = context.clone();

    let semaphore_fg = Arc::new(tokio::sync::Semaphore::new(1));

        // Async task to keep reading new lines from user
        let semaphore = semaphore_fg.clone();
        let editor = AsyncEditor::new(command_rule_list, context_for_closure)
@@ -434,6 +482,7 @@ async fn start_interactive_shell(
                let _ = tx.send(ForegroundActions::Readline(result)).await;
            }
        });
    }

    'readline: loop {
        let m = rx.recv().await;
@@ -458,6 +507,11 @@ async fn start_interactive_shell(
            }
            ForegroundActions::RunCallback(callback) => {
                callback(context.clone());

                // Break the loop as a non-interactive command is completed.
                if let Ok(_) = command {
                    break;
                }
            }
            // Once adapter is ready, register callbacks, get the address and mark it as ready
            ForegroundActions::RegisterAdapterCallback(adapter) => {
@@ -619,6 +673,7 @@ async fn start_interactive_shell(
                    .rpc
                    .register_callback(Box::new(MediaCallback::new(
                        media_cb_objpath,
                        context.clone(),
                        dbus_connection.clone(),
                        dbus_crossroads.clone(),
                    )))
@@ -630,6 +685,23 @@ async fn start_interactive_shell(
                context.lock().unwrap().update_bonded_devices();

                print_info!("Adapter {} is ready", adapter_address);

                // Run the command with the command arguments as the client is
                // non-interactive.
                if let Some(command) = command.as_ref().ok() {
                    let mut iter = command.split(' ').map(String::from);
                    let first = iter.next().unwrap_or(String::from(""));
                    if !handler.process_cmd_line(&first, &iter.collect::<Vec<String>>()) {
                        // Break immediately if the command fails to execute.
                        break;
                    }

                    // Break the loop immediately if there is no callback
                    // to wait for.
                    if !context.lock().unwrap().client_commands_with_callbacks.contains(&first) {
                        break;
                    }
                }
            }
            ForegroundActions::Readline(result) => match result {
                Err(rustyline::error::ReadlineError::Interrupted) => {