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)
+}