blob: 8d0d3a2f503b473e5daa308c806481297f27b1fe [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
pub mod args;
pub mod builtin;
pub mod error;
use {
crate::shell::{
args::args_to_json,
builtin::{Builtin, BuiltinCommand},
error::ShellError,
},
anyhow::{Error, Result},
log::{error, info, warn},
rustyline::{
completion::{Completer, FilenameCompleter, Pair},
error::ReadlineError,
highlight::Highlighter,
hint::Hinter,
CompletionType, Config, Editor, Helper,
},
scrutiny::{
engine::{
dispatcher::{ControllerDispatcher, DispatcherError},
manager::{PluginManager, PluginState},
plugin::PluginDescriptor,
},
model::controller::ConnectionMode,
},
serde_json::{self, json, Value},
std::{
borrow::Cow::{self, Borrowed},
collections::{HashMap, VecDeque},
fmt::Write,
process,
sync::{Arc, Mutex, RwLock},
},
termion::{self, clear, color, cursor, style},
};
/// A CommandResponse is an internal type to define whether a command was acted
/// on by a submodule or not.
#[derive(Eq, PartialEq)]
enum CommandResponse {
Executed,
NotFound,
}
#[allow(dead_code)]
struct ScrutinyHelper {
dispatcher: Arc<RwLock<ControllerDispatcher>>,
file_completer: FilenameCompleter,
}
impl ScrutinyHelper {
fn new(dispatcher: Arc<RwLock<ControllerDispatcher>>) -> Self {
Self { dispatcher, file_completer: FilenameCompleter::new() }
}
}
impl Completer for ScrutinyHelper {
type Candidate = Pair;
fn complete(&self, line: &str, _pos: usize) -> Result<(usize, Vec<Pair>), ReadlineError> {
let tokens = line.split(" ").collect::<Vec<&str>>();
// If we have more than 1 token then just use the file completer.
if tokens.len() <= 1 {
let mut controllers = self.dispatcher.read().unwrap().controllers_all();
controllers.append(&mut BuiltinCommand::commands());
let mut matches = vec![];
let search_term = tokens.first().unwrap();
for controller in controllers.iter() {
let mut command = str::replace(&controller, "/api/", "");
command = str::replace(&command, "/", ".");
if command.starts_with(search_term) {
matches.push(Pair {
display: command.clone(),
replacement: command.trim_start_matches(search_term).to_string(),
});
}
}
return Ok((line.len(), matches));
} else {
let current_param = tokens.last().unwrap();
if current_param.starts_with('-') {
let mut namespace = tokens.first().unwrap().to_string();
if !namespace.starts_with("/api") {
namespace = str::replace(&namespace, ".", "/");
namespace.insert_str(0, "/api/");
if let Ok(hints) = self.dispatcher.read().unwrap().hints(namespace) {
let mut matches = vec![];
for (param, _data_type) in hints.iter() {
if param.starts_with(current_param) {
matches.push(Pair {
display: param.clone(),
replacement: param[current_param.len()..].to_string(),
});
}
}
return Ok((line.len(), matches));
}
}
}
}
Ok((line.len(), vec![]))
}
}
impl Hinter for ScrutinyHelper {
fn hint(&self, _line: &str, _pos: usize) -> Option<String> {
None
}
}
impl Highlighter for ScrutinyHelper {
fn highlight_prompt<'p>(&self, prompt: &'p str) -> Cow<'p, str> {
Borrowed(prompt)
}
}
impl Helper for ScrutinyHelper {}
pub struct Shell {
manager: Arc<Mutex<PluginManager>>,
dispatcher: Arc<RwLock<ControllerDispatcher>>,
readline: Editor<ScrutinyHelper>,
}
impl Shell {
pub fn new(
manager: Arc<Mutex<PluginManager>>,
dispatcher: Arc<RwLock<ControllerDispatcher>>,
) -> Self {
let config = Config::builder()
.history_ignore_space(true)
.completion_type(CompletionType::List)
.build();
let mut readline = Editor::with_config(config);
let helper = ScrutinyHelper::new(dispatcher.clone());
readline.set_helper(Some(helper));
if let Err(_) = readline.load_history("/tmp/scrutiny_history") {
warn!("No shell history available");
}
Self { manager, dispatcher, readline }
}
fn prompt(&mut self) -> Option<String> {
let prompt = format!(
"{reset}{bold}{yellow_fg}scrutiny ยป{reset} ",
yellow_fg = color::Fg(color::Yellow),
bold = style::Bold,
reset = style::Reset,
);
let readline = self.readline.readline(&prompt);
if let Err(e) = self.readline.save_history("/tmp/scrutiny_history") {
warn!("Failed to save scrutiny shell history: {}", e);
}
match readline {
Ok(line) => {
self.readline.add_history_entry(line.as_str());
Some(line)
}
_ => None,
}
}
fn builtin(&mut self, command: String, out_buffer: &mut impl std::fmt::Write) -> Result<CommandResponse> {
if let Some(builtin) = BuiltinCommand::parse(command) {
match builtin.program {
Builtin::PluginLoad => {
if builtin.args.len() != 1 {
writeln!(out_buffer, "Error: Provide a single plugin to load.")?;
return Ok(CommandResponse::Executed);
}
let desc = PluginDescriptor::new(builtin.args.first().unwrap());
if let Err(e) = self.manager.lock().unwrap().load(&desc) {
writeln!(out_buffer, "Error: {}", e)?;
}
}
Builtin::PluginUnload => {
if builtin.args.len() != 1 {
writeln!(out_buffer, "Error: Provide a single plugin to unload.")?;
return Ok(CommandResponse::Executed);
}
let desc = PluginDescriptor::new(builtin.args.first().unwrap());
if let Err(e) = self.manager.lock().unwrap().unload(&desc) {
writeln!(out_buffer, "Error: {}", e)?;
}
}
Builtin::Print => {
let message = builtin.args.join(" ");
writeln!(out_buffer, "{}", message)?;
}
Builtin::Help => {
// Provide usage for a specific command.
if builtin.args.len() == 1 {
let mut command = builtin.args.first().unwrap().clone();
command = str::replace(&command, ".", "/");
if !command.starts_with("/api/") {
command.insert_str(0, "/api/");
}
let result = self.dispatcher.read().unwrap().usage(command);
if let Ok(usage) = result {
writeln!(out_buffer, "{}", usage)?;
} else {
writeln!(out_buffer, "Error: Command does not exist.")?;
}
return Ok(CommandResponse::Executed);
}
// Provide usage for a general command.
BuiltinCommand::usage();
writeln!(out_buffer, "")?;
let manager = self.manager.lock().unwrap();
let plugins = manager.plugins();
for plugin in plugins.iter() {
let state = manager.state(plugin).unwrap();
if state == PluginState::Loaded {
let instance_id = manager.instance_id(plugin).unwrap();
let mut controllers =
self.dispatcher.read().unwrap().controllers(instance_id);
controllers.sort();
// Add spacing for the plugin names.
let mut plugin_name = format!("{}", plugin);
let mut formatted: Vec<char> = Vec::new();
for c in plugin_name.chars() {
if c.is_uppercase() && !formatted.is_empty() {
formatted.push(' ');
}
formatted.push(c);
}
plugin_name = formatted.into_iter().collect();
writeln!(out_buffer, "{} Commands:", plugin_name)?;
// Print out all the plugin specific commands
for controller in controllers.iter() {
let description = self
.dispatcher
.read()
.unwrap()
.description(controller.to_string())
.unwrap();
let command = str::replace(&controller, "/api/", "");
let shell_command = str::replace(&command, "/", ".");
if description.is_empty() {
writeln!(out_buffer, " {}", shell_command)?;
} else {
writeln!(
out_buffer,
" {:<25} - {}",
shell_command, description
)?;
}
}
writeln!(out_buffer, "")?;
}
}
}
Builtin::History => {
let mut count = 1;
for entry in self.readline.history().iter() {
writeln!(out_buffer, "{} {}", count, entry)?;
count += 1;
}
}
Builtin::Clear => {
write!(out_buffer, "{}{}", clear::All, cursor::Goto(1, 1))?;
}
Builtin::Exit => {
process::exit(0);
}
};
return Ok(CommandResponse::Executed);
}
Ok(CommandResponse::NotFound)
}
/// Parses a command returning the namespace and arguments as a json value.
/// This can fail if any of the command arguments are invalid. This function
/// does not check whether the command exists just verifies the input is
/// sanitized.
fn parse_command(command: impl Into<String>) -> Result<(String, Value)> {
let mut tokens: VecDeque<String> =
command.into().split_whitespace().map(|s| String::from(s)).collect();
if tokens.len() == 0 {
return Err(Error::new(ShellError::empty_command()));
}
// Transform foo.bar to /foo/bar
let head = tokens.pop_front().unwrap();
let namespace = if head.starts_with("/") {
head.to_string()
} else {
"/api/".to_owned() + &str::replace(&head, ".", "/")
};
// Parse the command arguments.
let empty_command: HashMap<String, String> = HashMap::new();
let mut query = json!(empty_command);
if !tokens.is_empty() {
if tokens.front().unwrap().starts_with("`") {
let mut body = Vec::from(tokens).join(" ");
if body.len() > 2 && body.ends_with("`") {
body.remove(0);
body.pop();
match serde_json::from_str(&body) {
Ok(expr) => {
query = expr;
}
Err(err) => {
return Err(Error::new(err));
}
}
} else {
return Err(Error::new(ShellError::unescaped_json_string(body)));
}
} else if tokens.front().unwrap().starts_with("--") {
query = args_to_json(&tokens);
}
}
Ok((namespace, query))
}
/// Attempts to transform the command into a dispatcher command to see if
/// any plugin services that url. Two syntaxes are supported /foo/bar or
/// foo.bar both will be translated into /foo/bar before being sent to the
/// dispatcher.
fn plugin(&mut self, command: String, out_buffer: &mut impl std::fmt::Write) -> Result<CommandResponse> {
let command_result = Self::parse_command(command);
if let Err(err) = command_result {
if let Some(shell_error) = err.downcast_ref::<ShellError>() {
if let ShellError::EmptyCommand = shell_error {
return Ok(CommandResponse::Executed);
}
}
writeln!(out_buffer, "Error: {}", err)?;
return Ok(CommandResponse::Executed);
}
let (namespace, query) = command_result.unwrap();
let result = self.dispatcher.read().unwrap().query(ConnectionMode::Local, namespace, query);
match result {
Err(e) => {
if let Some(dispatch_error) = e.downcast_ref::<DispatcherError>() {
match dispatch_error {
DispatcherError::NamespaceDoesNotExist(_) => {
return Ok(CommandResponse::NotFound);
}
_ => {}
}
}
writeln!(out_buffer, "Error: {}", e)?;
Ok(CommandResponse::Executed)
}
Ok(result) => {
writeln!(out_buffer, "{}", serde_json::to_string_pretty(&result).unwrap())?;
Ok(CommandResponse::Executed)
}
}
}
/// Executes a single command.
pub fn execute(&mut self, command: String) -> Result<String> {
if command.is_empty() || command.starts_with("#") {
return Ok(String::new());
}
info!("Command: {}", command);
let mut output = String::new();
if self.builtin(command.clone(), &mut output)? == CommandResponse::Executed {
} else if self.plugin(command.clone(), &mut output)? == CommandResponse::Executed {
} else {
writeln!(output, "scrutiny: command not found: {}", command)?;
}
print!("{}", output);
Ok(output)
}
/// Runs a blocking loop executing commands from standard input.
pub fn run(&mut self) {
loop {
if let Some(command) = self.prompt() {
if let Err(err) = self.execute(command) {
error!("Execute failed: {:?}", err);
}
} else {
break;
}
}
}
}
#[cfg(test)]
mod tests {
use {
super::*,
scrutiny::model::{
controller::{DataController, HintDataType},
model::DataModel,
},
scrutiny_testing::fake::*,
uuid::Uuid,
};
#[derive(Default)]
struct FakeController {}
impl DataController for FakeController {
fn query(&self, _: Arc<DataModel>, _: Value) -> Result<Value> {
Ok(json!(""))
}
fn hints(&self) -> Vec<(String, HintDataType)> {
vec![("--baz".to_string(), HintDataType::NoType)]
}
}
fn test_model() -> Arc<DataModel> {
fake_data_model()
}
#[test]
fn test_parse_command_errors() {
assert_eq!(Shell::parse_command("foo").is_ok(), true);
assert_eq!(Shell::parse_command("foo `{}`").is_ok(), true);
assert_eq!(Shell::parse_command("foo `{\"abc\": 1}`").is_ok(), true);
assert_eq!(Shell::parse_command("foo `{aaa}`").is_ok(), false);
assert_eq!(Shell::parse_command("foo `{").is_ok(), false);
assert_eq!(Shell::parse_command("foo `").is_ok(), false);
}
#[test]
fn test_parse_command_tokens() {
assert_eq!(Shell::parse_command("foo").unwrap().0, "/api/foo");
assert_eq!(Shell::parse_command("foo `{\"a\": 1}`").unwrap().1, json!({"a": 1}));
}
#[test]
fn test_help_ok() {
assert_eq!(Shell::parse_command("help").is_ok(), true);
assert_eq!(Shell::parse_command("help zbi.bootfs").is_ok(), true);
}
#[test]
fn test_hinter_empty() {
let data_model = test_model();
let dispatcher = Arc::new(RwLock::new(ControllerDispatcher::new(data_model)));
let helper = ScrutinyHelper::new(dispatcher);
let result = helper.complete("", 0).unwrap();
assert_eq!(result.1.len(), BuiltinCommand::commands().len());
}
#[test]
fn test_hinter() {
let data_model = test_model();
let dispatcher = Arc::new(RwLock::new(ControllerDispatcher::new(data_model)));
let fake = Arc::new(FakeController::default());
let namespace = "/api/test/foo/bar".to_string();
dispatcher.write().unwrap().add(Uuid::new_v4(), namespace.clone(), fake).unwrap();
let helper = ScrutinyHelper::new(dispatcher);
let result = helper.complete("", 0).unwrap();
assert_eq!(result.1.len(), BuiltinCommand::commands().len() + 1);
let result = helper.complete("test.foo.ba", 0).unwrap();
assert_eq!(result.1.len(), 1);
let result = helper.complete("test.foo.bar -", 0).unwrap();
assert_eq!(result.1.len(), 1);
let result = helper.complete("test.foo.bar --a", 0).unwrap();
assert_eq!(result.1.len(), 0);
let result = helper.complete("test.foo.bar --b", 0).unwrap();
assert_eq!(result.1.len(), 1);
}
}