add basic autocomplete and history support to REPL

This commit is contained in:
vms 2020-08-09 14:14:03 +03:00
parent 31a15baf76
commit d68e0cd76e
4 changed files with 327 additions and 173 deletions

11
Cargo.lock generated
View File

@ -566,6 +566,7 @@ dependencies = [
"fluence-app-service", "fluence-app-service",
"rustop", "rustop",
"rustyline", "rustyline",
"rustyline-derive",
"serde_json", "serde_json",
"uuid", "uuid",
"wasmer-wasi-fl", "wasmer-wasi-fl",
@ -1132,6 +1133,16 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "rustyline-derive"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a50e29610a5be68d4a586a5cce3bfb572ed2c2a74227e4168444b7bf4e5235"
dependencies = [
"quote",
"syn",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.5" version = "1.0.5"

View File

@ -20,5 +20,6 @@ serde_json = "1.0.57"
wasmer-wasi = { package = "wasmer-wasi-fl", version = "0.17.0"} wasmer-wasi = { package = "wasmer-wasi-fl", version = "0.17.0"}
rustyline = "6.1.2" rustyline = "6.1.2"
rustyline-derive = "0.3.1"
rustop = "1.1.0" rustop = "1.1.0"
uuid = { version = "0.8.1", features = ["v4"] } uuid = { version = "0.8.1", features = ["v4"] }

View File

@ -25,204 +25,146 @@
#![warn(rust_2018_idioms)] #![warn(rust_2018_idioms)]
/// Command-line tool intended to test Fluence FaaS. /// Command-line tool intended to test Fluence FaaS.
use std::fs;
macro_rules! next_argument { mod repl;
($arg_name:ident, $args:ident, $error_msg:expr) => {
let $arg_name = if let Some($arg_name) = $args.next() {
$arg_name
} else {
println!($error_msg);
continue;
};
};
}
fn main() -> Result<(), anyhow::Error> { use repl::REPL;
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::config::OutputStreamType;
use rustyline::error::ReadlineError;
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
use rustyline::hint::{Hinter, HistoryHinter};
use rustyline::validate::{self, MatchingBracketValidator, Validator};
use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyPress};
use rustyline_derive::Helper;
use std::borrow::Cow::{self, Borrowed, Owned};
pub(crate) type Result<T> = std::result::Result<T, anyhow::Error>;
fn main() -> Result<()> {
let (args, _) = rustop::opts! { let (args, _) = rustop::opts! {
synopsis "Fluence Application service REPL"; synopsis "Fluence Application service REPL";
param config_file_path: Option<String>, desc: "Path to a service config"; param config_file_path: Option<String>, desc: "Path to a service config";
} }
.parse_or_exit(); .parse_or_exit();
println!("Welcome to the Fluence FaaS REPL:"); let config = Config::builder()
let mut app_service = create_service_from_config(args.config_file_path)?; .history_ignore_space(true)
.completion_type(CompletionType::List)
.edit_mode(EditMode::Emacs)
.output_stream(OutputStreamType::Stdout)
.build();
let h = MyHelper {
completer: FilenameCompleter::new(),
highlighter: MatchingBracketHighlighter::new(),
hinter: HistoryHinter {},
colored_prompt: "".to_owned(),
validator: MatchingBracketValidator::new(),
};
let mut rl = Editor::with_config(config);
rl.set_helper(Some(h));
rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward);
rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward);
let _ = rl.load_history("history.txt");
let mut rl = rustyline::Editor::<()>::new(); println!("Welcome to the Fluence FaaS REPL:");
let mut repl = REPL::new(args.config_file_path)?;
let mut count = 1;
loop { loop {
let readline = rl.readline(">> "); let p = format!("{}> ", count);
let readline = match readline { rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{}\x1b[0m", p);
Ok(readline) => readline, let readline = rl.readline(&p);
Err(e) => { match readline {
println!("a error occurred: {}", e); Ok(line) => {
rl.add_history_entry(line.as_str());
repl.execute(line.split_whitespace());
}
Err(ReadlineError::Interrupted) => {
println!("CTRL-C");
break; break;
} }
}; Err(ReadlineError::Eof) => {
println!("CTRL-D");
let mut args = readline.split_whitespace(); break;
match args.next() {
Some("new") => {
app_service = match create_service_from_config(args.next()) {
Ok(service) => service,
Err(e) => {
println!("failed to create a new application service: {}", e);
app_service
} }
}; Err(err) => {
} println!("Error: {:?}", err);
Some("load") => { break;
next_argument!(module_name, args, "Module name should be specified");
next_argument!(module_path, args, "Module path should be specified");
let wasm_bytes = fs::read(module_path);
if let Err(e) = wasm_bytes {
println!("failed to read wasm module: {}", e);
continue;
}
let result_msg = match app_service
.load_module::<String, fluence_app_service::ModuleConfig>(
module_name.into(),
&wasm_bytes.unwrap(),
None,
) {
Ok(_) => "module successfully loaded into App service".to_string(),
Err(e) => format!("module loaded failed with: {:?}", e),
};
println!("{}", result_msg);
}
Some("unload") => {
next_argument!(module_name, args, "Module name should be specified");
let result_msg = match app_service.unload_module(module_name) {
Ok(_) => "module successfully unloaded from App service".to_string(),
Err(e) => format!("module unloaded failed with: {:?}", e),
};
println!("{}", result_msg);
}
Some("call") => {
next_argument!(module_name, args, "Module name should be specified");
next_argument!(func_name, args, "Function name should be specified");
let module_arg: String = args.collect();
let module_arg: serde_json::Value = match serde_json::from_str(&module_arg) {
Ok(module_arg) => module_arg,
Err(e) => {
println!("incorrect arguments {}", e);
continue;
}
};
let result = match app_service.call(module_name, func_name, module_arg) {
Ok(result) => format!("result: {:?}", result),
Err(e) => format!("execution failed with {:?}", e),
};
println!("{}", result);
}
Some("envs") => {
next_argument!(module_name, args, "Module name should be specified");
match app_service.get_wasi_state(module_name) {
Ok(wasi_state) => print_envs(wasi_state),
Err(e) => println!("{}", e),
};
}
Some("fs") => {
next_argument!(module_name, args, "Module name should be specified");
match app_service.get_wasi_state(module_name) {
Ok(wasi_state) => print_fs_state(wasi_state),
Err(e) => println!("{}", e),
};
}
Some("interface") => {
let interface = app_service.get_interface();
println!("application service interface:\n{}", interface);
}
Some("h") | Some("help") | None => {
println!(
"Enter:\n\
new [config_path] - to create a new AppService (current will be removed)\n\
load <module_name> <module_path> - to load a new Wasm module into App service\n\
unload <module_name> - to unload Wasm module from AppService\n\
call <module_name> <func_name> [args] - to call function with func_name of module with module_name\n\
interface - to print public interface of current AppService\n\
envs <module_name> - to print environment variables of module with module_name\n\
fs <module_name> - to print filesystem state of module with module_name\n\
h/help - to print this message\n\
e/exit/q/quit - to exit"
);
}
Some("e") | Some("exit") | Some("q") | Some("quit") => break,
_ => {
println!("unsupported command");
} }
} }
count += 1;
} }
Ok(()) Ok(())
} }
fn create_service_from_config<S: Into<String>>( #[derive(Helper)]
config_file_path: Option<S>, struct MyHelper {
) -> Result<fluence_app_service::AppService, anyhow::Error> { completer: FilenameCompleter,
let tmp_path: String = std::env::temp_dir().to_string_lossy().into(); highlighter: MatchingBracketHighlighter,
let service_id = uuid::Uuid::new_v4().to_string(); validator: MatchingBracketValidator,
hinter: HistoryHinter,
let app_service = match config_file_path { colored_prompt: String,
Some(config_file_path) => {
let config_file_path = config_file_path.into();
fluence_app_service::AppService::with_raw_config(
config_file_path,
&service_id,
Some(&tmp_path),
)
}
None => {
let mut config: fluence_app_service::RawModulesConfig = <_>::default();
config.service_base_dir = Some(tmp_path);
fluence_app_service::AppService::new(std::iter::empty(), config, &service_id)
}
}?;
println!("app service's created with service id = {}", service_id);
Ok(app_service)
} }
fn print_envs(wasi_state: &wasmer_wasi::state::WasiState) { impl Completer for MyHelper {
let envs = &wasi_state.envs; type Candidate = Pair;
println!("Environment variables:"); fn complete(
for env in envs.iter() { &self,
match String::from_utf8(env.clone()) { line: &str,
Ok(string) => println!("{}", string), pos: usize,
Err(_) => println!("{:?}", env), ctx: &Context<'_>,
} ) -> std::result::Result<(usize, Vec<Pair>), ReadlineError> {
self.completer.complete(line, pos, ctx)
} }
} }
fn print_fs_state(wasi_state: &wasmer_wasi::state::WasiState) { impl Hinter for MyHelper {
let wasi_fs = &wasi_state.fs; fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
self.hinter.hint(line, pos, ctx)
println!("preopened file descriptors:\n{:?}\n", wasi_fs.preopen_fds); }
}
println!("name map:");
for (name, inode) in &wasi_fs.name_map { impl Highlighter for MyHelper {
println!("{} - {:?}", name, inode); fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
} self.highlighter.highlight(line, pos)
}
println!("\nfile descriptors map:");
for (id, fd) in &wasi_fs.fd_map { fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
println!("{} - {:?}", id, fd); &'s self,
} prompt: &'p str,
default: bool,
println!("\norphan file descriptors:"); ) -> Cow<'b, str> {
for (fd, inode) in &wasi_fs.orphan_fds { if default {
println!("{:?} - {:?}", fd, inode); Borrowed(&self.colored_prompt)
} } else {
Borrowed(prompt)
println!("\ninodes:"); }
for (id, inode) in wasi_fs.inodes.iter().enumerate() { }
println!("{}: {:?}", id, inode);
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
Owned("\x1b[1m".to_owned() + hint + "\x1b[m")
}
fn highlight_char(&self, line: &str, pos: usize) -> bool {
self.highlighter.highlight_char(line, pos)
}
}
impl Validator for MyHelper {
fn validate(
&self,
ctx: &mut validate::ValidationContext<'_>,
) -> rustyline::Result<validate::ValidationResult> {
self.validator.validate(ctx)
}
fn validate_while_typing(&self) -> bool {
self.validator.validate_while_typing()
} }
} }

