mysqlshdk/shellcore/base_shell.cc (542 lines of code) (raw):

/* * Copyright (c) 2014, 2025, Oracle and/or its affiliates. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2.0, * as published by the Free Software Foundation. * * This program is designed to work with certain software (including * but not limited to OpenSSL) that is licensed under separate terms, * as designated in a particular file or component or in included license * documentation. The authors of MySQL hereby grant you an additional * permission to link the program and your derivative works with the * separately licensed software that they have either included with * the program or referenced in the documentation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See * the GNU General Public License, version 2.0, for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "shellcore/base_shell.h" #include <tuple> #include "mysqlshdk/libs/db/mysql/session.h" #include "mysqlshdk/libs/utils/fault_injection.h" #include "mysqlshdk/libs/utils/logger.h" #include "mysqlshdk/libs/utils/threads.h" #include "mysqlshdk/shellcore/shell_console.h" #include "shellcore/base_session.h" #include "shellcore/interrupt_handler.h" #include "shellcore/ishell_core.h" #include "shellcore/shell_notifications.h" #include "shellcore/shell_resultset_dumper.h" #include "utils/utils_file.h" #include "utils/utils_general.h" #include "utils/utils_path.h" #include "utils/utils_string.h" #ifdef HAVE_PYTHON #include "mysqlshdk/shellcore/provider_python.h" #include "shellcore/shell_python.h" #endif #ifdef HAVE_JIT_EXECUTOR #include "mysqlshdk/include/shellcore/shell_polyglot.h" #include "mysqlshdk/shellcore/provider_polyglot.h" #endif #include "shellcore/shell_sql.h" namespace mysqlsh { namespace { const std::map<std::string, std::string> k_shell_hooks = { {"ClassicResult", "dump"}, {"Result", "dump"}, {"DocResult", "dump"}, {"RowResult", "dump"}, {"SqlResult", "dump"}, {"CollectionAdd", "execute"}, {"CollectionFind", "execute"}, {"CollectionModify", "execute"}, {"CollectionRemove", "execute"}, {"TableDelete", "execute"}, {"TableInsert", "execute"}, {"TableSelect", "execute"}, {"TableUpdate", "execute"}, {"SqlExecute", "execute"}, }; } namespace { void print_diag(const std::string &s) { current_console()->print_diag(s); } void println(const std::string &s) { current_console()->println(s); } void print(const std::string &s) { current_console()->print(s); } void print_value(const shcore::Value &value, const std::string &s) { current_console()->print_value(value, s); } } // namespace Base_shell::Base_shell(const std::shared_ptr<Shell_options> &cmdline_options) : m_shell_options{cmdline_options}, _deferred_output(std::make_unique<std::string>()) { shcore::current_interrupt()->setup(); _input_mode = shcore::Input_state::Ok; // Whether the Shell_core is interactive or not will depend from the // interactive option which is set depending on how the shell is started _shell = std::make_shared<shcore::Shell_core>( m_shell_options.get()->get().interactive); _completer_object_registry = std::make_shared<shcore::completer::Object_registry>(); } Base_shell::~Base_shell() {} std::string Base_shell::get_shell_hook(const std::string &class_name) { auto item = k_shell_hooks.find(class_name); return item == k_shell_hooks.end() ? "" : item->second; } void Base_shell::finish_init() { current_console()->set_verbose(options().verbose_level); shcore::IShell_core::Mode initial_mode = options().initial_mode; if (initial_mode == shcore::IShell_core::Mode::None) { #ifdef HAVE_JS initial_mode = shcore::IShell_core::Mode::JavaScript; #else #ifdef HAVE_PYTHON initial_mode = shcore::IShell_core::Mode::Python; #else initial_mode = shcore::IShell_core::Mode::SQL; #endif #endif } // If we're not in the main thread, it means the shell object is created // again, because of this we don't need need any mode initialized by default // as it's very unlikely that we will be parsing any input directly. If // however this will be needed, switch_shell_mode can be used to fix this. if (!mysqlshdk::utils::in_main_thread()) { initial_mode = shcore::IShell_core::Mode::None; } // Final initialization that must happen outside the constructor switch_shell_mode(initial_mode, {}, true); // Pre-init the SQL completer, since it's used in places other than SQL mode _provider_sql.reset(new shcore::completer::Provider_sql()); _completer.add_provider( shcore::IShell_core::Mode_mask(shcore::IShell_core::Mode::SQL), _provider_sql); } // load scripts for standard locations in order to be able to implement standard // routines void Base_shell::init_scripts(shcore::Shell_core::Mode mode) { std::string extension; switch (mode) { case shcore::Shell_core::Mode::JavaScript: extension.append(".js"); break; case shcore::Shell_core::Mode::Python: extension.append(".py"); break; case shcore::Shell_core::Mode::None: case shcore::Shell_core::Mode::SQL: return; } try { std::vector<std::string> script_paths; const auto startup_script = "mysqlshrc" + extension; { // Checks existence of global startup script auto path = shcore::path::join_path(shcore::get_global_config_path(), startup_script); if (shcore::is_file(path)) { #ifdef WIN32 // Global configuration files are no longer supported in windows as the // %programdata% permissions are open-wide, which opens the door for // unauthorized users defining malicious startup files current_console()->print_warning( shcore::str_format("Global startup scripts are no longer " "supported, '%s' will be ignored.", path.c_str())); #else script_paths.emplace_back(std::move(path)); #endif } } { // Checks existence of startup script at MYSQLSH_HOME // Or the binary location if not a standard installation const auto home = shcore::get_mysqlx_home_path(); std::string path; if (!home.empty()) { path = shcore::path::join_path(home, "share", "mysqlsh", startup_script); } else { path = shcore::path::join_path(shcore::get_binary_folder(), startup_script); } if (shcore::is_file(path)) script_paths.emplace_back(std::move(path)); } { // Checks existence of user startup script auto path = shcore::path::join_path(shcore::get_user_config_path(), startup_script); if (shcore::is_file(path)) script_paths.emplace_back(std::move(path)); } for (const auto &script : script_paths) { std::ifstream stream(script); if (stream && stream.peek() != std::ifstream::traits_type::eof()) { process_file(script, {script}); } } } catch (const std::exception &e) { std::string error(e.what()); error += "\n"; print_diag(error); } } void Base_shell::load_default_modules(shcore::Shell_core::Mode mode) { std::string tmp; if (mode == shcore::Shell_core::Mode::JavaScript) { tmp = "var mysqlx = require('mysqlx');"; _shell->handle_input(tmp, _input_mode); tmp = "var mysql = require('mysql');"; _shell->handle_input(tmp, _input_mode); } else if (mode == shcore::Shell_core::Mode::Python) { tmp = "from mysqlsh import mysqlx"; _shell->handle_input(tmp, _input_mode); tmp = "from mysqlsh import mysql"; _shell->handle_input(tmp, _input_mode); } } /** * Simple, default prompt handler. * * @return prompt string */ std::string Base_shell::prompt() { std::string ret_val; switch (_shell->interactive_mode()) { case shcore::IShell_core::Mode::None: break; case shcore::IShell_core::Mode::SQL: ret_val = "mysql-sql> "; break; case shcore::IShell_core::Mode::JavaScript: ret_val = "mysql-js> "; break; case shcore::IShell_core::Mode::Python: ret_val = "mysql-py> "; break; } // The continuation prompt should be used if state != Ok if (_input_mode != shcore::Input_state::Ok) { if (ret_val.length() > 4) ret_val = std::string(ret_val.length() - 4, ' '); else ret_val.clear(); ret_val.append("... "); } return ret_val; } std::map<std::string, std::string> *Base_shell::prompt_variables() { update_prompt_variables(); return &_prompt_variables; } void Base_shell::request_prompt_variables_update(bool clear_cache) { const auto type = clear_cache ? Prompt_variables_update_type::CLEAR_CACHE : Prompt_variables_update_type::UPDATE; if (type > m_pending_update) { m_pending_update = type; } } void Base_shell::update_prompt_variables() { FI_SUPPRESS(mysql); FI_SUPPRESS(mysqlx); if (Prompt_variables_update_type::CLEAR_CACHE == m_pending_update) { _prompt_variables.clear(); } const auto session = _shell->get_dev_session(); if (_prompt_variables.empty()) { _prompt_variables["system_user"] = shcore::get_system_user(); if (session && session->is_open()) { const mysqlshdk::db::Connection_options &options( session->get_connection_options()); std::string socket; std::string port; _prompt_variables["ssl"] = session->get_ssl_cipher().empty() ? "" : "SSL"; _prompt_variables["uri"] = session->uri(mysqlshdk::db::uri::formats::user_transport()); _prompt_variables["user"] = options.has_user() ? options.get_user() : ""; _prompt_variables["host"] = options.has_host() ? options.get_host() : "localhost"; const auto &ssh = options.get_ssh_options(); if (ssh.has_data()) { _prompt_variables["ssh_host"] = ssh.get_host(); } else { _prompt_variables["ssh_host"] = ""; } if (session->get_member("__connection_info").descr().find("TCP") != std::string::npos) { port = options.has_port() ? std::to_string(options.get_port()) : ""; } else { if (options.has_socket()) socket = options.get_socket(); else socket = "default"; } if (session->session_type() == mysqlsh::SessionType::Classic) { _prompt_variables["session"] = "c"; if (socket.empty() && port.empty()) port = "3306"; } else { if (socket.empty() && port.empty()) port = "33060"; _prompt_variables["session"] = "x"; } _prompt_variables["port"] = port; _prompt_variables["socket"] = socket; _prompt_variables["node_type"] = session->get_node_type(); _prompt_variables["connection_id"] = std::to_string(session->get_connection_id()); } else { _prompt_variables["ssl"] = ""; _prompt_variables["uri"] = ""; _prompt_variables["user"] = ""; _prompt_variables["host"] = ""; _prompt_variables["port"] = ""; _prompt_variables["socket"] = ""; _prompt_variables["session"] = ""; _prompt_variables["node_type"] = ""; _prompt_variables["connection_id"] = ""; } } _prompt_variables["trx"] = ""; _prompt_variables["autocommit"] = ""; _prompt_variables["slow_query"] = ""; if (session && session->is_open()) { if (m_pending_update != Prompt_variables_update_type::NO_UPDATE) { try { _prompt_variables["schema"] = session->get_current_schema(); } catch (...) { _prompt_variables["schema"] = ""; } } if (auto s = std::dynamic_pointer_cast<mysqlshdk::db::mysql::Session>( session->get_core_session())) { auto status = s->get_server_status(); if (status & SERVER_STATUS_IN_TRANS) _prompt_variables["trx"] = "*"; if (status & SERVER_STATUS_IN_TRANS_READONLY) _prompt_variables["trx"] = "^"; if ((status & SERVER_STATUS_AUTOCOMMIT) == 0) _prompt_variables["autocommit"] = "."; if (status & SERVER_QUERY_WAS_SLOW) _prompt_variables["slow_query"] = "&"; } } else { if (m_pending_update != Prompt_variables_update_type::NO_UPDATE) { _prompt_variables["schema"] = ""; } } switch (_shell->interactive_mode()) { case shcore::IShell_core::Mode::None: break; case shcore::IShell_core::Mode::SQL: _prompt_variables["mode"] = "sql"; _prompt_variables["Mode"] = "SQL"; break; case shcore::IShell_core::Mode::JavaScript: _prompt_variables["mode"] = "js"; _prompt_variables["Mode"] = "JS"; break; case shcore::IShell_core::Mode::Python: _prompt_variables["mode"] = "py"; _prompt_variables["Mode"] = "Py"; break; } m_pending_update = Prompt_variables_update_type::NO_UPDATE; } bool Base_shell::switch_shell_mode(shcore::Shell_core::Mode mode, const std::vector<std::string> & /*args*/, bool initializing, bool prompt_variables_update) { shcore::Shell_core::Mode old_mode = _shell->interactive_mode(); bool lang_initialized = false; if (old_mode != mode) { _input_mode = shcore::Input_state::Ok; _input_buffer.clear(); switch (mode) { case shcore::Shell_core::Mode::None: break; case shcore::Shell_core::Mode::SQL: if (_shell->switch_mode(mode) && !initializing) println("Switching to SQL mode... Commands end with ;"); { auto sql = static_cast<shcore::Shell_sql *>(_shell->language_object(mode)); if (!sql->result_processor()) { sql->set_result_processor( std::bind(&Base_shell::process_sql_result, this, _1, _2)); lang_initialized = true; } } break; case shcore::Shell_core::Mode::JavaScript: #ifdef HAVE_JS if (_shell->switch_mode(mode) && !initializing) println("Switching to JavaScript mode..."); { auto js = static_cast<shcore::Shell_polyglot *>( _shell->language_object(mode)); if (!js->result_processor()) { js->set_result_processor( std::bind(&Base_shell::process_result, this, _1, _2)); _completer.add_provider( shcore::IShell_core::Mode_mask( shcore::IShell_core::Mode::JavaScript), std::unique_ptr<shcore::completer::Provider>( new shcore::completer::Provider_polyglot( _completer_object_registry, js->polyglot_context()))); lang_initialized = true; } } #else println("JavaScript mode is not supported, command ignored."); #endif break; case shcore::Shell_core::Mode::Python: #ifdef HAVE_PYTHON if (_shell->switch_mode(mode) && !initializing) println("Switching to Python mode..."); { auto py = static_cast<shcore::Shell_python *>( _shell->language_object(mode)); if (!py->result_processor()) { py->set_result_processor( std::bind(&Base_shell::process_result, this, _1, _2)); _completer.add_provider( shcore::IShell_core::Mode_mask( shcore::IShell_core::Mode::Python), std::unique_ptr<shcore::completer::Provider>( new shcore::completer::Provider_python( _completer_object_registry, py->python_context()))); lang_initialized = true; } } #else println("Python mode is not supported, command ignored."); #endif break; } // load scripts for standard locations if (lang_initialized) { load_default_modules(mode); init_scripts(mode); } } if (prompt_variables_update) request_prompt_variables_update(); return lang_initialized; } /** * Print output after the shell initialization is done (after Copyright info) * @param str text to be printed. */ void Base_shell::println_deferred(const std::string &str) { // This can't be called once the deferred output is flushed assert(_deferred_output != nullptr); _deferred_output->append(str + "\n"); } void Base_shell::clear_input() { _input_mode = shcore::Input_state::Ok; _input_buffer.clear(); _shell->clear_input(); } void Base_shell::process_line(const std::string &line) { if (_input_mode == shcore::Input_state::ContinuedBlock && line.empty()) { _input_mode = shcore::Input_state::Ok; } // Appends the line, no matter if it is an empty line _input_buffer.append(_shell->preprocess_input_line(line)); // Appends the new line if anything has been added to the buffer if (!_input_buffer.empty()) { _input_buffer.append("\n"); } if (_input_mode != shcore::Input_state::ContinuedBlock && !_input_buffer.empty()) { execute_buffered_code(false); } } void Base_shell::execute_buffered_code(bool flush) { std::string to_history; try { if (flush) { shcore::Scoped_callback reset_state([&]() { clear_input(); }); _shell->flush_input(_input_buffer); } else { _shell->handle_input(_input_buffer, _input_mode); } // Here we analyze the input mode as it was let after executing the code if (_input_mode == shcore::Input_state::Ok) { to_history = _shell->get_handled_input(); } } catch (shcore::Exception &exc) { print_value(shcore::Value(exc.error()), "error"); to_history = _input_buffer; } catch (const std::exception &exc) { std::string error(exc.what()); error += "\n"; print_diag(error); to_history = _input_buffer; } // TODO: Do we need this cleanup? i.e. in case of exceptions above?? // Clears the buffer if OK, if continued, buffer will contain // the non executed code if (_input_mode == shcore::Input_state::Ok) { _input_buffer.clear(); } if (!to_history.empty()) { notify_executed_statement(to_history); } } void Base_shell::flush_input() { execute_buffered_code(true); } void Base_shell::notify_executed_statement(const std::string &line) { shcore::Value::Map_type_ref data(new shcore::Value::Map_type()); (*data)["statement"] = shcore::Value(line); shcore::ShellNotifications::get()->notify("SN_STATEMENT_EXECUTED", nullptr, data); } void Base_shell::process_sql_result( const std::shared_ptr<mysqlshdk::db::IResult> &result, const shcore::Sql_result_info & /*info*/) { if (!result) { // Return value of undefined implies an error processing // TODO(alfredo) - signaling of errors should be moved down _shell->set_error_processing(); } } void Base_shell::print_result(const shcore::Value &result) { if (result) { if (result.type == shcore::Object) { auto object = result.as_object(); auto shell_hook = get_shell_hook(object->class_name()); if (!shell_hook.empty()) { if (object->has_member(shell_hook)) { shcore::Value hook_result = object->call(shell_hook, {}); // Recursive call to continue processing shell hooks if any process_result(hook_result, false); return; } } } // In JSON mode: the json representation is used for Object, Array and // Map For anything else a map is printed with the "value" key std::string tag; if (result.type != shcore::Object && result.type != shcore::Array && result.type != shcore::Map) tag = "value"; print_value(result, tag); } } void Base_shell::process_result(const shcore::Value &result, bool got_error) { assert(_shell->interactive_mode() != shcore::Shell_core::Mode::SQL); if (options().interactive) print_result(result); if (got_error) _shell->set_error_processing(); } int Base_shell::process_file(const std::string &path, const std::vector<std::string> &argv) { // Default return value will be 1 indicating there were errors int ret_val = 1; if (path.empty()) { print_diag("Invalid filename"); } else { std::string file = shcore::path::expand_user(path); if (shcore::is_folder(file)) { print_diag( shcore::str_format("Failed to open file: '%s' is a " "directory\n", file.c_str())); return ret_val; } #ifdef _WIN32 std::ifstream s(shcore::utf8_to_wide(file)); #else std::ifstream s(file.c_str()); #endif if (!s.fail()) { // The return value now depends on the stream processing ret_val = process_stream(s, file, argv); // When force is used, we do not care of the processing // errors if (options().force) ret_val = 0; s.close(); } else { // TODO: add a log entry once logging is print_diag(shcore::str_format("Failed to open file '%s', error: %s\n", file.c_str(), std::strerror(errno))); } } return ret_val; } int Base_shell::run_module(const std::string &module, const std::vector<std::string> &argv) { if (module.empty()) { print_diag("Invalid module name"); return 1; } _shell->execute_module(module, argv); return 0; } int Base_shell::process_stream(std::istream &stream, const std::string &source, const std::vector<std::string> &argv, bool force_batch) { _shell->set_argv(argv); // If interactive is set, it means that the shell was started with the option // to Emulate interactive mode while processing the stream if (!force_batch && options().interactive) { if (options().full_interactive) print(prompt()); bool comment_first_js_line = _shell->interactive_mode() == shcore::IShell_core::Mode::JavaScript; std::string line; while (!stream.eof()) { line.clear(); shcore::getline(stream, line); // When processing JavaScript files, validates the very first line to // start with #! If that's the case, it is replaced by a comment indicator // // if (comment_first_js_line && line.size() > 1 && line[0] == '#' && line[1] == '!') line.replace(0, 2, "//"); comment_first_js_line = false; if (options().full_interactive) println(line); process_line(line); if (options().full_interactive) print(prompt()); } // If stream ended, and state is in continuation mode, the execution is // forced if (_input_mode == shcore::Input_state::ContinuedBlock || _input_mode == shcore::Input_state::ContinuedSingle) { flush_input(); } // Being interactive, we do not care about the return value return 0; } else { return _shell->process_stream(stream, source); } } void Base_shell::set_global_object( const std::string &name, const std::shared_ptr<shcore::Cpp_object_bridge> &object, shcore::IShell_core::Mode_mask modes) { if (object) { _shell->set_global(name, shcore::Value(object), modes); } else { _shell->set_global(name, shcore::Value(), modes); } } } // namespace mysqlsh