diff --git a/src/cli/admin.rs b/src/cli/admin.rs index 01491bf9b279f88fea4ac728923579f7e912863a..929be1e82c17112e25498257e0735e0a6cc720be 100644 --- a/src/cli/admin.rs +++ b/src/cli/admin.rs @@ -82,7 +82,7 @@ impl UnixClient { /// Creates struct object using `path` for raw unix socket. /// - /// Setup delimiter and ignore tarantool prompt. + /// Setup delimiter, default language and ignore tarantool prompt. fn new(path: &str) -> Result<Self> { let socket = UnixStream::connect(path)?; let mut client = Self::from_stream(socket)?; @@ -99,6 +99,11 @@ impl UnixClient { debug_assert!(prompt.contains("Tarantool")); debug_assert!(prompt.contains("Lua console")); + // set default language SQL + client.write("\\set language sql")?; + let response = client.read()?; + debug_assert!(response.contains("true")); + Ok(client) } @@ -176,7 +181,7 @@ impl UnixClient { let res = completions .unwrap_or_default() .first() - .map(|v| v[1..].to_owned()) + .map(|v| v[1..].into()) .unwrap_or_default(); Ok(res) @@ -202,7 +207,12 @@ fn admin_repl(args: args::Admin) -> core::result::Result<(), ReplError> { }, }; - let mut console = Console::with_completer("picoadmin :) ", helper)?; + let mut console = Console::with_completer(helper)?; + + console.greet(&format!( + "Connected to admin console by socket path \"{}\"", + args.socket_path + )); while let Some(line) = console.read()? { let mut temp_client = client.borrow_mut(); diff --git a/src/cli/connect.rs b/src/cli/connect.rs index d7365decb1711b4c37f1e426baa93692b42ae23f..7383a15e0d35b65a917403dd1452cc91714eec4a 100644 --- a/src/cli/connect.rs +++ b/src/cli/connect.rs @@ -120,7 +120,7 @@ fn sql_repl(args: args::Connect) -> Result<(), ReplError> { }; let mut config = Config::default(); - config.creds = Some((user, password)); + config.creds = Some((user.clone(), password)); config.auth_method = args.auth_method; let client = ::tarantool::fiber::block_on(Client::connect_with_config( @@ -133,7 +133,12 @@ fn sql_repl(args: args::Connect) -> Result<(), ReplError> { // and we want to check whether authentication have succeeded or not ::tarantool::fiber::block_on(client.call("box.schema.user.info", &()))?; - let mut console = Console::new("picosql :) ")?; + let mut console = Console::new()?; + + console.greet(&format!( + "Connected to interactive console by address \"{}:{}\" under \"{}\" user", + address.host, address.port, user + )); while let Some(line) = console.read()? { let response = ::tarantool::fiber::block_on(client.call("pico.sql", &(line,)))?; diff --git a/src/cli/console.rs b/src/cli/console.rs index 3cd308dbb4ccf0ad8280ff15886f5e21b80533f2..8e3efd55c690382091f64cbe7c43ba73291591b2 100644 --- a/src/cli/console.rs +++ b/src/cli/console.rs @@ -34,15 +34,22 @@ pub enum ReplError { pub type Result<T> = std::result::Result<T, ReplError>; +/// Shows user of console +pub enum ConsoleType { + Admin, + User, +} + /// Input/output handler pub struct Console<H: Helper> { editor: Editor<H, FileHistory>, history_file_path: PathBuf, - prompt: String, + console_type: ConsoleType, } impl<T: Helper> Console<T> { - const HISTORY_FILE_NAME: &'static str = ".picodata_history"; + const HISTORY_FILE_NAME: &str = ".picodata_history"; + const PROMPT: &str = "picodata> "; // Ideally we should have an enum for all commands. For now we have only two options, usual line // and only one special command. To not overengineer things at this point just handle this as ifs. @@ -77,13 +84,34 @@ impl<T: Helper> Console<T> { return Ok(ControlFlow::Continue(())); } + // we don't check content intentionally let line = read_to_string(temp.path()).map_err(ReplError::Io)?; return Ok(ControlFlow::Break(line)); - } else if line == "\\lua" { - return Ok(ControlFlow::Break("\\set language lua".to_owned())); - } else if line == "\\sql" { - return Ok(ControlFlow::Break("\\set language sql".to_owned())); + } else if line == "\\help" { + self.print_help(); + return Ok(ControlFlow::Continue(())); + } + + // all language switching is availiable only for admin console + match self.console_type { + ConsoleType::User => (), + ConsoleType::Admin => { + if line == "\\lua" { + return Ok(ControlFlow::Break("\\set language lua".into())); + } else if line == "\\sql" { + return Ok(ControlFlow::Break("\\set language sql".into())); + } + + let splitted = line.split_whitespace().collect::<Vec<_>>(); + if splitted.len() > 2 + && (splitted[0] == "\\s" || splitted[0] == "\\set") + && (splitted[1] == "language" || splitted[1] == "l" || splitted[1] == "lang") + && (splitted[2] == "lua" || splitted[2] == "sql") + { + return Ok(ControlFlow::Break(line)); + } + } } self.write("Unknown special sequence"); @@ -98,7 +126,7 @@ impl<T: Helper> Console<T> { /// Reads from stdin. Takes into account treating special symbols. pub fn read(&mut self) -> Result<Option<String>> { loop { - let readline = self.editor.readline(&self.prompt); + let readline = self.editor.readline(Self::PROMPT); match readline { Ok(line) => { let line = match self.process_line(line)? { @@ -151,10 +179,40 @@ impl<T: Helper> Console<T> { Ok((editor, history_file_path)) } + + fn print_help(&self) { + let switch_language_info: &'static str = " + \\lua -- switch current language to Lua + \\sql -- switch current language to SQL"; + + let lang_info = match self.console_type { + ConsoleType::Admin => switch_language_info, + ConsoleType::User => "", + }; + + let help = format!(" + Available backslash commands: + \\e -- open text editor. Path to editor should be set via EDITOR environment variable + \\help -- show this screen{lang_info} + + Available special commands: + Alt + Enter -- move the carriage to newline + Ctrl + C -- discard current input + Ctrl + D -- quit interactive console + "); + + self.write(&help); + } + + /// Prints information about connection and help hint + pub fn greet(&self, connection_info: &str) { + self.write(connection_info); + self.write("type '\\help' for interactive help"); + } } impl Console<LuaHelper> { - pub fn with_completer(prompt: &str, helper: LuaHelper) -> Result<Self> { + pub fn with_completer(helper: LuaHelper) -> Result<Self> { let (mut editor, history_file_path) = Self::editor_with_history()?; editor.set_helper(Some(helper)); @@ -164,19 +222,19 @@ impl Console<LuaHelper> { Ok(Console { editor, history_file_path, - prompt: prompt.to_string(), + console_type: ConsoleType::Admin, }) } } impl Console<()> { - pub fn new(prompt: &str) -> Result<Self> { + pub fn new() -> Result<Self> { let (editor, history_file_path) = Self::editor_with_history()?; Ok(Console { editor, history_file_path, - prompt: prompt.to_string(), + console_type: ConsoleType::User, }) } } diff --git a/test/int/test_cli_connect.py b/test/int/test_cli_connect.py index cf27807026d967eedb3d8de054d5c1fda102a962..f5d0ebc589a50b789534bdf65b67406e6051c872 100644 --- a/test/int/test_cli_connect.py +++ b/test/int/test_cli_connect.py @@ -34,7 +34,7 @@ def test_connect_testuser(i1: Instance): cli.expect_exact("Enter password for testuser: ") cli.sendline("testpass") - cli.expect_exact("picosql :)") + cli.expect_exact("picodata> ") eprint("^D") cli.sendcontrol("d") @@ -53,7 +53,7 @@ def test_connect_user_host_port(i1: Instance): cli.expect_exact("Enter password for testuser: ") cli.sendline("testpass") - cli.expect_exact("picosql :)") + cli.expect_exact("picodata> ") eprint("^D") cli.sendcontrol("d") @@ -69,7 +69,7 @@ def test_connect_guest(i1: Instance): ) cli.logfile = sys.stdout - cli.expect_exact("picosql :)") + cli.expect_exact("picodata> ") eprint("^D") cli.sendcontrol("d") @@ -140,7 +140,7 @@ def test_connect_auth_type_ok(i1: Instance): cli.expect_exact("Enter password for testuser: ") cli.sendline("testpass") - cli.expect_exact("picosql :)") + cli.expect_exact("picodata> ") eprint("^D") cli.sendcontrol("d") @@ -242,6 +242,10 @@ def configure_ldap_server(username, password, data_dir) -> LDAPServerState: ) +@pytest.mark.xfail( + run=False, + reason=("need installed glauth"), +) def test_connect_auth_type_ldap(cluster: Cluster): username = "ldapuser" password = "ldappass" @@ -384,7 +388,7 @@ def test_connect_unix_ok_via_default_sock(cluster: Cluster): ) cli.logfile = sys.stdout - cli.expect_exact("picoadmin :) ") + cli.expect_exact("picodata> ") # Change language to SQL works cli.sendline("\\sql") @@ -469,7 +473,7 @@ def test_connect_with_password_from_file(i1: Instance, binary_path: str): ) cli.logfile = sys.stdout - cli.expect_exact("picosql :)") + cli.expect_exact("picodata> ") eprint("^D") cli.sendcontrol("d") @@ -497,28 +501,84 @@ def test_lua_completion(cluster: Cluster): ) cli.logfile = sys.stdout - cli.expect_exact("picoadmin :) ") + cli.expect_exact("picodata> ") + cli.sendline("\\lua") # With several possible variants they are shown as list - cli.sendline("to\t\t") + cli.send("to") + cli.send("\t\t") cli.expect_exact("tostring( tonumber( tonumber64(") + cli.sendcontrol("c") - cli.sendline("box.c\t\t") + cli.send("box.c") + cli.send("\t\t") cli.expect_exact("box.ctl box.cfg box.commit(") + cli.sendcontrol("c") - cli.sendline("tonumber(to\t\t") + cli.send("tonumber(to") + cli.send("\t\t") cli.expect_exact("tostring( tonumber( tonumber64(") + cli.sendcontrol("c") # With one possible variant it automaticaly completes current word # so we can check that is completed by result of completing this command - cli.sendline("hel\t\t") + cli.send("hel") + cli.send("\t") + cli.expect_exact("help") + cli.sendcontrol("c") + + cli.send("bred bo") + cli.send("\t") + cli.expect_exact("bred box") + + +def test_connect_connection_info_and_help(i1: Instance): + cli = pexpect.spawn( + command=i1.binary_path, + args=["connect", f"{i1.host}:{i1.port}", "-u", "testuser", "-a", "chap-sha1"], + encoding="utf-8", + timeout=1, + ) + cli.logfile = sys.stdout + + cli.expect_exact("Enter password for testuser: ") + cli.sendline("testpass") + cli.expect_exact( - "To get help, see the Tarantool manual at https://tarantool.io/en/doc/" + f'Connected to interactive console by address "{i1.host}:{i1.port}" under "testuser" user' ) + cli.expect_exact("type '\\help' for interactive help") + cli.expect_exact("picodata> ") - cli.sendline("bred bo\t\t") - cli.expect_exact("bred box") - cli.sendline(" ") + eprint("^D") + cli.sendcontrol("d") + cli.expect_exact(pexpect.EOF) + + +def test_admin_connection_info_and_help(cluster: Cluster): + i1 = cluster.add_instance(wait_online=False) + i1.start() + i1.wait_online() + + cli = pexpect.spawn( + # For some uninvestigated reason, readline trims the propmt in CI + # Instead of + # unix/:/some/path/to/admin.sock> + # it prints + # </path/to/admin.sock> + # + # We were unable to debug it quickly and used cwd as a workaround + cwd=i1.data_dir, + command=i1.binary_path, + args=["admin", "./admin.sock"], + encoding="utf-8", + timeout=1, + ) + cli.logfile = sys.stdout + + cli.expect_exact('Connected to admin console by socket path "./admin.sock"') + cli.expect_exact("type '\\help' for interactive help") + cli.expect_exact("picodata> ") eprint("^D") cli.sendcontrol("d")