blob: 1318a46fff9b5b51cae68f2c9fca197d7978014b [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 regex::{Captures, Regex};
use std::fs::File;
use std::io::{Read, Write};
use std::os::unix::io::AsRawFd;
use std::time::{Duration, Instant, SystemTime};
use crate::io::uart::{Uart, UartError};
use crate::util::file;
#[derive(Default)]
pub struct UartConsole {
pub logfile: Option<File>,
pub timeout: Option<Duration>,
pub deadline: Option<Instant>,
pub exit_success: Option<Regex>,
pub exit_failure: Option<Regex>,
pub timestamp: bool,
pub buffer: String,
pub newline: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExitStatus {
None,
CtrlC,
Timeout,
ExitSuccess,
ExitFailure,
}
// Creates a vtable for implementors of Read and AsRawFd traits.
pub trait ReadAsRawFd: Read + AsRawFd {}
impl<T: Read + AsRawFd> ReadAsRawFd for T {}
impl UartConsole {
const CTRL_C: u8 = 3;
const BUFFER_LEN: usize = 4096;
// Runs an interactive console until CTRL_C is received.
pub fn interact(
&mut self,
uart: &dyn Uart,
mut stdin: Option<&mut dyn ReadAsRawFd>,
mut stdout: Option<&mut dyn Write>,
) -> Result<ExitStatus> {
if let Some(timeout) = &self.timeout {
self.deadline = Some(Instant::now() + *timeout);
}
loop {
match self.interact_once(uart, &mut stdin, &mut stdout)? {
ExitStatus::None => {}
status => return Ok(status),
}
}
}
// 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));
while self.buffer.len() > UartConsole::BUFFER_LEN {
self.buffer.remove(0);
}
}
// Read from the uart and process the data read.
fn uart_read(
&mut self,
uart: &dyn Uart,
timeout: Duration,
stdout: &mut Option<&mut dyn Write>,
) -> Result<()> {
let mut buf = [0u8; 256];
let len = uart.read_timeout(&mut buf, timeout)?;
if len == 0 {
return Ok(());
}
for i in 0..len {
if self.timestamp && self.newline {
let t = humantime::format_rfc3339_millis(SystemTime::now());
stdout
.as_mut()
.map_or(Ok(()), |out| out.write_fmt(format_args!("[{}]", t)))?;
self.newline = false;
}
self.newline = buf[i] == b'\n';
stdout.as_mut().map_or(Ok(()), |out| {
out.write_all(if self.newline {
b"\r\n"
} else {
&buf[i..i + 1]
})
})?;
}
stdout.as_mut().map_or(Ok(()), |out| out.flush())?;
// If we're logging, save it to the logfile.
self.logfile
.as_mut()
.map_or(Ok(()), |f| f.write_all(&buf[..len]))?;
self.append_buffer(&buf[..len]);
Ok(())
}
fn process_input(
&self,
uart: &dyn Uart,
stdin: &mut Option<&mut (dyn ReadAsRawFd)>,
) -> Result<ExitStatus> {
if let Some(ref mut input) = stdin.as_mut() {
if file::wait_fd_read_timeout(input.as_raw_fd(), Duration::from_millis(0)).is_ok() {
let mut buf = [0u8; 256];
let len = input.read(&mut buf)?;
if len == 1 && buf[0] == UartConsole::CTRL_C {
return Ok(ExitStatus::CtrlC);
}
if len > 0 {
uart.write(&buf[..len])?;
}
}
}
Ok(ExitStatus::None)
}
pub fn interact_once(
&mut self,
uart: &dyn Uart,
stdin: &mut Option<&mut (dyn ReadAsRawFd)>,
stdout: &mut Option<&mut dyn Write>,
) -> Result<ExitStatus> {
if let Some(deadline) = &self.deadline {
if Instant::now() > *deadline {
return Ok(ExitStatus::Timeout);
}
}
// This _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.
self.uart_read(uart, Duration::from_millis(10), stdout)?;
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);
}
self.process_input(uart, stdin)
}
pub fn captures(&self, status: ExitStatus) -> Option<Captures> {
match status {
ExitStatus::ExitSuccess => self
.exit_success
.as_ref()
.and_then(|rx| rx.captures(&self.buffer)),
ExitStatus::ExitFailure => self
.exit_failure
.as_ref()
.and_then(|rx| rx.captures(&self.buffer)),
_ => None,
}
}
pub fn wait_for(uart: &dyn Uart, rx: &str, timeout: Duration) -> Result<String> {
let mut console = UartConsole {
timeout: Some(timeout),
exit_success: Some(Regex::new(rx)?),
..Default::default()
};
let mut stdout = std::io::stdout();
let result = console.interact(uart, None, Some(&mut stdout))?;
match result {
ExitStatus::ExitSuccess => {
let cap = console.captures(ExitStatus::ExitSuccess).expect("capture");
let s = cap.get(0).expect("capture group").as_str().to_owned();
Ok(s)
}
ExitStatus::Timeout => Err(UartError::GenericError("Timed Out".into()).into()),
_ => Err(anyhow!("Impossible result: {:?}", result)),
}
}
}