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