ril-ctl tool
Change-Id: I592bb8311776c78171d5c323017be4066800dc61
diff --git a/bin/telephony/tools/ril-ctl/BUILD.gn b/bin/telephony/tools/ril-ctl/BUILD.gn
index 6f256d6..0cf623a 100644
--- a/bin/telephony/tools/ril-ctl/BUILD.gn
+++ b/bin/telephony/tools/ril-ctl/BUILD.gn
@@ -16,7 +16,9 @@
"//garnet/public/rust/fuchsia-app",
"//garnet/public/rust/fuchsia-async",
"//garnet/public/rust/fuchsia-zircon",
+ "//third_party/rust-crates/rustc_deps:pin-utils",
"//third_party/rust-crates/rustc_deps:failure",
+ "//third_party/rust-crates/rustc_deps:parking_lot",
"//third_party/rust-crates/rustc_deps:futures-preview",
"//third_party/rust-crates/rustc_deps:log",
"//third_party/rust-mirrors/rustyline",
@@ -30,10 +32,10 @@
binary = "rust_crates/ril_ctl"
- meta = [
- {
- path = rebase_path("meta/ril-ctl.cmx")
- dest = "ril-ctl.cmx"
- },
- ]
+ meta = [
+ {
+ path = rebase_path("meta/ril-ctl.cmx")
+ dest = "ril-ctl.cmx"
+ },
+ ]
}
diff --git a/bin/telephony/tools/ril-ctl/src/commands.rs b/bin/telephony/tools/ril-ctl/src/commands.rs
index fc17716..785c5d9 100644
--- a/bin/telephony/tools/ril-ctl/src/commands.rs
+++ b/bin/telephony/tools/ril-ctl/src/commands.rs
@@ -2,41 +2,65 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-use rustyline::completion::Completer;
-use rustyline::error::ReadlineError;
-use std::fmt;
-use std::str::FromStr;
+use {
+ rustyline::{
+ completion::Completer, error::ReadlineError, highlight::Highlighter, hint::Hinter, Helper,
+ },
+ std::{
+ borrow::Cow::{self, Borrowed, Owned},
+ fmt,
+ str::FromStr,
+ },
+};
-macro_rules! gen_completer {
+/// Macro to generate a command enum and its impl.
+macro_rules! gen_commands {
($name:ident {
- $($variant:ident = ($val:expr, $help:expr)),*,
+ $($variant:ident = ($val:expr, [$($arg:expr),*], $help:expr)),*,
}) => {
+ /// Enum of all possible commands
#[derive(PartialEq)]
pub enum $name {
- Nothing,
$($variant),*
}
impl $name {
+ /// Returns a list of the string representations of all variants
pub fn variants() -> Vec<String> {
let mut variants = Vec::new();
$(variants.push($val.to_string());)*
variants
}
- pub fn help_msg() -> String {
- let mut msg = String::new();
- $(
- msg.push_str(format!("{} -- {}\n", $val, $help).as_str());
- )*
- msg
+ pub fn arguments(&self) -> &'static str {
+ match self {
+ $(
+ $name::$variant => concat!($("<", $arg, "> ",)*)
+ ),*
+ }
}
+
+ /// Help string for a given varient. The format is "command <arg>.. -- help message"
+ pub fn cmd_help(&self) -> &'static str {
+ match self {
+ $(
+ $name::$variant => concat!($val, " ", $("<", $arg, "> ",)* "-- ", $help)
+ ),*
+ }
+ }
+
+ /// Multiline help string for `$name` including usage of all variants.
+ pub fn help_msg() -> &'static str {
+ concat!("Commands:\n", $(
+ "\t", $val, " ", $("<", $arg, "> ",)* "-- ", $help, "\n"
+ ),*)
+ }
+
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
- $name::Nothing => write!(f, ""),
$($name::$variant => write!(f, $val)),* ,
}
}
@@ -48,29 +72,37 @@
fn from_str(s: &str) -> Result<$name, ()> {
match s {
$($val => Ok($name::$variant)),* ,
- "" => Ok($name::Nothing),
_ => Err(()),
}
}
}
+
}
}
-gen_completer! {
+// `Cmd` is the declarative specification of all commands that bt-cli accepts.
+gen_commands! {
Cmd {
- GetVersion = ("version", "get the version of the modem"),
+ Imei = ("imei", [], "IMEI (International Mobile Equipment Identity) for the radio"),
+ PowerStatus = ("power-status", [], "Radio Power status"),
+ Help = ("help", [], "This message"),
+ Exit = ("exit", [], "Close REPL"),
+ Quit = ("quit", [], "Close REPL"),
}
}
-pub struct CmdCompleter;
+/// CmdHelper provides completion, hints, and highlighting for bt-cli
+pub struct CmdHelper;
-impl CmdCompleter {
- pub fn new() -> CmdCompleter {
- CmdCompleter {}
+impl CmdHelper {
+ pub fn new() -> CmdHelper {
+ CmdHelper {}
}
}
-impl Completer for CmdCompleter {
+impl Completer for CmdHelper {
+ type Candidate = String;
+
fn complete(&self, line: &str, _pos: usize) -> Result<(usize, Vec<String>), ReadlineError> {
let mut variants = Vec::new();
for variant in Cmd::variants() {
@@ -81,3 +113,40 @@
Ok((0, variants))
}
}
+
+impl Hinter for CmdHelper {
+ /// CmdHelper provides hints for commands with arguments
+ fn hint(&self, line: &str, _pos: usize) -> Option<String> {
+ let needs_space = !line.ends_with(" ");
+ line.trim()
+ .parse::<Cmd>()
+ .map(|cmd| {
+ format!(
+ "{}{}",
+ if needs_space { " " } else { "" },
+ cmd.arguments().to_string(),
+ )
+ })
+ .ok()
+ }
+}
+
+impl Highlighter for CmdHelper {
+ /// CmdHelper provides highlights for commands with hints
+ fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
+ if hint.trim().is_empty() {
+ Borrowed(hint)
+ } else {
+ Owned(format!("\x1b[90m{}\x1b[0m", hint))
+ }
+ }
+}
+
+/// CmdHelper can be used as an `Editor` helper for entering input commands
+impl Helper for CmdHelper {}
+
+/// Represents either continuation or breaking out of a read-evaluate-print loop.
+pub enum ReplControl {
+ Break,
+ Continue,
+}
diff --git a/bin/telephony/tools/ril-ctl/src/main.rs b/bin/telephony/tools/ril-ctl/src/main.rs
index 37f5c75..ef6015b 100644
--- a/bin/telephony/tools/ril-ctl/src/main.rs
+++ b/bin/telephony/tools/ril-ctl/src/main.rs
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-//! ril-ctl is used for interacting with devices that expose a QMI RIL over
-//! FIDL on Fuchsia.
+//! ril-ctl is used for interacting with devices that expose the standard
+//! Fuchsia RIL (FRIL)
//!
//! Ex: run ril-ctl -d /dev/class/ril-transport/000
//!
@@ -11,22 +11,65 @@
//! modem service is planned. A REPL is also planned as the FIDL interfaces
//! evolve.
-#![feature(async_await, await_macro, futures_api)]
-//#![deny(warnings)]
+#![feature(async_await, await_macro, futures_api, pin)]
+#![deny(warnings)]
-use failure::{format_err, Error, ResultExt};
-use fidl::endpoints::create_proxy;
-use fidl_fuchsia_telephony_ril::RadioInterfaceLayerMarker;
-use fuchsia_app::client::Launcher;
-use fuchsia_async as fasync;
-use qmi;
-use std::env;
-use std::fs::File;
+use {
+ crate::commands::{Cmd, ReplControl},
+ failure::{Error, ResultExt},
+ fidl_fuchsia_telephony_ril::{RadioInterfaceLayerMarker, RadioInterfaceLayerProxy, RadioPowerState},
+ fuchsia_app::client::Launcher,
+ fuchsia_async::{self as fasync, futures::select},
+ futures::TryFutureExt,
+ pin_utils::pin_mut,
+ qmi,
+ std::{env, fs::File},
+};
+
+mod commands;
+mod repl;
+
+static PROMPT: &str = "\x1b[35mril>\x1b[0m ";
+
+async fn get_imei<'a>(
+ _args: &'a [&'a str], ril_modem: &'a RadioInterfaceLayerProxy,
+) -> Result<String, Error> {
+ let resp = await!(ril_modem.get_device_identity())?;
+ Ok(resp)
+}
+
+async fn get_power<'a>(
+ _args: &'a [&'a str], ril_modem: &'a RadioInterfaceLayerProxy,
+) -> Result<String, Error> {
+ match await!(ril_modem.radio_power_status())? {
+ RadioPowerState::On => Ok(String::from("radio on")),
+ RadioPowerState::Off => Ok(String::from("radio off")),
+ }
+}
+
+async fn handle_cmd(
+ ril_modem: &RadioInterfaceLayerProxy, line: String,
+) -> Result<ReplControl, Error> {
+ let components: Vec<_> = line.trim().split_whitespace().collect();
+ if let Some((raw_cmd, args)) = components.split_first() {
+ let cmd = raw_cmd.parse();
+ let res = match cmd {
+ Ok(Cmd::PowerStatus) => await!(get_power(args, &ril_modem)),
+ Ok(Cmd::Imei) => await!(get_imei(args, &ril_modem)),
+ Ok(Cmd::Help) => Ok(Cmd::help_msg().to_string()),
+ Ok(Cmd::Exit) | Ok(Cmd::Quit) => return Ok(ReplControl::Break),
+ Err(_) => Ok(format!("\"{}\" is not a valid command", raw_cmd)),
+ }?;
+ if res != "" {
+ println!("{}", res);
+ }
+ }
+
+ Ok(ReplControl::Continue)
+}
pub fn main() -> Result<(), Error> {
let mut exec = fasync::Executor::new().context("error creating event loop")?;
-// let (_client_proxy, client_server) = create_proxy()?;
-
let args: Vec<String> = env::args().collect();
// TODO more advanced arg parsing
@@ -43,18 +86,19 @@
let ril_modem = app.connect_to_service(RadioInterfaceLayerMarker)?;
let path = &args[2];
-
let file = File::open(&path)?;
let chan = qmi::connect_transport_device(&file)?;
let client_fut = async {
- let connected_transport = await!(ril_modem.connect_transport(chan))?;
- if connected_transport {
- let client_res = await!(ril_modem.get_device_identity())?;
- eprintln!("resp: {:?}", client_res);
- return Ok(());
+ await!(ril_modem.connect_transport(chan))?;
+ let repl =
+ repl::run(ril_modem).unwrap_or_else(|e| eprintln!("REPL failed unexpectedly {:?}", e));
+ pin_mut!(repl);
+ select! {
+ repl => Ok(()),
+ // TODO(bwb): events loop future
}
- Err(format_err!("Failed to request modem or client"))
};
+
exec.run_singlethreaded(client_fut)
}
diff --git a/bin/telephony/tools/ril-ctl/src/repl.rs b/bin/telephony/tools/ril-ctl/src/repl.rs
new file mode 100644
index 0000000..76e3366
--- /dev/null
+++ b/bin/telephony/tools/ril-ctl/src/repl.rs
@@ -0,0 +1,98 @@
+// 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 {
+ crate::{
+ commands::{CmdHelper, ReplControl},
+ // TODO finish repling!
+ PROMPT,
+ },
+ failure::{Error, ResultExt},
+ fidl_fuchsia_telephony_ril::RadioInterfaceLayerProxy,
+ fuchsia_async as fasync,
+ futures::{
+ channel::mpsc::{channel, SendError},
+ Sink, SinkExt, Stream, StreamExt,
+ },
+ rustyline::{error::ReadlineError, CompletionType, Config, EditMode, Editor},
+ std::thread,
+};
+
+pub async fn run(ril_modem: RadioInterfaceLayerProxy) -> Result<(), Error> {
+ // `cmd_stream` blocks on input in a seperate thread and passes commands and acks back to
+ // the main thread via async channels.
+ let (mut commands, mut acks) = cmd_stream();
+ loop {
+ if let Some(cmd) = await!(commands.next()) {
+ match await!(crate::handle_cmd(&ril_modem, cmd)) {
+ Ok(ReplControl::Continue) => {}
+ Ok(ReplControl::Break) => {
+ break;
+ }
+ Err(e) => {
+ println!("Error handling command: {}", e);
+ }
+ }
+ } else {
+ break;
+ }
+ await!(acks.send(()))?;
+ }
+ Ok(())
+}
+
+/// 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() -> (
+ impl Stream<Item = String>,
+ impl Sink<SinkItem = (), SinkError = SendError>,
+) {
+ // Editor thread and command processing thread must be syncronized so that output
+ // is printed in the correct order.
+ let (mut cmd_sender, cmd_receiver) = channel(512);
+ let (ack_sender, mut ack_receiver) = channel(512);
+
+ thread::spawn(move || -> Result<(), Error> {
+ let mut exec = fasync::Executor::new().context("error creating readline event loop")?;
+
+ 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();
+ 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
+ await!(ack_receiver.next());
+ }
+ };
+ exec.run_singlethreaded(fut)
+ });
+ (cmd_receiver, ack_sender)
+}