blob: b6c568a4876d8e87ea782300b283d32ea05c0be3 [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
mod base;
mod dev;
mod dispatcher;
mod history;
mod legacy;
mod util;
use std::env::var;
use std::io::{self, stdin, stdout, Write};
use std::mem;
use std::path::PathBuf;
use std::ptr::null_mut;
use std::sync::atomic::{AtomicI32, Ordering};
use std::thread::sleep;
use std::time::Duration;
use libc::{
c_int, fork, kill, pid_t, sigaction, waitpid, SA_RESTART, SIGHUP, SIGINT, SIGKILL, SIG_DFL,
WIFSTOPPED,
};
use sys_util::error;
use termion::event::Key;
use termion::input::TermRead;
use termion::raw::IntoRawMode;
use termion::terminal_size;
use crate::dispatcher::{CompletionResult, Dispatcher};
use crate::history::History;
const HISTORY_FILENAME: &str = ".crosh_history";
fn usage(error: bool) {
let usage_msg = r#"Usage: crosh [options] [-- [args]]
Options:
--dev Force dev mode.
--removable Force removable (boot from USB/SD/etc...) mode.
--usb Same as above.
--help, -h Show this help string.
-- <all args after this are a command to run>
Execute a single command and exit.
"#;
if error {
eprintln!("{}", usage_msg)
} else {
println!("{}", usage_msg);
}
}
fn intro() {
println!(
r#"Welcome to crosh, the Chrome OS developer shell.
If you got here by mistake, don't panic! Just close this tab and carry on.
Type 'help' for a list of commands.
If you want to customize the look/behavior, you can use the options page.
Load it by using the Ctrl-Shift-P keyboard shortcut.
"#
);
}
fn new_prompt() {
print!("\x1b[1;33mcrosh>\x1b[0m ");
let _ = stdout().flush();
}
static COMMAND_RUNNING_PID: AtomicI32 = AtomicI32::new(-1);
// Print a slice of strings in columns of equal width. This is used to display command completion
// results when there is more than one match.
fn write_in_columns(
w: &mut dyn Write,
list: &[String],
width_opt: Option<usize>,
) -> Result<(), io::Error> {
if list.is_empty() {
return writeln!(w, "\r");
}
let mut max_len: usize = 0;
for entry in list {
if entry.len() > max_len {
max_len = entry.len()
}
}
if let Some(width) = width_opt {
if max_len < width {
let columns = (width + 2) / (max_len + 2);
let mut col_x = 0;
for entry in &list[..list.len() - 1] {
col_x += 1;
if col_x == columns {
writeln!(w, "{}\r", entry)?;
col_x = 0
} else {
write!(w, "{}{} ", entry, " ".repeat(max_len - entry.len()))?;
}
}
return writeln!(w, "{}\r", list[list.len() - 1]);
}
}
for entry in &list[..list.len() - 1] {
write!(w, "{} ", entry)?;
}
writeln!(w, "{}\r", list[list.len() - 1])
}
// Forks off a child process which executes the command handler and waits for it to return.
// COMMAND_RUNNING_PID is updated to have the child process id so SIGINT can be sent.
fn handle_cmd(dispatcher: &Dispatcher, args: Vec<String>) -> Result<(), ()> {
let pid: pid_t;
unsafe {
pid = fork();
}
if pid < 0 {
return Err(());
}
// Handle the child thread case.
if pid == 0 {
clear_signal_handlers();
dispatch_cmd(dispatcher, args);
}
COMMAND_RUNNING_PID.store(pid as i32, Ordering::Release);
let mut status: c_int = 1;
let code: pid_t;
unsafe {
code = waitpid(pid, &mut status, 0);
// This should only happen if the child process is ptraced.
if WIFSTOPPED(status) && kill(-pid, SIGKILL) != 0 {
eprintln!("kill failed.");
}
}
COMMAND_RUNNING_PID.store(-1, Ordering::Release);
if code != pid {
eprintln!("waitpid failed.");
return Err(());
}
match status {
0 => Ok(()),
_ => Err(()),
}
}
// Execute the specific command. This should be called in a child process.
fn dispatch_cmd(dispatcher: &Dispatcher, args: Vec<String>) {
std::process::exit(match args.get(0).map(|s| &**s) {
Some("help") => {
let mut ret: i32 = 0;
if args.len() == 2 {
let list: [&str; 1] = [&args[1]];
if dispatcher.help_string(&mut stdout(), Some(&list)).is_err() {
eprintln!("help: unknown command '{}'", &args[1]);
ret = 1;
}
} else {
if args.len() > 1 {
eprintln!("ERROR: too many arguments");
ret = 1;
}
let list = ["exit", "help", "help_advanced", "ping", "top"];
if dispatcher.help_string(&mut stdout(), Some(&list)).is_err() {
panic!();
}
}
ret
}
Some("help_advanced") => {
if args.len() > 1 {
eprintln!("ERROR: too many arguments");
1
} else {
if dispatcher.help_string(&mut stdout(), None).is_err() {
panic!();
}
0
}
}
_ => match dispatcher.handle_command(args) {
Ok(_) => 0,
Err(e) => {
eprintln!("ERROR: {}", e);
1
}
},
});
}
// Handle Ctrl-c/SIGINT by sending a SIGINT to any running child process.
unsafe extern "C" fn sigint_handler() {
let mut command_pid: i32 = COMMAND_RUNNING_PID.load(Ordering::Acquire);
if command_pid >= 0 {
let _ = stdout().flush();
if kill(command_pid, SIGINT) != 0 {
eprintln!("kill failed.");
} else {
command_pid = -1;
}
} else {
println!();
new_prompt();
}
COMMAND_RUNNING_PID.store(command_pid, Ordering::Release);
}
// Ignore SIGHUP.
extern "C" fn sighup_handler() {}
fn register_signal_handlers() {
unsafe {
let mut sigact: sigaction = mem::zeroed();
sigact.sa_flags = SA_RESTART;
sigact.sa_sigaction = sigint_handler as *const () as usize;
let ret = sigaction(SIGINT, &sigact, null_mut());
if ret < 0 {
eprintln!("sigaction failed for SIGINT.");
}
sigact = mem::zeroed();
sigact.sa_flags = SA_RESTART;
sigact.sa_sigaction = sighup_handler as *const () as usize;
let ret = sigaction(SIGHUP, &sigact, null_mut());
if ret < 0 {
eprintln!("sigaction failed for SIGHUP.");
}
}
}
fn clear_signal_handlers() {
unsafe {
let mut sigact: sigaction = mem::zeroed();
sigact.sa_sigaction = SIG_DFL;
let ret = sigaction(SIGINT, &sigact, null_mut());
if ret < 0 {
eprintln!("sigaction failed for SIGINT.");
}
sigact = mem::zeroed();
sigact.sa_sigaction = SIG_DFL;
let ret = sigaction(SIGHUP, &sigact, null_mut());
if ret < 0 {
eprintln!("sigaction failed for SIGHUP.");
}
// Leave the handler for SIGTTIN.
}
}
fn parse_command(command: &str) -> Vec<String> {
// TODO(crbug.com/1002931) handle quotes
command.split_whitespace().map(|s| s.to_string()).collect()
}
// Handle user input to obtain a signal command. This includes handling cases like history lookups
// and command completion.
fn next_command(dispatcher: &Dispatcher, history: &mut History) -> String {
let mut stdin_keys = stdin().keys();
new_prompt();
let mut command = String::new();
// Reprint the command.
fn refresh_command(command: &str) {
print!("\r\x1b[K");
new_prompt();
print!("{}", command);
};
// Use the function scope to return stdout to normal mode before executing a command.
let mut stdout = stdout().into_raw_mode().unwrap();
loop {
if let Some(Ok(key)) = stdin_keys.next() {
match key {
Key::Char('\t') => {
let tokens: Vec<String> = parse_command(&command);
match dispatcher.complete_command(tokens) {
CompletionResult::NoMatches => {
println!("\r");
new_prompt();
print!("{}", command);
}
CompletionResult::SingleDiff(diff) => {
command.push_str(&diff);
print!("{}", diff);
}
CompletionResult::WholeTokenList(matches) => {
println!("\r");
write_in_columns(
&mut stdout,
&matches,
match terminal_size() {
Ok((w, _height)) => Some(w as usize),
_ => None,
},
)
.unwrap_or(());
new_prompt();
print!("{}", command);
}
}
}
Key::Char('\n') => {
print!("\n\r");
if !command.is_empty() {
break;
} else {
new_prompt();
}
}
Key::Char(value) => {
print!("{}", value);
command.push(value);
}
Key::Ctrl('h') | Key::Backspace => {
if !command.is_empty() {
command.pop();
// Move cursor back one and clear rest of line.
print!("\x1b[D\x1b[K");
}
}
Key::Ctrl('d') => {
if command.is_empty() {
command = "exit".to_owned();
println!("\n\r");
break;
}
}
Key::Up => {
if let Some(prev) = history.previous(&command) {
command = prev.to_string();
refresh_command(prev);
}
}
Key::Down => {
if let Some(next) = history.next() {
command = next.to_string();
refresh_command(next);
}
}
_ => {
// Ignore
}
}
let _ = stdout.flush();
} else {
// If no input was returned, don't busy wait because stdin_keys.next() doesn't block.
sleep(Duration::from_millis(25));
}
}
let _ = stdout.flush();
history.new_entry(command.to_string());
command
}
// Loop for getting each command from the user and dispatching it to the handler.
fn input_loop(dispatcher: Dispatcher) {
let mut history = History::new();
let history_path = match var("HOME") {
Ok(h) => {
if h.is_empty() {
None
} else {
Some(PathBuf::from(h).join(HISTORY_FILENAME))
}
}
_ => None,
};
if let Some(h) = history_path.as_ref() {
if let Err(e) = history.load_from_file(h.as_path()) {
eprintln!("Error loading history: {}", e);
}
}
loop {
let line = next_command(&dispatcher, &mut history);
let command = line.trim();
if command == "exit" || command == "quit" {
break;
} else if !command.is_empty() {
let _ = handle_cmd(&dispatcher, parse_command(&command));
if let Some(h) = history_path.as_ref() {
if let Err(e) = history.persist_to_file(h.as_path()) {
eprintln!("Error persisting history: {}", e);
}
}
}
}
}
fn setup_dispatcher() -> Dispatcher {
let mut dispatcher = Dispatcher::new();
if util::dev_commands_included() {
legacy::register_dev_mode_commands(&mut dispatcher);
dev::register(&mut dispatcher);
}
if util::usb_commands_included() {
legacy::register_removable_commands(&mut dispatcher);
}
base::register(&mut dispatcher);
legacy::register(&mut dispatcher);
if let Err(err) = dispatcher.validate() {
panic!("FATAL: {}", err);
}
dispatcher
}
fn main() -> Result<(), ()> {
let mut args = std::env::args();
if args.next().is_none() {
error!("expected executable name.");
return Err(());
}
let mut args_as_command = false;
let mut command_args: Vec<String> = Vec::new();
util::set_dev_commands_included(util::is_dev_mode().unwrap_or_else(|_| {
eprintln!("Could not locate 'crossystem'; assuming devmode is off.");
false
}));
util::set_usb_commands_included(util::is_removable().unwrap_or_else(|_| {
eprintln!("Could not query filesystem; assuming not removable.");
false
}));
for arg in args {
if args_as_command {
command_args.push(arg)
} else {
match arg.as_ref() {
"--help" | "-h" => {
usage(false);
return Ok(());
}
"--dev" => {
util::set_dev_commands_included(true);
}
"--removable" | "--usb" => {
util::set_usb_commands_included(true);
}
"--" => {
args_as_command = true;
}
_ => {
usage(true);
return Err(());
}
}
}
}
let dispatcher = setup_dispatcher();
if args_as_command {
dispatch_cmd(
&dispatcher,
command_args.iter().map(|a| a.to_string()).collect(),
);
Ok(())
} else {
register_signal_handlers();
intro();
input_loop(dispatcher);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use std::str::from_utf8;
#[test]
fn test_validate_registered_commands() {
util::set_dev_commands_included(true);
util::set_usb_commands_included(true);
setup_dispatcher();
}
#[test]
fn test_write_in_columns_no_width() {
let mut output = Cursor::new(Vec::new());
assert!(write_in_columns(
&mut output,
&["a".to_string(), "bb".to_string(), "ccc".to_string()],
None
)
.is_ok());
assert_eq!(from_utf8(output.get_ref()).unwrap(), "a bb ccc\r\n");
}
#[test]
fn test_write_in_columns_width() {
let mut output = Cursor::new(Vec::new());
assert!(write_in_columns(
&mut output,
&["a".to_string(), "bb".to_string(), "ccc".to_string()],
Some(10)
)
.is_ok());
assert_eq!(from_utf8(output.get_ref()).unwrap(), "a bb\r\nccc\r\n");
}
}