[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),