[opentitantool] Add interactive console.

Add an interactive `console` command.  The console command includes
options which allow it to be useful by automation as well:

`--logfile=<file>` logs the console output to a file.
`--timeout=<seconds>` causes the console to exit after `seconds`.
`--exit_success=<regex>` causes the console to exit successfully after
observing `regex`.
`--exit_success=<regex>` causes the console to exit with failure after
observing `regex`.

Signed-off-by: Chris Frantz <cfrantz@google.com>
diff --git a/sw/host/opentitantool/src/command/console.rs b/sw/host/opentitantool/src/command/console.rs
new file mode 100644
index 0000000..037dfbb
--- /dev/null
+++ b/sw/host/opentitantool/src/command/console.rs
@@ -0,0 +1,232 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use anyhow::{anyhow, Result};
+use erased_serde::Serialize;
+use nix::unistd::isatty;
+use raw_tty::TtyModeGuard;
+use regex::Regex;
+use std::fs::File;
+use std::io;
+use std::io::{ErrorKind, Read, Write};
+use std::os::unix::io::AsRawFd;
+use std::time::{Duration, Instant};
+use structopt::StructOpt;
+
+use opentitanlib::app::command::CommandDispatch;
+use opentitanlib::io::uart::Uart;
+use opentitanlib::transport::{Capability, Transport};
+use opentitanlib::util::file;
+
+#[derive(Debug, StructOpt)]
+pub struct Console {
+    #[structopt(short, long, help = "Do not print console start end exit messages.")]
+    quiet: bool,
+
+    #[structopt(short, long, help = "Log console output to a file")]
+    logfile: Option<String>,
+
+    #[structopt(short, long, help = "Exit after a timeout in seconds.")]
+    timeout: Option<u64>,
+
+    #[structopt(long, help = "Exit with success if the specified regex is matched.")]
+    exit_success: Option<String>,
+
+    #[structopt(long, help = "Exit with failure if the specified regex is matched.")]
+    exit_failure: Option<String>,
+}
+
+impl CommandDispatch for Console {
+    fn run(&self, transport: &mut dyn Transport) -> Result<Option<Box<dyn Serialize>>> {
+        // We need the UART for the console command to operate.
+        transport.capabilities().request(Capability::UART).ok()?;
+        let mut stdout = std::io::stdout();
+        let mut stdin = std::io::stdin();
+
+        // Set up resources specified by the command line parameters.
+        let mut console = InnerConsole {
+            logfile: self.logfile.as_ref().map(File::create).transpose()?,
+            deadline: self.timeout.map(|t| Instant::now() + Duration::new(t, 0)),
+            exit_success: self
+                .exit_success
+                .as_ref()
+                .map(|s| Regex::new(s.as_str()))
+                .transpose()?,
+            exit_failure: self
+                .exit_failure
+                .as_ref()
+                .map(|s| Regex::new(s.as_str()))
+                .transpose()?,
+            ..Default::default()
+        };
+
+        if !self.quiet {
+            println!("Starting interactive console");
+            println!("[CTRL+C] to exit.\n");
+        }
+        {
+            // Put the terminal into raw mode.  The tty guard will restore the
+            // console settings when it goes out of scope.
+            let _stdin_guard = if isatty(stdin.as_raw_fd())? {
+                let mut guard = TtyModeGuard::new(stdin.as_raw_fd())?;
+                guard.set_raw_mode()?;
+                Some(guard)
+            } else {
+                None
+            };
+            let _stdout_guard = if isatty(stdout.as_raw_fd())? {
+                let mut guard = TtyModeGuard::new(stdout.as_raw_fd())?;
+                guard.set_raw_mode()?;
+                Some(guard)
+            } else {
+                None
+            };
+            console.interact(transport, &mut stdin, &mut stdout)?;
+        }
+        if !self.quiet {
+            println!("\n\nExiting interactive console.");
+        }
+        Ok(None)
+    }
+}
+
+#[derive(Default)]
+struct InnerConsole {
+    logfile: Option<File>,
+    deadline: Option<Instant>,
+    exit_success: Option<Regex>,
+    exit_failure: Option<Regex>,
+    buffer: String,
+}
+
+enum ExitStatus {
+    None,
+    ExitSuccess,
+    ExitFailure,
+}
+
+impl InnerConsole {
+    const CTRL_C: u8 = 3;
+    const BUFFER_LEN: usize = 1024;
+
+    // Runs an interactive console until CTRL_C is received.
+    fn interact(
+        &mut self,
+        transport: &mut dyn Transport,
+        stdin: &mut (impl Read + AsRawFd),
+        stdout: &mut impl Write,
+    ) -> Result<()> {
+        let mut uart = transport.uart()?;
+        let mut buf = [0u8; 256];
+
+        loop {
+            if let Some(deadline) = self.deadline {
+                if Instant::now() > deadline {
+                    // If we have an exit success condition, then a timeout
+                    // should be an error.
+                    if self.exit_success.is_some() {
+                        return Err(anyhow!("Console timeout exceeded"));
+                    } else {
+                        break;
+                    }
+                }
+            }
+
+            // This loop _should_ really use unix `poll` in the conventional way
+            // to learn when the console or uart file descriptors become ready,
+            // but some UART backends will bury their implementation in libusb
+            // and make discovering the file descriptor difficult or impossible.
+            //
+            // As a pragmatic implementation detail, we wait for the UART
+            // for a short period of time and then service the console.
+            //
+            // TODO: as we write more backends, re-evaluate whether there is a
+            // better way to approach waiting on the UART and keyboard.
+
+            // Check for input on the uart.
+            match self.uart_read(&mut *uart, Duration::from_millis(10), stdout)? {
+                ExitStatus::None => {}
+                ExitStatus::ExitSuccess => {
+                    break;
+                }
+                ExitStatus::ExitFailure => {
+                    return Err(anyhow!("Matched exit_failure expression"));
+                }
+            };
+
+            // Wait for input from the user.
+            if file::wait_read_timeout(&*stdin, Duration::from_millis(0)).is_ok() {
+                let len = stdin.read(&mut buf)?;
+                if len == 1 && buf[0] == InnerConsole::CTRL_C {
+                    break;
+                }
+                uart.write(&buf[..len])?;
+            }
+        }
+        Ok(())
+    }
+
+    // Maintain a buffer for the exit regexes to match against.
+    fn append_buffer(&mut self, data: &[u8]) {
+        self.buffer.push_str(&String::from_utf8_lossy(&data[..]));
+        if self.buffer.len() > InnerConsole::BUFFER_LEN {
+            let (_, end) = self
+                .buffer
+                .split_at(self.buffer.len() - InnerConsole::BUFFER_LEN);
+            self.buffer = end.to_string();
+        }
+    }
+
+    // Read from the uart and process the data read.
+    fn uart_read(
+        &mut self,
+        uart: &mut dyn Uart,
+        timeout: Duration,
+        stdout: &mut impl Write,
+    ) -> Result<ExitStatus> {
+        let mut buf = [0u8; 256];
+        match uart.read_timeout(&mut buf, timeout) {
+            Ok(len) => {
+                stdout.write_all(&buf[..len])?;
+                stdout.flush()?;
+
+                // If we're logging, save it to the logfile.
+                if let Some(logfile) = &mut self.logfile {
+                    logfile.write_all(&buf[..len])?;
+                }
+
+                // If we have exit condition regexes check them.
+                self.append_buffer(&buf[..len]);
+                if self
+                    .exit_success
+                    .as_ref()
+                    .map(|rx| rx.is_match(&self.buffer))
+                    == Some(true)
+                {
+                    return Ok(ExitStatus::ExitSuccess);
+                }
+                if self
+                    .exit_failure
+                    .as_ref()
+                    .map(|rx| rx.is_match(&self.buffer))
+                    == Some(true)
+                {
+                    return Ok(ExitStatus::ExitFailure);
+                }
+            }
+            Err(e) => {
+                // If we got a timeout from the uart, ignore it.
+                // Return all other errors.
+                if let Some(ioerr) = e.downcast_ref::<io::Error>() {
+                    if ioerr.kind() != ErrorKind::TimedOut {
+                        return Err(e);
+                    }
+                } else {
+                    return Err(e);
+                }
+            }
+        }
+        Ok(ExitStatus::None)
+    }
+}
diff --git a/sw/host/opentitantool/src/command/mod.rs b/sw/host/opentitantool/src/command/mod.rs
index 950ba35..42c97da 100644
--- a/sw/host/opentitantool/src/command/mod.rs
+++ b/sw/host/opentitantool/src/command/mod.rs
@@ -2,4 +2,5 @@
 // Licensed under the Apache License, Version 2.0, see LICENSE for details.
 // SPDX-License-Identifier: Apache-2.0
 
+pub mod console;
 pub mod hello;
diff --git a/sw/host/opentitantool/src/main.rs b/sw/host/opentitantool/src/main.rs
index 3dbbb2b..fb458a1 100644
--- a/sw/host/opentitantool/src/main.rs
+++ b/sw/host/opentitantool/src/main.rs
@@ -12,6 +12,9 @@
 
 #[derive(Debug, StructOpt, CommandDispatch)]
 enum RootCommandHierarchy {
+    // Not flattened because `Console` is a leaf command.
+    Console(command::console::Console),
+
     // Flattened because `Greetings` is a subcommand hierarchy.
     #[structopt(flatten)]
     Greetings(command::hello::Greetings),