diff --git a/Cargo.lock b/Cargo.lock index d2debb24..970914ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -566,6 +566,7 @@ dependencies = [ "fluence-app-service", "rustop", "rustyline", + "rustyline-derive", "serde_json", "uuid", "wasmer-wasi-fl", @@ -1132,6 +1133,16 @@ dependencies = [ "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]] name = "ryu" version = "1.0.5" diff --git a/tools/repl/Cargo.toml b/tools/repl/Cargo.toml index ef07601d..58802b79 100644 --- a/tools/repl/Cargo.toml +++ b/tools/repl/Cargo.toml @@ -20,5 +20,6 @@ serde_json = "1.0.57" wasmer-wasi = { package = "wasmer-wasi-fl", version = "0.17.0"} rustyline = "6.1.2" +rustyline-derive = "0.3.1" rustop = "1.1.0" uuid = { version = "0.8.1", features = ["v4"] } diff --git a/tools/repl/src/main.rs b/tools/repl/src/main.rs index 45663f97..4460d41d 100644 --- a/tools/repl/src/main.rs +++ b/tools/repl/src/main.rs @@ -25,204 +25,146 @@ #![warn(rust_2018_idioms)] /// Command-line tool intended to test Fluence FaaS. -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); - continue; - }; - }; -} +mod repl; -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 = std::result::Result; + +fn main() -> Result<()> { let (args, _) = rustop::opts! { synopsis "Fluence Application service REPL"; param config_file_path: Option, desc: "Path to a service config"; } .parse_or_exit(); - println!("Welcome to the Fluence FaaS REPL:"); - let mut app_service = create_service_from_config(args.config_file_path)?; + let config = Config::builder() + .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 { - let readline = rl.readline(">> "); - let readline = match readline { - Ok(readline) => readline, - Err(e) => { - println!("a error occurred: {}", e); + let p = format!("{}> ", count); + rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{}\x1b[0m", p); + let readline = rl.readline(&p); + match readline { + Ok(line) => { + rl.add_history_entry(line.as_str()); + repl.execute(line.split_whitespace()); + } + Err(ReadlineError::Interrupted) => { + println!("CTRL-C"); break; } - }; - - let mut args = readline.split_whitespace(); - 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(ReadlineError::Eof) => { + println!("CTRL-D"); + break; } - 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); - continue; - } - - let result_msg = match app_service - .load_module::( - 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 - to load a new Wasm module into App service\n\ - unload - to unload Wasm module from AppService\n\ - call [args] - to call function with func_name of module with module_name\n\ - interface - to print public interface of current AppService\n\ - envs - to print environment variables of module with module_name\n\ - fs - 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"); + Err(err) => { + println!("Error: {:?}", err); + break; } } + count += 1; } Ok(()) } -fn create_service_from_config>( - config_file_path: Option, -) -> Result { - 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(); - 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) +#[derive(Helper)] +struct MyHelper { + completer: FilenameCompleter, + highlighter: MatchingBracketHighlighter, + validator: MatchingBracketValidator, + hinter: HistoryHinter, + colored_prompt: String, } -fn print_envs(wasi_state: &wasmer_wasi::state::WasiState) { - let envs = &wasi_state.envs; +impl Completer for MyHelper { + type Candidate = Pair; - println!("Environment variables:"); - for env in envs.iter() { - match String::from_utf8(env.clone()) { - Ok(string) => println!("{}", string), - Err(_) => println!("{:?}", env), - } + fn complete( + &self, + line: &str, + pos: usize, + ctx: &Context<'_>, + ) -> std::result::Result<(usize, Vec), ReadlineError> { + self.completer.complete(line, pos, ctx) } } -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); +impl Hinter for MyHelper { + fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { + self.hinter.hint(line, pos, ctx) + } +} + +impl Highlighter for MyHelper { + fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { + self.highlighter.highlight(line, pos) + } + + fn highlight_prompt<'b, 's: 'b, 'p: 'b>( + &'s self, + prompt: &'p str, + default: bool, + ) -> Cow<'b, str> { + if default { + Borrowed(&self.colored_prompt) + } else { + Borrowed(prompt) + } + } + + 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 { + self.validator.validate(ctx) + } + + fn validate_while_typing(&self) -> bool { + self.validator.validate_while_typing() } } diff --git a/tools/repl/src/repl.rs b/tools/repl/src/repl.rs new file mode 100644 index 00000000..3da39fc5 --- /dev/null +++ b/tools/repl/src/repl.rs @@ -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>(config_file_path: Option) -> Result { + let app_service = Self::create_app_service(config_file_path)?; + Ok(Self { app_service }) + } + + pub fn execute<'a>(&mut self, mut args: impl Iterator) { + 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::( + 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 - to load a new Wasm module into App service\n\ + unload - to unload Wasm module from AppService\n\ + call [args] - to call function with func_name of module with module_name\n\ + interface - to print public interface of current AppService\n\ + envs - to print environment variables of module with module_name\n\ + fs - 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>(config_file_path: Option) -> Result { + 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); + } + } +}