blob: 037dfbbbbc9e0a6a1c87ebb9f1b256f06a809790 [file] [log] [blame]
// 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)
}
}