200
tools/repl/src/repl.rs Normal file
View File

@ -0,0 +1,200 @@
/*
* Copyright 2020 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use crate::Result;
use fluence_app_service::AppService;
use std::path::PathBuf;
use std::fs;
macro_rules! next_argument {
($arg_name:ident, $args:ident, $error_msg:expr) => {
let $arg_name = if let Some($arg_name) = $args.next() {
$arg_name
} else {
println!($error_msg);
return;
};
};
}
pub(super) struct REPL {
app_service: AppService,
}
impl REPL {
pub fn new<S: Into<PathBuf>>(config_file_path: Option<S>) -> Result<Self> {
let app_service = Self::create_app_service(config_file_path)?;
Ok(Self { app_service })
}
pub fn execute<'a>(&mut self, mut args: impl Iterator<Item = &'a str>) {
match args.next() {
Some("new") => {
match Self::create_app_service(args.next()) {
Ok(service) => self.app_service = service,
Err(e) => println!("failed to create a new application service: {}", e),
};
}
Some("load") => {
next_argument!(module_name, args, "Module name should be specified");
next_argument!(module_path, args, "Module path should be specified");
let wasm_bytes = fs::read(module_path);
if let Err(e) = wasm_bytes {
println!("failed to read wasm module: {}", e);
return;
}
let result_msg = match self
.app_service
.load_module::<String, fluence_app_service::ModuleConfig>(
module_name.into(),
&wasm_bytes.unwrap(),
None,
) {
Ok(_) => "module successfully loaded into App service".to_string(),
Err(e) => format!("module loaded failed with: {:?}", e),
};
println!("{}", result_msg);
}
Some("unload") => {
next_argument!(module_name, args, "Module name should be specified");
let result_msg = match self.app_service.unload_module(module_name) {
Ok(_) => "module successfully unloaded from App service".to_string(),
Err(e) => format!("module unloaded failed with: {:?}", e),
};
println!("{}", result_msg);
}
Some("call") => {
next_argument!(module_name, args, "Module name should be specified");
next_argument!(func_name, args, "Function name should be specified");
let module_arg: String = args.collect();
let module_arg: serde_json::Value = match serde_json::from_str(&module_arg) {
Ok(module_arg) => module_arg,
Err(e) => {
println!("incorrect arguments {}", e);
return;
}
};
let result = match self.app_service.call(module_name, func_name, module_arg) {
Ok(result) => format!("result: {:?}", result),
Err(e) => format!("execution failed with {:?}", e),
};
println!("{}", result);
}
Some("envs") => {
next_argument!(module_name, args, "Module name should be specified");
match self.app_service.get_wasi_state(module_name) {
Ok(wasi_state) => Self::print_envs(wasi_state),
Err(e) => println!("{}", e),
};
}
Some("fs") => {
next_argument!(module_name, args, "Module name should be specified");
match self.app_service.get_wasi_state(module_name) {
Ok(wasi_state) => Self::print_fs_state(wasi_state),
Err(e) => println!("{}", e),
};
}
Some("interface") => {
let interface = self.app_service.get_interface();
println!("application service interface:\n{}", interface);
}
Some("h") | Some("help") | None => {
println!(
"Enter:\n\
new [config_path] - to create a new AppService (current will be removed)\n\
load <module_name> <module_path> - to load a new Wasm module into App service\n\
unload <module_name> - to unload Wasm module from AppService\n\
call <module_name> <func_name> [args] - to call function with func_name of module with module_name\n\
interface - to print public interface of current AppService\n\
envs <module_name> - to print environment variables of module with module_name\n\
fs <module_name> - to print filesystem state of module with module_name\n\
h/help - to print this message\n\
Ctrl-C - to exit"
);
}
_ => {
println!("unsupported command");
}
}
}
fn create_app_service<S: Into<PathBuf>>(config_file_path: Option<S>) -> Result<AppService> {
let tmp_path: String = std::env::temp_dir().to_string_lossy().into();
let service_id = uuid::Uuid::new_v4().to_string();
let app_service = match config_file_path {
Some(config_file_path) => {
let config_file_path = config_file_path.into();
AppService::with_raw_config(config_file_path, &service_id, Some(&tmp_path))
}
None => {
let mut config: fluence_app_service::RawModulesConfig = <_>::default();
config.service_base_dir = Some(tmp_path);
AppService::new(std::iter::empty(), config, &service_id)
}
}?;
println!("app service's created with service id = {}", service_id);
Ok(app_service)
}
fn print_envs(wasi_state: &wasmer_wasi::state::WasiState) {
let envs = &wasi_state.envs;
println!("Environment variables:");
for env in envs.iter() {
match String::from_utf8(env.clone()) {
Ok(string) => println!("{}", string),
Err(_) => println!("{:?}", env),
}
}
}
fn print_fs_state(wasi_state: &wasmer_wasi::state::WasiState) {
let wasi_fs = &wasi_state.fs;
println!("preopened file descriptors:\n{:?}\n", wasi_fs.preopen_fds);
println!("name map:");
for (name, inode) in &wasi_fs.name_map {
println!("{} - {:?}", name, inode);
}
println!("\nfile descriptors map:");
for (id, fd) in &wasi_fs.fd_map {
println!("{} - {:?}", id, fd);
}
println!("\norphan file descriptors:");
for (fd, inode) in &wasi_fs.orphan_fds {
println!("{:?} - {:?}", fd, inode);
}
println!("\ninodes:");
for (id, inode) in wasi_fs.inodes.iter().enumerate() {
println!("{}: {:?}", id, inode);
}
}
}